mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
436 lines
16 KiB
TypeScript
436 lines
16 KiB
TypeScript
import { MigrationInterface, QueryRunner, EntityManager } from 'typeorm';
|
|
import { AppVersion } from '@entities/app_version.entity';
|
|
import { Component } from '@entities/component.entity';
|
|
import { Page } from '@entities/page.entity';
|
|
import { Layout } from '@entities/layout.entity';
|
|
import { EventHandler, Target } from '@entities/event_handler.entity';
|
|
import { DataQuery } from '@entities/data_query.entity';
|
|
import { dbTransactionWrap } from '@helpers/database.helper';
|
|
import { MigrationProgress, processDataInBatches } from '@helpers/migration.helper';
|
|
import { v4 as uuid } from 'uuid';
|
|
|
|
interface AppResourceMappings {
|
|
pagesMapping: Record<string, string>;
|
|
componentsMapping: Record<string, string>;
|
|
}
|
|
|
|
export class MigrateAppsDefinitionSchemaTransition1697473340856 implements MigrationInterface {
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
const totalAppVersions = await queryRunner.manager.count(AppVersion);
|
|
|
|
if (totalAppVersions === 0) {
|
|
console.log('No app versions found. Skipping migration.');
|
|
return;
|
|
}
|
|
|
|
await this.migrateAppsDefinition(queryRunner.manager);
|
|
}
|
|
|
|
private async migrateAppsDefinition(entityManager: EntityManager, queryRunner?: QueryRunner): Promise<void> {
|
|
return dbTransactionWrap(async (entityManager: EntityManager) => {
|
|
const appVersionRepository = entityManager.getRepository(AppVersion);
|
|
const appVersions = await appVersionRepository.query(
|
|
`SELECT id FROM app_versions WHERE definition->>'pages' IS NOT NULL ORDER BY updated_at DESC`
|
|
);
|
|
const totalVersions = appVersions.length;
|
|
|
|
const startTime = new Date().getTime();
|
|
const migrationProgress = new MigrationProgress(
|
|
'MigrateAppsDefinitionSchemaTransition1697473340856',
|
|
totalVersions
|
|
);
|
|
|
|
const batchSize = 100; // Number of apps to migrate at a time
|
|
|
|
await processDataInBatches(
|
|
entityManager,
|
|
async (entityManager: EntityManager, skip: number, take: number) => {
|
|
const ids = appVersions.slice(skip, skip + take).map((appVersion) => appVersion.id);
|
|
if (!ids || ids.length === 0) {
|
|
return [];
|
|
}
|
|
return entityManager.query(
|
|
`SELECT * FROM app_versions WHERE id IN (${ids.map((id) => `'${id}'`).join(',')})`
|
|
);
|
|
},
|
|
async (entityManager: EntityManager, versions: AppVersion[]) => {
|
|
await this.processVersions(entityManager, versions, migrationProgress);
|
|
},
|
|
batchSize
|
|
);
|
|
|
|
const endTime = new Date().getTime();
|
|
console.log(`Migration time taken: ${(endTime - startTime) / 1000} seconds`);
|
|
}, entityManager);
|
|
}
|
|
|
|
private async processVersions(
|
|
entityManager: EntityManager,
|
|
versions: AppVersion[],
|
|
migrationProgress: MigrationProgress
|
|
) {
|
|
for (const version of versions) {
|
|
const definition = version['definition'];
|
|
|
|
const dataQueriesRepository = entityManager.getRepository(DataQuery);
|
|
const dataQueries = await dataQueriesRepository.find({
|
|
where: { appVersionId: version.id },
|
|
});
|
|
|
|
let updateHomepageId = null;
|
|
|
|
const appResourceMappings: AppResourceMappings = {
|
|
pagesMapping: {},
|
|
componentsMapping: {},
|
|
};
|
|
if (definition?.pages && Object.keys(definition?.pages).length > 0) {
|
|
for (const pageId of Object.keys(definition?.pages)) {
|
|
const page = definition.pages[pageId];
|
|
const pagePositionInTheList = Object.keys(definition?.pages).indexOf(pageId);
|
|
const pageEvents = page.events || [];
|
|
const pageComponents = page.components || {};
|
|
|
|
const isHomepage = (definition['homePageId'] as any) === pageId;
|
|
|
|
const componentEvents = [];
|
|
const componentLayouts = [];
|
|
let savedComponents = [];
|
|
const transformedComponents = this.transformComponentData(
|
|
pageComponents,
|
|
componentEvents,
|
|
appResourceMappings.componentsMapping
|
|
);
|
|
|
|
const newPage = entityManager.create(Page, {
|
|
name: page.name || page.handle || pageId,
|
|
handle: page.handle || pageId,
|
|
appVersionId: version.id,
|
|
disabled: page.disabled || false,
|
|
hidden: page.hidden || false,
|
|
index: pagePositionInTheList,
|
|
});
|
|
|
|
const pageCreated = await entityManager.save(newPage);
|
|
|
|
appResourceMappings.pagesMapping[pageId] = pageCreated.id;
|
|
|
|
if (Array.isArray(transformedComponents) && transformedComponents.length > 0) {
|
|
transformedComponents.forEach((component) => {
|
|
component.page = pageCreated;
|
|
});
|
|
|
|
savedComponents = await entityManager.save(Component, transformedComponents);
|
|
|
|
for (const componentId in pageComponents) {
|
|
const componentLayout = pageComponents[componentId]['layouts'];
|
|
|
|
if (componentLayout && appResourceMappings.componentsMapping[componentId]) {
|
|
for (const type in componentLayout) {
|
|
const layout = componentLayout[type];
|
|
const newLayout = new Layout();
|
|
newLayout.type = type;
|
|
newLayout.top = layout.top;
|
|
newLayout.left = layout.left;
|
|
newLayout.width = layout.width;
|
|
newLayout.height = layout.height;
|
|
newLayout.componentId = appResourceMappings.componentsMapping[componentId];
|
|
|
|
componentLayouts.push(newLayout);
|
|
}
|
|
}
|
|
}
|
|
|
|
await entityManager.save(Layout, componentLayouts);
|
|
}
|
|
|
|
if (Array.isArray(pageEvents) && pageEvents.length > 0) {
|
|
pageEvents.forEach(async (event, index) => {
|
|
const newEvent = {
|
|
name: event.eventId || `${pageCreated.name} Page Event ${index}`,
|
|
sourceId: pageCreated.id,
|
|
target: Target.page,
|
|
event: event,
|
|
index: index,
|
|
appVersionId: version.id,
|
|
};
|
|
|
|
await entityManager.save(EventHandler, newEvent);
|
|
});
|
|
}
|
|
|
|
componentEvents.forEach((eventObj) => {
|
|
if (eventObj.event?.length === 0) return;
|
|
|
|
if (Array.isArray(eventObj.event) && eventObj.event.length > 0) {
|
|
eventObj.event.forEach(async (event, index) => {
|
|
const newEvent = {
|
|
name: event.eventId || `event ${index}`,
|
|
sourceId: appResourceMappings.componentsMapping[eventObj.componentId],
|
|
target: Target.component,
|
|
event: event,
|
|
index: index,
|
|
appVersionId: version.id,
|
|
};
|
|
|
|
await entityManager.save(EventHandler, newEvent);
|
|
});
|
|
}
|
|
});
|
|
|
|
if (savedComponents.length > 0) {
|
|
savedComponents.forEach(async (component) => {
|
|
if (component.type === 'Table') {
|
|
const tableActions = component?.properties?.actions?.value || [];
|
|
const tableColumns = component?.properties?.columns?.value || [];
|
|
const tableActionAndColumnEvents = [];
|
|
|
|
tableActions.forEach((action) => {
|
|
const actionEvents = action?.events || [];
|
|
|
|
if (!actionEvents || !Array.isArray(actionEvents)) return;
|
|
|
|
actionEvents.forEach((event, index) => {
|
|
tableActionAndColumnEvents.push({
|
|
name: event.eventId,
|
|
sourceId: component.id,
|
|
target: Target.tableAction,
|
|
event: { ...event, ref: action.name },
|
|
index: index,
|
|
appVersionId: version.id,
|
|
});
|
|
});
|
|
});
|
|
|
|
tableColumns.forEach((column) => {
|
|
if (column?.columnType !== 'toggle') return;
|
|
const columnEvents = column?.events || [];
|
|
|
|
if (!columnEvents || !Array.isArray(columnEvents)) return;
|
|
|
|
columnEvents.forEach((event, index) => {
|
|
tableActionAndColumnEvents.push({
|
|
name: event.eventId || `event ${index}`,
|
|
sourceId: component.id,
|
|
target: Target.tableColumn,
|
|
event: { ...event, ref: column.name },
|
|
index: index,
|
|
appVersionId: version.id,
|
|
});
|
|
});
|
|
});
|
|
|
|
await entityManager.save(EventHandler, tableActionAndColumnEvents);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (isHomepage) {
|
|
updateHomepageId = pageCreated.id;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const dataQuery of dataQueries) {
|
|
const queryEvents = dataQuery?.options?.events || [];
|
|
|
|
if (Array.isArray(queryEvents) && queryEvents.length > 0) {
|
|
queryEvents.forEach(async (event, index) => {
|
|
const newEvent = {
|
|
name: event.eventId || `${dataQuery.name} Query Event ${index}`,
|
|
sourceId: dataQuery.id,
|
|
target: Target.dataQuery,
|
|
event: event,
|
|
index: index,
|
|
appVersionId: version.id,
|
|
};
|
|
|
|
await entityManager.save(EventHandler, newEvent);
|
|
});
|
|
}
|
|
}
|
|
|
|
let globalSettings = definition?.globalSettings;
|
|
if (!definition?.globalSettings) {
|
|
globalSettings = {
|
|
hideHeader: false,
|
|
appInMaintenance: false,
|
|
canvasMaxWidth: 100,
|
|
canvasMaxWidthType: '%',
|
|
canvasMaxHeight: 2400,
|
|
canvasBackgroundColor: '#edeff5',
|
|
backgroundFxQuery: '',
|
|
};
|
|
}
|
|
|
|
await entityManager.update(
|
|
AppVersion,
|
|
{ id: version.id },
|
|
{
|
|
homePageId: updateHomepageId,
|
|
showViewerNavigation: definition?.showViewerNavigation || true,
|
|
globalSettings: globalSettings,
|
|
}
|
|
);
|
|
|
|
await this.updateEventActionsForNewVersionWithNewMappingIds(
|
|
entityManager,
|
|
version.id,
|
|
appResourceMappings.componentsMapping,
|
|
appResourceMappings.pagesMapping
|
|
);
|
|
|
|
migrationProgress.show();
|
|
}
|
|
}
|
|
|
|
async updateEventActionsForNewVersionWithNewMappingIds(
|
|
manager: EntityManager,
|
|
versionId: string,
|
|
oldComponentToNewComponentMapping: Record<string, unknown>,
|
|
oldPageToNewPageMapping: Record<string, unknown>
|
|
) {
|
|
const allEvents = await manager.find(EventHandler, {
|
|
where: { appVersionId: versionId },
|
|
});
|
|
|
|
if (!allEvents || allEvents.length === 0) return;
|
|
|
|
for (const event of allEvents) {
|
|
const eventDefinition = event.event;
|
|
|
|
if (eventDefinition?.actionId === 'switch-page') {
|
|
eventDefinition.pageId = oldPageToNewPageMapping[eventDefinition.pageId];
|
|
}
|
|
|
|
if (eventDefinition?.actionId === 'control-component') {
|
|
eventDefinition.componentId = oldComponentToNewComponentMapping[eventDefinition.componentId];
|
|
}
|
|
|
|
if (eventDefinition?.actionId == 'show-modal' || eventDefinition?.actionId === 'close-modal') {
|
|
eventDefinition.modal = oldComponentToNewComponentMapping[eventDefinition.modal];
|
|
}
|
|
|
|
event.event = eventDefinition;
|
|
|
|
await manager.save(event);
|
|
}
|
|
}
|
|
|
|
private transformComponentData(
|
|
data: object,
|
|
componentEvents: any[],
|
|
componentsMapping: Record<string, string>
|
|
): Component[] {
|
|
if (!data || Object.keys(data).length == 0) return [];
|
|
|
|
const transformedComponents: Component[] = [];
|
|
|
|
const allComponents = Object.keys(data).map((key) => {
|
|
if (!key) return;
|
|
|
|
return {
|
|
id: key,
|
|
...data[key],
|
|
};
|
|
});
|
|
|
|
if (!allComponents || allComponents.length === 0) return [];
|
|
|
|
for (const componentId in data) {
|
|
if (!componentId || !data[componentId]) return;
|
|
|
|
const component = data[componentId];
|
|
|
|
const componentData = component['component'];
|
|
|
|
if (!componentData?.component) return;
|
|
|
|
let skipComponent = false;
|
|
const transformedComponent: Component = new Component();
|
|
|
|
let parentId = component?.parent ? component.parent : null;
|
|
|
|
const isParentTabOrCalendar = this.isChildOfTabsOrCalendar(component, allComponents, parentId);
|
|
|
|
if (isParentTabOrCalendar) {
|
|
const childTabId = component?.parent.split('-')[component?.parent.split('-').length - 1];
|
|
const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
|
|
const mappedParentId = componentsMapping[_parentId];
|
|
|
|
parentId = mappedParentId ? `${mappedParentId}-${childTabId}` : null;
|
|
} else if (this.isChildOfKanbanModal(component, allComponents, parentId)) {
|
|
const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
|
|
const mappedParentId = componentsMapping[_parentId];
|
|
|
|
parentId = mappedParentId ? `${mappedParentId}-modal` : null;
|
|
} else {
|
|
if (component.parent && !componentsMapping[parentId]) {
|
|
skipComponent = true;
|
|
}
|
|
parentId = componentsMapping[parentId] ? componentsMapping[parentId] : null;
|
|
}
|
|
|
|
if (!skipComponent) {
|
|
transformedComponent.id = uuid();
|
|
transformedComponent.name = componentData.name || componentId;
|
|
transformedComponent.type = componentData.component;
|
|
transformedComponent.properties = componentData?.definition?.properties || {};
|
|
transformedComponent.styles = componentData?.definition?.styles || {};
|
|
transformedComponent.validation = componentData?.definition?.validation || {};
|
|
transformedComponent.general = componentData?.definition?.general || {};
|
|
transformedComponent.generalStyles = componentData?.definition?.generalStyles || {};
|
|
transformedComponent.displayPreferences = componentData?.definition?.others || {};
|
|
transformedComponent.parent = component?.parent ? parentId : null;
|
|
|
|
transformedComponents.push(transformedComponent);
|
|
|
|
componentEvents.push({
|
|
componentId: componentId,
|
|
event: componentData?.definition?.events || [],
|
|
});
|
|
componentsMapping[componentId] = transformedComponent.id;
|
|
}
|
|
}
|
|
|
|
return transformedComponents;
|
|
}
|
|
|
|
isChildOfTabsOrCalendar = (component, allComponents = [], componentParentId = undefined) => {
|
|
if (!component || allComponents.length === 0) return false;
|
|
|
|
if (componentParentId && component) {
|
|
const parentId = component?.parent?.split('-').slice(0, -1).join('-');
|
|
|
|
const parentComponent = allComponents.find((comp) => comp.id === parentId);
|
|
|
|
if (parentComponent) {
|
|
return parentComponent?.component?.component === 'Tabs' || parentComponent?.component?.component === 'Calendar';
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
isChildOfKanbanModal = (component, allComponents = [], componentParentId = undefined) => {
|
|
if (!component || !componentParentId || !componentParentId.includes('modal')) return false;
|
|
const parentId = component?.parent?.split('-').slice(0, -1).join('-');
|
|
const parentComponent = allComponents.find((comp) => comp.id === parentId);
|
|
|
|
if (parentComponent) {
|
|
return parentComponent?.component?.component === 'Kanban';
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query('DELETE FROM page');
|
|
await queryRunner.query('DELETE FROM component');
|
|
await queryRunner.query('DELETE FROM layout');
|
|
await queryRunner.query('DELETE FROM event_handler');
|
|
|
|
await queryRunner.query('ALTER TABLE app_versions DROP COLUMN IF EXISTS homePageId');
|
|
await queryRunner.query('ALTER TABLE app_versions DROP COLUMN IF EXISTS globalSettings');
|
|
await queryRunner.query('ALTER TABLE app_versions DROP COLUMN IF EXISTS showViewerNavigation');
|
|
}
|
|
}
|