Improve upgrade command and prepare 1.5 release (#14325)
In this PR: - refactor the upgrade command / upgrade command runner to keep upgrade command as light as possible (all wrapping logic should go to upgrade command runner) - prevent any upgrade if there is at least one workspace.version < previsousVersion ==> this leads to corrupted state where only core migrations are run if the self-hoster is skipping a version
This commit is contained in:
+1
-1
@@ -41,7 +41,7 @@ export type AddPositionsToWorkflowVersionsAndWorkflowRunsOptions =
|
||||
name: 'upgrade:1-5:add-positions-to-workflow-versions-and-workflow-runs',
|
||||
description: 'Add positions to workflow versions and workflow runs',
|
||||
})
|
||||
export class AddPositionsToWorkflowVersionsAndWorkflowRuns extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
export class AddPositionsToWorkflowVersionsAndWorkflowRunsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace)
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
|
||||
+668
@@ -0,0 +1,668 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { type ViewFilterOperand as SharedViewFilterOperand } from 'twenty-shared/types';
|
||||
import { DataSource, In, Repository, type QueryRunner } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/core-modules/view/entities/view-filter-group.entity';
|
||||
import { ViewFilterEntity } from 'src/engine/core-modules/view/entities/view-filter.entity';
|
||||
import { ViewGroupEntity } from 'src/engine/core-modules/view/entities/view-group.entity';
|
||||
import { ViewSortEntity } from 'src/engine/core-modules/view/entities/view-sort.entity';
|
||||
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
|
||||
import { type ViewFilterGroupLogicalOperator } from 'src/engine/core-modules/view/enums/view-filter-group-logical-operator';
|
||||
import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum';
|
||||
import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in';
|
||||
import { type ViewSortDirection } from 'src/engine/core-modules/view/enums/view-sort-direction';
|
||||
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||
import { type ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||
import { type ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||
import { type ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||
import { type ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||
import { type ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
import { convertViewFilterOperandToCoreOperand } from 'src/modules/view/utils/convert-view-filter-operand-to-core-operand.util';
|
||||
import { convertViewFilterWorkspaceValueToCoreValue } from 'src/modules/view/utils/convert-view-filter-workspace-value-to-core-value';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-5:migrate-views-to-core',
|
||||
description: 'Migrate views from workspace schemas to core schema',
|
||||
})
|
||||
export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace)
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Migrating views to core schema for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
|
||||
try {
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await this.migrateViews(
|
||||
workspaceId,
|
||||
options.dryRun ?? false,
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would migrate views to core schema for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
await queryRunner.commitTransaction();
|
||||
this.logger.log(
|
||||
`Successfully migrated views to core schema for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (queryRunner.isTransactionActive) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Transaction rolled back for workspace ${workspaceId} due to error: ${error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to migrate views to core schema for workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateViews(
|
||||
workspaceId: string,
|
||||
dryRun: boolean,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
const workspaceViewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'view',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workspaceViews = await workspaceViewRepository.find({
|
||||
relations: [
|
||||
'viewFields',
|
||||
'viewFilters',
|
||||
'viewSorts',
|
||||
'viewGroups',
|
||||
'viewFilterGroups',
|
||||
],
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
const {
|
||||
corruptedViewIds,
|
||||
corruptedViewFieldIds,
|
||||
corruptedViewFilterIds,
|
||||
corruptedViewSortIds,
|
||||
corruptedViewGroupIds,
|
||||
} = await this.fetchAndDeleteCorruptedViewResources(
|
||||
workspaceId,
|
||||
workspaceViews,
|
||||
queryRunner,
|
||||
dryRun,
|
||||
);
|
||||
|
||||
const workspaceViewWithoutOrphansResources = workspaceViews
|
||||
.filter((view) => !corruptedViewIds.includes(view.id))
|
||||
.map((view) => ({
|
||||
...view,
|
||||
viewFields: view.viewFields.filter(
|
||||
(viewField) => !corruptedViewFieldIds.includes(viewField.id),
|
||||
),
|
||||
viewFilters: view.viewFilters.filter(
|
||||
(viewFilter) => !corruptedViewFilterIds.includes(viewFilter.id),
|
||||
),
|
||||
viewSorts: view.viewSorts.filter(
|
||||
(viewSort) => !corruptedViewSortIds.includes(viewSort.id),
|
||||
),
|
||||
viewGroups: view.viewGroups.filter(
|
||||
(viewGroup) => !corruptedViewGroupIds.includes(viewGroup.id),
|
||||
),
|
||||
}));
|
||||
|
||||
if (workspaceViewWithoutOrphansResources.length === 0) {
|
||||
this.logger.log(`No views to migrate for workspace ${workspaceId}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'DRY RUN: ' : ''}Found ${workspaceViews.length} views to migrate for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
for (const view of workspaceViewWithoutOrphansResources) {
|
||||
const deletedStatus = view.deletedAt ? ' (DELETED)' : '';
|
||||
|
||||
this.logger.log(
|
||||
`DRY RUN: Would migrate view ${view.id} (${view.name})${deletedStatus} with ${view.viewFields.length} fields, ${view.viewFilters.length} filters, ${view.viewSorts.length} sorts, ${view.viewGroups.length} groups, ${view.viewFilterGroups.length} filter groups`,
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.deleteExistingCoreViewObjects(workspaceId, queryRunner, dryRun);
|
||||
|
||||
for (const workspaceView of workspaceViewWithoutOrphansResources) {
|
||||
await this.migrateViewEntity(workspaceView, workspaceId, queryRunner);
|
||||
|
||||
if (workspaceView.viewFields?.length > 0) {
|
||||
await this.migrateViewFields(
|
||||
workspaceView.viewFields,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaceView.viewFilterGroups?.length > 0) {
|
||||
await this.migrateViewFilterGroups(
|
||||
workspaceView.viewFilterGroups,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaceView.viewFilters?.length > 0) {
|
||||
await this.migrateViewFilters(
|
||||
workspaceView.viewFilters,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaceView.viewSorts?.length > 0) {
|
||||
await this.migrateViewSorts(
|
||||
workspaceView.viewSorts,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
if (workspaceView.viewGroups?.length > 0) {
|
||||
await this.migrateViewGroups(
|
||||
workspaceView.viewGroups,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
);
|
||||
}
|
||||
|
||||
const deletedStatus = workspaceView.deletedAt ? ' (DELETED)' : '';
|
||||
|
||||
this.logger.log(
|
||||
`Migrated view ${workspaceView.id} (${workspaceView.name})${deletedStatus} to core schema`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchAndDeleteCorruptedViewResources(
|
||||
workspaceId: string,
|
||||
views: ViewWorkspaceEntity[],
|
||||
queryRunner: QueryRunner,
|
||||
dryRun: boolean,
|
||||
): Promise<{
|
||||
corruptedViewIds: string[];
|
||||
corruptedViewFieldIds: string[];
|
||||
corruptedViewFilterIds: string[];
|
||||
corruptedViewSortIds: string[];
|
||||
corruptedViewGroupIds: string[];
|
||||
}> {
|
||||
const fieldMetadataIdsUsedInViewFields = views
|
||||
.flatMap((view) => view.viewFields)
|
||||
.map((viewField) => viewField.fieldMetadataId);
|
||||
|
||||
const fieldMetadataIdsUsedInViewFilters = views
|
||||
.flatMap((view) => view.viewFilters)
|
||||
.map((viewFilter) => viewFilter.fieldMetadataId);
|
||||
|
||||
const fieldMetadataIdsUsedInViewSorts = views
|
||||
.flatMap((view) => view.viewSorts)
|
||||
.map((viewSort) => viewSort.fieldMetadataId);
|
||||
|
||||
const fieldMetadataIdsUsedInViewGroups = views
|
||||
.flatMap((view) => view.viewGroups)
|
||||
.map((viewGroup) => viewGroup.fieldMetadataId);
|
||||
|
||||
const fieldMetadataIds = [
|
||||
...fieldMetadataIdsUsedInViewFields,
|
||||
...fieldMetadataIdsUsedInViewFilters,
|
||||
...fieldMetadataIdsUsedInViewSorts,
|
||||
...fieldMetadataIdsUsedInViewGroups,
|
||||
];
|
||||
|
||||
const fieldMetadataRepository =
|
||||
queryRunner.manager.getRepository(FieldMetadataEntity);
|
||||
const fieldMetadataItems = await fieldMetadataRepository.find({
|
||||
where: {
|
||||
id: In(fieldMetadataIds),
|
||||
},
|
||||
});
|
||||
|
||||
const existingFieldMetadataIds = fieldMetadataItems.map(
|
||||
(fieldMetadata) => fieldMetadata.id,
|
||||
);
|
||||
|
||||
const corruptedViewFieldIds = views
|
||||
.flatMap((view) => view.viewFields)
|
||||
.filter(
|
||||
(viewField) =>
|
||||
!existingFieldMetadataIds.includes(viewField.fieldMetadataId) ||
|
||||
viewField.position === null,
|
||||
)
|
||||
.map((viewField) => viewField.id);
|
||||
|
||||
const corruptedViewFilterIds = views
|
||||
.flatMap((view) => view.viewFilters)
|
||||
.filter(
|
||||
(viewFilter) =>
|
||||
!existingFieldMetadataIds.includes(viewFilter.fieldMetadataId),
|
||||
)
|
||||
.map((viewFilter) => viewFilter.id);
|
||||
|
||||
const corruptedViewSortIds = views
|
||||
.flatMap((view) => view.viewSorts)
|
||||
.filter(
|
||||
(viewSort) =>
|
||||
!existingFieldMetadataIds.includes(viewSort.fieldMetadataId),
|
||||
)
|
||||
.map((viewSort) => viewSort.id);
|
||||
|
||||
const corruptedViewGroupIds = views
|
||||
.flatMap((view) => view.viewGroups)
|
||||
.filter(
|
||||
(viewGroup) =>
|
||||
!existingFieldMetadataIds.includes(viewGroup.fieldMetadataId),
|
||||
)
|
||||
.map((viewGroup) => viewGroup.id);
|
||||
|
||||
const workspaceViewFieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewField',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await workspaceViewFieldRepository.delete({
|
||||
id: In(corruptedViewFieldIds),
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${corruptedViewFieldIds.length} out of ${views.flatMap((view) => view.viewFields).length} view fields that have no corresponding field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const workspaceViewFilterRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFilterWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewFilter',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await workspaceViewFilterRepository.delete({
|
||||
id: In(corruptedViewFilterIds),
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${corruptedViewFilterIds.length} out of ${views.flatMap((view) => view.viewFilters).length} view filters that have no corresponding field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const workspaceViewSortRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewSortWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewSort',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await workspaceViewSortRepository.delete({
|
||||
id: In(corruptedViewSortIds),
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${corruptedViewSortIds.length} out of ${views.flatMap((view) => view.viewSorts).length} view sorts that have no corresponding field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const workspaceViewGroupRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewGroupWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'viewGroup',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await workspaceViewGroupRepository.delete({
|
||||
id: In(corruptedViewGroupIds),
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${corruptedViewGroupIds.length} out of ${views.flatMap((view) => view.viewGroups).length} view groups that have no corresponding field metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
const objectMetadataIdsUsedInViews = views.map(
|
||||
(view) => view.objectMetadataId,
|
||||
);
|
||||
|
||||
const objectMetadataRepository =
|
||||
queryRunner.manager.getRepository(ObjectMetadataEntity);
|
||||
const objectMetadataItems = await objectMetadataRepository.find({
|
||||
where: {
|
||||
id: In(objectMetadataIdsUsedInViews),
|
||||
},
|
||||
});
|
||||
|
||||
const existingObjectMetadataIds = objectMetadataItems.map(
|
||||
(objectMetadata) => objectMetadata.id,
|
||||
);
|
||||
|
||||
const corruptedViewIds = views
|
||||
.filter(
|
||||
(view) => !existingObjectMetadataIds.includes(view.objectMetadataId),
|
||||
)
|
||||
.map((view) => view.id);
|
||||
|
||||
const workspaceViewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'view',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
await workspaceViewRepository.delete({
|
||||
id: In(corruptedViewIds),
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${corruptedViewIds.length} out of ${views.length} views that have no corresponding object metadata for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
corruptedViewIds,
|
||||
corruptedViewFieldIds,
|
||||
corruptedViewFilterIds,
|
||||
corruptedViewSortIds,
|
||||
corruptedViewGroupIds,
|
||||
};
|
||||
}
|
||||
|
||||
private async deleteExistingCoreViewObjects(
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
dryRun: boolean,
|
||||
): Promise<void> {
|
||||
const viewRepository = queryRunner.manager.getRepository(ViewEntity);
|
||||
const existingViews = await viewRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id'],
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
if (existingViews.length === 0) {
|
||||
this.logger.log(
|
||||
`No existing core view objects found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${dryRun ? 'DRY RUN: ' : ''}Deleting ${existingViews.length} existing core view objects for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would delete all existing core view objects for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await viewRepository.delete({ workspaceId });
|
||||
|
||||
this.logger.log(
|
||||
`Deleted all existing core view objects for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async migrateViewEntity(
|
||||
workspaceView: ViewWorkspaceEntity,
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
let viewName = workspaceView.name;
|
||||
|
||||
if (workspaceView.key === 'INDEX' && !viewName.includes('{')) {
|
||||
viewName = 'All {objectLabelPlural}';
|
||||
}
|
||||
|
||||
const coreView: Partial<ViewEntity> = {
|
||||
id: workspaceView.id,
|
||||
name: viewName,
|
||||
objectMetadataId: workspaceView.objectMetadataId,
|
||||
type: workspaceView.type === 'table' ? ViewType.TABLE : ViewType.KANBAN,
|
||||
key:
|
||||
workspaceView.key === 'INDEX' || workspaceView.key === ViewKey.INDEX
|
||||
? ViewKey.INDEX
|
||||
: null,
|
||||
icon: workspaceView.icon,
|
||||
position: workspaceView.position,
|
||||
isCompact: workspaceView.isCompact,
|
||||
isCustom: workspaceView.key !== 'INDEX',
|
||||
openRecordIn:
|
||||
workspaceView.openRecordIn === 'SIDE_PANEL'
|
||||
? ViewOpenRecordIn.SIDE_PANEL
|
||||
: ViewOpenRecordIn.RECORD_PAGE,
|
||||
kanbanAggregateOperation: workspaceView.kanbanAggregateOperation,
|
||||
kanbanAggregateOperationFieldMetadataId:
|
||||
workspaceView.kanbanAggregateOperationFieldMetadataId,
|
||||
workspaceId,
|
||||
createdAt: new Date(workspaceView.createdAt),
|
||||
updatedAt: new Date(workspaceView.updatedAt),
|
||||
deletedAt: workspaceView.deletedAt
|
||||
? new Date(workspaceView.deletedAt)
|
||||
: null,
|
||||
anyFieldFilterValue: workspaceView.anyFieldFilterValue,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(ViewEntity);
|
||||
|
||||
await repository.insert(coreView);
|
||||
}
|
||||
|
||||
private async migrateViewFields(
|
||||
workspaceViewFields: ViewFieldWorkspaceEntity[],
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
for (const field of workspaceViewFields) {
|
||||
const coreViewField: Partial<ViewFieldEntity> = {
|
||||
id: field.id,
|
||||
fieldMetadataId: field.fieldMetadataId,
|
||||
viewId: field.viewId,
|
||||
position: field.position,
|
||||
isVisible: field.isVisible,
|
||||
size: field.size,
|
||||
workspaceId,
|
||||
createdAt: new Date(field.createdAt),
|
||||
updatedAt: new Date(field.updatedAt),
|
||||
deletedAt: field.deletedAt ? new Date(field.deletedAt) : null,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(ViewFieldEntity);
|
||||
|
||||
await repository.insert(coreViewField);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateViewFilters(
|
||||
workspaceViewFilters: ViewFilterWorkspaceEntity[],
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
for (const filter of workspaceViewFilters) {
|
||||
if (!filter.viewId) {
|
||||
this.logger.warn(
|
||||
`Skipping view filter ${filter.id} with null viewId for workspace ${workspaceId}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const coreViewFilter: Partial<ViewFilterEntity> = {
|
||||
id: filter.id,
|
||||
fieldMetadataId: filter.fieldMetadataId,
|
||||
viewId: filter.viewId,
|
||||
operand: convertViewFilterOperandToCoreOperand(
|
||||
filter.operand as SharedViewFilterOperand,
|
||||
),
|
||||
value: convertViewFilterWorkspaceValueToCoreValue(filter.value),
|
||||
viewFilterGroupId: filter.viewFilterGroupId,
|
||||
workspaceId,
|
||||
createdAt: new Date(filter.createdAt),
|
||||
updatedAt: new Date(filter.updatedAt),
|
||||
deletedAt: filter.deletedAt ? new Date(filter.deletedAt) : null,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(ViewFilterEntity);
|
||||
|
||||
await repository.insert(coreViewFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateViewSorts(
|
||||
workspaceViewSorts: ViewSortWorkspaceEntity[],
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
for (const sort of workspaceViewSorts) {
|
||||
if (!sort.viewId) {
|
||||
this.logger.warn(
|
||||
`Skipping view sort ${sort.id} with null viewId for workspace ${workspaceId}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const direction = sort.direction.toUpperCase() as ViewSortDirection;
|
||||
|
||||
const coreViewSort: Partial<ViewSortEntity> = {
|
||||
id: sort.id,
|
||||
fieldMetadataId: sort.fieldMetadataId,
|
||||
viewId: sort.viewId,
|
||||
direction: direction,
|
||||
workspaceId,
|
||||
createdAt: new Date(sort.createdAt),
|
||||
updatedAt: new Date(sort.updatedAt),
|
||||
deletedAt: sort.deletedAt ? new Date(sort.deletedAt) : null,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(ViewSortEntity);
|
||||
|
||||
await repository.insert(coreViewSort);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateViewGroups(
|
||||
workspaceViewGroups: ViewGroupWorkspaceEntity[],
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
for (const group of workspaceViewGroups) {
|
||||
if (!group.viewId) {
|
||||
this.logger.warn(
|
||||
`Skipping view group ${group.id} with null viewId for workspace ${workspaceId}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const coreViewGroup: Partial<ViewGroupEntity> = {
|
||||
id: group.id,
|
||||
fieldMetadataId: group.fieldMetadataId,
|
||||
viewId: group.viewId,
|
||||
fieldValue: group.fieldValue,
|
||||
isVisible: group.isVisible,
|
||||
position: group.position,
|
||||
workspaceId,
|
||||
createdAt: new Date(group.createdAt),
|
||||
updatedAt: new Date(group.updatedAt),
|
||||
deletedAt: group.deletedAt ? new Date(group.deletedAt) : null,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(ViewGroupEntity);
|
||||
|
||||
await repository.insert(coreViewGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateViewFilterGroups(
|
||||
workspaceViewFilterGroups: ViewFilterGroupWorkspaceEntity[],
|
||||
workspaceId: string,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<void> {
|
||||
for (const filterGroup of workspaceViewFilterGroups.sort(
|
||||
(a, b) =>
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
||||
)) {
|
||||
const coreViewFilterGroup: Partial<ViewFilterGroupEntity> = {
|
||||
id: filterGroup.id,
|
||||
viewId: filterGroup.viewId,
|
||||
logicalOperator:
|
||||
filterGroup.logicalOperator as ViewFilterGroupLogicalOperator,
|
||||
parentViewFilterGroupId: filterGroup.parentViewFilterGroupId,
|
||||
positionInViewFilterGroup: filterGroup.positionInViewFilterGroup,
|
||||
workspaceId,
|
||||
createdAt: new Date(filterGroup.createdAt),
|
||||
updatedAt: new Date(filterGroup.updatedAt),
|
||||
deletedAt: filterGroup.deletedAt
|
||||
? new Date(filterGroup.deletedAt)
|
||||
: null,
|
||||
};
|
||||
|
||||
const repository = queryRunner.manager.getRepository(
|
||||
ViewFilterGroupEntity,
|
||||
);
|
||||
|
||||
await repository.insert(coreViewFilterGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -22,10 +22,10 @@ import {
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
|
||||
@Command({
|
||||
name: 'migrate:1-5:remove-favorite-view-relation',
|
||||
name: 'upgrade:1-5:remove-favorite-view-relation',
|
||||
description: 'Remove favorite view relation.',
|
||||
})
|
||||
export class RemoveFavoriteViewRelation extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
export class RemoveFavoriteViewRelationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace)
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
|
||||
+9
-6
@@ -1,8 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AddPositionsToWorkflowVersionsAndWorkflowRuns } from 'src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command';
|
||||
import { RemoveFavoriteViewRelation } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command';
|
||||
import { AddPositionsToWorkflowVersionsAndWorkflowRunsCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command';
|
||||
import { MigrateViewsToCoreCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-migrate-views-to-core.command';
|
||||
import { RemoveFavoriteViewRelationCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -22,12 +23,14 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
||||
WorkspaceMetadataVersionModule,
|
||||
],
|
||||
providers: [
|
||||
RemoveFavoriteViewRelation,
|
||||
AddPositionsToWorkflowVersionsAndWorkflowRuns,
|
||||
RemoveFavoriteViewRelationCommand,
|
||||
AddPositionsToWorkflowVersionsAndWorkflowRunsCommand,
|
||||
MigrateViewsToCoreCommand,
|
||||
],
|
||||
exports: [
|
||||
RemoveFavoriteViewRelation,
|
||||
AddPositionsToWorkflowVersionsAndWorkflowRuns,
|
||||
RemoveFavoriteViewRelationCommand,
|
||||
AddPositionsToWorkflowVersionsAndWorkflowRunsCommand,
|
||||
MigrateViewsToCoreCommand,
|
||||
],
|
||||
})
|
||||
export class V1_5_UpgradeVersionCommandModule {}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Workspace,
|
||||
FieldMetadataEntity,
|
||||
ObjectMetadataEntity,
|
||||
]),
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceSchemaManagerModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
],
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
export class V1_6_UpgradeVersionCommandModule {}
|
||||
+4
-5
@@ -7,10 +7,8 @@ import { V1_1_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
|
||||
import { V1_2_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-2/1-2-upgrade-version-command.module';
|
||||
import { V1_3_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-3/1-3-upgrade-version-command.module';
|
||||
import { V1_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module';
|
||||
import {
|
||||
DatabaseMigrationService,
|
||||
UpgradeCommand,
|
||||
} from 'src/database/commands/upgrade-version-command/upgrade.command';
|
||||
import { V1_6_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module';
|
||||
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
|
||||
@@ -23,8 +21,9 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
V1_2_UpgradeVersionCommandModule,
|
||||
V1_3_UpgradeVersionCommandModule,
|
||||
V1_5_UpgradeVersionCommandModule,
|
||||
V1_6_UpgradeVersionCommandModule,
|
||||
WorkspaceSyncMetadataModule,
|
||||
],
|
||||
providers: [DatabaseMigrationService, UpgradeCommand],
|
||||
providers: [UpgradeCommand],
|
||||
})
|
||||
export class UpgradeVersionCommandModule {}
|
||||
|
||||
+17
-125
@@ -1,13 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type ActiveOrSuspendedWorkspacesMigrationCommandOptions } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import {
|
||||
@@ -31,96 +25,13 @@ import { RemoveWorkflowRunsWithoutState } from 'src/database/commands/upgrade-ve
|
||||
import { AddNextStepIdsToWorkflowRunsTrigger } from 'src/database/commands/upgrade-version-command/1-3/1-3-add-next-step-ids-to-workflow-runs-trigger.command';
|
||||
import { AssignRolesToExistingApiKeysCommand } from 'src/database/commands/upgrade-version-command/1-3/1-3-assign-roles-to-existing-api-keys.command';
|
||||
import { UpdateTimestampColumnTypeInWorkspaceSchemaCommand } from 'src/database/commands/upgrade-version-command/1-3/1-3-update-timestamp-column-type-in-workspace-schema.command';
|
||||
import { AddPositionsToWorkflowVersionsAndWorkflowRuns } from 'src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command';
|
||||
import { RemoveFavoriteViewRelation } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command';
|
||||
import { AddPositionsToWorkflowVersionsAndWorkflowRunsCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-add-positions-to-workflow-versions-and-workflow-runs.command';
|
||||
import { MigrateViewsToCoreCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-migrate-views-to-core.command';
|
||||
import { RemoveFavoriteViewRelationCommand } from 'src/database/commands/upgrade-version-command/1-5/1-5-remove-favorite-view-relation.command';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
import { compareVersionMajorAndMinor } from 'src/utils/version/compare-version-minor-and-major';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseMigrationService {
|
||||
private logger = new Logger(DatabaseMigrationService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Workspace)
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
// TODO centralize with ActiveOrSuspendedRunner method
|
||||
private async loadActiveOrSuspendedWorkspace() {
|
||||
return await this.workspaceRepository.find({
|
||||
select: ['id', 'version'],
|
||||
where: {
|
||||
activationStatus: In([
|
||||
WorkspaceActivationStatus.ACTIVE,
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
]),
|
||||
},
|
||||
order: {
|
||||
id: 'ASC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async shouldSkipUpgradeIfFreshInstallation(): Promise<boolean> {
|
||||
const activeWorkspaceOrSuspendedWorkspaceCount =
|
||||
await this.loadActiveOrSuspendedWorkspace();
|
||||
|
||||
return activeWorkspaceOrSuspendedWorkspaceCount.length === 0;
|
||||
}
|
||||
|
||||
async runMigrations(): Promise<void> {
|
||||
this.logger.log('Running global database migrations');
|
||||
|
||||
try {
|
||||
this.logger.log('Running core datasource migrations...');
|
||||
const coreResult = await execPromise(
|
||||
'npx -y typeorm migration:run -d dist/src/database/typeorm/core/core.datasource',
|
||||
);
|
||||
|
||||
this.logger.log(coreResult.stdout);
|
||||
|
||||
this.logger.log('Database migrations completed successfully');
|
||||
} catch (error) {
|
||||
this.logger.error('Error running database migrations:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async areAllWorkspacesAboveVersion0_53(): Promise<boolean> {
|
||||
try {
|
||||
const allActiveOrSuspendedWorkspaces =
|
||||
await this.loadActiveOrSuspendedWorkspace();
|
||||
|
||||
if (allActiveOrSuspendedWorkspaces.length === 0) {
|
||||
this.logger.log(
|
||||
'No workspaces found. Running migrations for fresh installation.',
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const workspacesBelowVersion = allActiveOrSuspendedWorkspaces.filter(
|
||||
({ version }) =>
|
||||
version === null ||
|
||||
compareVersionMajorAndMinor(version, '0.53.0') === 'lower',
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Found ${workspacesBelowVersion.length} active or suspended workspaces that are below version 0.53.0 \n${workspacesBelowVersion.map((el) => el.id).join('\n')}`,
|
||||
);
|
||||
|
||||
return workspacesBelowVersion.length === 0;
|
||||
} catch (error) {
|
||||
this.logger.error('Error checking workspaces below version:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'upgrade',
|
||||
@@ -136,8 +47,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
|
||||
private readonly databaseMigrationService: DatabaseMigrationService,
|
||||
|
||||
// 0.54 Commands
|
||||
protected readonly fixStandardSelectFieldsPositionCommand: FixStandardSelectFieldsPositionCommand,
|
||||
protected readonly fixCreatedByDefaultValueCommand: FixCreatedByDefaultValueCommand,
|
||||
@@ -165,8 +74,9 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly updateTimestampColumnTypeInWorkspaceSchemaCommand: UpdateTimestampColumnTypeInWorkspaceSchemaCommand,
|
||||
|
||||
// 1.5 Commands
|
||||
protected readonly removeFavoriteViewRelation: RemoveFavoriteViewRelation,
|
||||
protected readonly addPositionsToWorkflowVersionsAndWorkflowRuns: AddPositionsToWorkflowVersionsAndWorkflowRuns,
|
||||
protected readonly removeFavoriteViewRelationCommand: RemoveFavoriteViewRelationCommand,
|
||||
protected readonly addPositionsToWorkflowVersionsAndWorkflowRunsCommand: AddPositionsToWorkflowVersionsAndWorkflowRunsCommand,
|
||||
protected readonly migrateViewsToCoreCommand: MigrateViewsToCoreCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
@@ -242,12 +152,18 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
|
||||
const commands_150: VersionCommands = {
|
||||
beforeSyncMetadata: [
|
||||
this.removeFavoriteViewRelation,
|
||||
this.addPositionsToWorkflowVersionsAndWorkflowRuns,
|
||||
this.migrateViewsToCoreCommand,
|
||||
this.removeFavoriteViewRelationCommand,
|
||||
this.addPositionsToWorkflowVersionsAndWorkflowRunsCommand,
|
||||
],
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
const commands_160: VersionCommands = {
|
||||
beforeSyncMetadata: [],
|
||||
afterSyncMetadata: [],
|
||||
};
|
||||
|
||||
this.allCommands = {
|
||||
'0.53.0': commands_053,
|
||||
'0.54.0': commands_054,
|
||||
@@ -259,6 +175,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
'1.3.0': commands_130,
|
||||
'1.4.0': commands_140,
|
||||
'1.5.0': commands_150,
|
||||
'1.6.0': commands_160,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -266,31 +183,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
passedParams: string[],
|
||||
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
||||
): Promise<void> {
|
||||
const shouldSkipUpgradeIfFreshInstallation =
|
||||
await this.databaseMigrationService.shouldSkipUpgradeIfFreshInstallation();
|
||||
|
||||
if (shouldSkipUpgradeIfFreshInstallation) {
|
||||
this.logger.log(
|
||||
chalk.blue('Fresh installation detected, skipping migration'),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldPreventFromUpgradingIfWorkspaceIsBelowVersion0_53 =
|
||||
!(await this.databaseMigrationService.areAllWorkspacesAboveVersion0_53());
|
||||
|
||||
if (shouldPreventFromUpgradingIfWorkspaceIsBelowVersion0_53) {
|
||||
this.logger.log(
|
||||
chalk.red(
|
||||
'Not able to run migrate command, aborting the whole migrate-upgrade operation',
|
||||
),
|
||||
);
|
||||
throw new Error('Could not run migration aborting');
|
||||
}
|
||||
|
||||
await this.databaseMigrationService.runMigrations();
|
||||
|
||||
await super.runMigrationCommand(passedParams, options);
|
||||
return await super.runMigrationCommand(passedParams, options);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user