Shared TypeScript library for the Lightdash platform containing common types, utilities, and business logic for analytics workflows
Overall
score
72%
Evaluation — 72%
↑ 1.09xAgent success when using this tile
Content promotion types for moving charts, dashboards, and spaces between environments (e.g., development, staging, production).
Actions that can be performed during content promotion.
enum PromotionAction {
NO_CHANGES = 'no changes',
CREATE = 'create',
UPDATE = 'update',
DELETE = 'delete'
}Space definition for promotion, excluding user access information.
type PromotedSpace = Omit<SpaceSummary, 'userAccess'>;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;
}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;
}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;
}>;
}Response type for promoting a single chart.
interface ApiPromoteChartResponse {
status: 'ok';
results: SavedChartDAO;
}Response type for promoting a single dashboard.
interface ApiPromoteDashboardResponse {
status: 'ok';
results: DashboardDAO;
}Response type for fetching promotion changes preview.
interface ApiPromotionChangesResponse {
status: 'ok';
results: PromotionChanges;
}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
};
}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;
}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;
}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
};
}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--commondocs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20