CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-lightdash--common

Shared TypeScript library for the Lightdash platform containing common types, utilities, and business logic for analytics workflows

Overall
score

72%

Evaluation72%

1.09x

Agent success when using this tile

Overview
Eval results
Files

promotion.mddocs/api/types/

Content Promotion

Content promotion types for moving charts, dashboards, and spaces between environments (e.g., development, staging, production).

Capabilities

Promotion Action

Actions that can be performed during content promotion.

enum PromotionAction {
  NO_CHANGES = 'no changes',
  CREATE = 'create',
  UPDATE = 'update',
  DELETE = 'delete'
}

Promoted Space

Space definition for promotion, excluding user access information.

type PromotedSpace = Omit<SpaceSummary, 'userAccess'>;

Promoted Dashboard

Dashboard with space location information for promotion.

interface PromotedDashboard extends DashboardDAO {
  /** URL-friendly slug of the space */
  spaceSlug: string;

  /** Full path to the space */
  spacePath: string;
}

Promoted Chart

Chart with space location and migration tracking information.

interface PromotedChart extends SavedChartDAO {
  /** URL-friendly slug of the space */
  spaceSlug: string;

  /** Full path to the space */
  spacePath: string;

  /** UUID from the source environment */
  oldUuid: string;
}

Promotion Changes

Complete set of changes for a promotion operation.

interface PromotionChanges {
  /** Space changes to be applied */
  spaces: Array<{
    action: PromotionAction;
    data: PromotedSpace;
  }>;

  /** Dashboard changes to be applied */
  dashboards: Array<{
    action: PromotionAction;
    data: PromotedDashboard;
  }>;

  /** Chart changes to be applied */
  charts: Array<{
    action: PromotionAction;
    data: PromotedChart;
  }>;
}

API Promote Chart Response

Response type for promoting a single chart.

interface ApiPromoteChartResponse {
  status: 'ok';
  results: SavedChartDAO;
}

API Promote Dashboard Response

Response type for promoting a single dashboard.

interface ApiPromoteDashboardResponse {
  status: 'ok';
  results: DashboardDAO;
}

API Promotion Changes Response

Response type for fetching promotion changes preview.

interface ApiPromotionChangesResponse {
  status: 'ok';
  results: PromotionChanges;
}

Usage Examples

Preview Promotion Changes

import {
  type PromotionChanges,
  type ApiPromotionChangesResponse,
  PromotionAction
} from '@lightdash/common';

// Get preview of changes before promotion
async function getPromotionPreview(
  sourceProjectUuid: string,
  targetProjectUuid: string
): Promise<PromotionChanges> {
  const response: ApiPromotionChangesResponse = await api.get(
    `/projects/${sourceProjectUuid}/promotion-changes`,
    { params: { targetProjectUuid } }
  );

  return response.results;
}

// Analyze promotion changes
function analyzePromotionChanges(changes: PromotionChanges): {
  creates: number;
  updates: number;
  deletes: number;
  noChanges: number;
} {
  const allChanges = [
    ...changes.spaces,
    ...changes.dashboards,
    ...changes.charts
  ];

  return {
    creates: allChanges.filter(c => c.action === PromotionAction.CREATE).length,
    updates: allChanges.filter(c => c.action === PromotionAction.UPDATE).length,
    deletes: allChanges.filter(c => c.action === PromotionAction.DELETE).length,
    noChanges: allChanges.filter(c => c.action === PromotionAction.NO_CHANGES).length
  };
}

Promote Individual Content

import {
  type ApiPromoteChartResponse,
  type ApiPromoteDashboardResponse
} from '@lightdash/common';

// Promote a single chart
async function promoteChart(
  sourceProjectUuid: string,
  chartUuid: string,
  targetProjectUuid: string
): Promise<SavedChartDAO> {
  const response: ApiPromoteChartResponse = await api.post(
    `/projects/${sourceProjectUuid}/charts/${chartUuid}/promote`,
    { targetProjectUuid }
  );

  return response.results;
}

// Promote a single dashboard
async function promoteDashboard(
  sourceProjectUuid: string,
  dashboardUuid: string,
  targetProjectUuid: string
): Promise<DashboardDAO> {
  const response: ApiPromoteDashboardResponse = await api.post(
    `/projects/${sourceProjectUuid}/dashboards/${dashboardUuid}/promote`,
    { targetProjectUuid }
  );

  return response.results;
}

Filter and Group Changes

import { type PromotionChanges, PromotionAction } from '@lightdash/common';

// Get only destructive changes (deletes and updates)
function getDestructiveChanges(changes: PromotionChanges): {
  spaces: number;
  dashboards: number;
  charts: number;
} {
  return {
    spaces: changes.spaces.filter(c =>
      c.action === PromotionAction.DELETE ||
      c.action === PromotionAction.UPDATE
    ).length,
    dashboards: changes.dashboards.filter(c =>
      c.action === PromotionAction.DELETE ||
      c.action === PromotionAction.UPDATE
    ).length,
    charts: changes.charts.filter(c =>
      c.action === PromotionAction.DELETE ||
      c.action === PromotionAction.UPDATE
    ).length
  };
}

// Get new content to be created
function getNewContent(changes: PromotionChanges) {
  return {
    spaces: changes.spaces
      .filter(c => c.action === PromotionAction.CREATE)
      .map(c => c.data),
    dashboards: changes.dashboards
      .filter(c => c.action === PromotionAction.CREATE)
      .map(c => c.data),
    charts: changes.charts
      .filter(c => c.action === PromotionAction.CREATE)
      .map(c => c.data)
  };
}

// Group changes by space
function groupChangesBySpace(changes: PromotionChanges): Map<string, {
  dashboards: PromotedDashboard[];
  charts: PromotedChart[];
}> {
  const spaceMap = new Map<string, {
    dashboards: PromotedDashboard[];
    charts: PromotedChart[];
  }>();

  // Group dashboards by space
  changes.dashboards.forEach(change => {
    if (change.action !== PromotionAction.DELETE) {
      const spaceKey = change.data.spaceSlug;
      const existing = spaceMap.get(spaceKey) ?? { dashboards: [], charts: [] };
      existing.dashboards.push(change.data);
      spaceMap.set(spaceKey, existing);
    }
  });

  // Group charts by space
  changes.charts.forEach(change => {
    if (change.action !== PromotionAction.DELETE) {
      const spaceKey = change.data.spaceSlug;
      const existing = spaceMap.get(spaceKey) ?? { dashboards: [], charts: [] };
      existing.charts.push(change.data);
      spaceMap.set(spaceKey, existing);
    }
  });

  return spaceMap;
}

Promotion Validation

import { type PromotionChanges, PromotionAction } from '@lightdash/common';

interface PromotionValidation {
  isValid: boolean;
  warnings: string[];
  errors: string[];
}

function validatePromotion(changes: PromotionChanges): PromotionValidation {
  const warnings: string[] = [];
  const errors: string[] = [];

  // Check for deletions
  const deletions = {
    spaces: changes.spaces.filter(c => c.action === PromotionAction.DELETE).length,
    dashboards: changes.dashboards.filter(c => c.action === PromotionAction.DELETE).length,
    charts: changes.charts.filter(c => c.action === PromotionAction.DELETE).length
  };

  if (deletions.spaces > 0 || deletions.dashboards > 0 || deletions.charts > 0) {
    warnings.push(
      `This promotion will delete ${deletions.spaces} spaces, ` +
      `${deletions.dashboards} dashboards, and ${deletions.charts} charts`
    );
  }

  // Check for large promotions
  const totalChanges =
    changes.spaces.length +
    changes.dashboards.length +
    changes.charts.length;

  if (totalChanges > 50) {
    warnings.push(`Large promotion with ${totalChanges} changes`);
  }

  // Check if any spaces need to be created
  const spacesToCreate = changes.spaces.filter(c =>
    c.action === PromotionAction.CREATE
  ).length;

  if (spacesToCreate > 0) {
    warnings.push(`${spacesToCreate} new spaces will be created`);
  }

  return {
    isValid: errors.length === 0,
    warnings,
    errors
  };
}

Tracking Promoted Content

import { type PromotedChart } from '@lightdash/common';

// Track relationship between source and target charts
function buildChartMigrationMap(
  promotedCharts: PromotedChart[]
): Map<string, string> {
  const migrationMap = new Map<string, string>();

  promotedCharts.forEach(chart => {
    migrationMap.set(chart.oldUuid, chart.uuid);
  });

  return migrationMap;
}

// Find promoted chart by old UUID
function findPromotedChart(
  charts: PromotedChart[],
  oldUuid: string
): PromotedChart | undefined {
  return charts.find(chart => chart.oldUuid === oldUuid);
}

Install with Tessl CLI

npx tessl i tessl/npm-lightdash--common

docs

api

index.md

tile.json