Activity as standard object (#6219)
In this PR I layout the first steps to migrate Activity to a traditional Standard objects Since this is a big transition, I'd rather split it into several deployments / PRs <img width="1512" alt="image" src="https://github.com/user-attachments/assets/012e2bbf-9d1b-4723-aaf6-269ef588b050"> --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: Weiko <corentin@twenty.com> Co-authored-by: Faisal-imtiyaz123 <142205282+Faisal-imtiyaz123@users.noreply.github.com> Co-authored-by: Prateek Jain <prateekj1171998@gmail.com>
This commit is contained in:
@ -8,6 +8,7 @@ import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-dem
|
||||
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
|
||||
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
|
||||
import { UpgradeTo0_23CommandModule } from 'src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module';
|
||||
import { UpgradeVersionModule } from 'src/database/commands/upgrade-version/upgrade-version.module';
|
||||
import { WorkspaceAddTotalCountCommand } from 'src/database/commands/workspace-add-total-count.command';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
@ -44,8 +45,8 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
ObjectMetadataModule,
|
||||
DataSeedDemoWorkspaceModule,
|
||||
WorkspaceCacheVersionModule,
|
||||
// Upgrades
|
||||
UpgradeTo0_23CommandModule,
|
||||
UpgradeVersionModule,
|
||||
],
|
||||
providers: [
|
||||
DataSeedWorkspaceCommand,
|
||||
|
||||
@ -0,0 +1,464 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { QueryRunner } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { notesAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/notes-all.view';
|
||||
import { tasksAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-all.view';
|
||||
import { tasksByStatusView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view';
|
||||
import { WorkspaceStatusService } from 'src/engine/workspace-manager/workspace-status/services/workspace-status.service';
|
||||
import { ActivityWorkspaceEntity } from 'src/modules/activity/standard-objects/activity.workspace-entity';
|
||||
import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
|
||||
import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/note-target.workspace-entity';
|
||||
import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity';
|
||||
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
||||
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
|
||||
interface UpdateActivitiesCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
type CoreLogicFunction = (params: {
|
||||
workspaceId: string;
|
||||
queryRunner?: QueryRunner;
|
||||
schema?: string;
|
||||
}) => Promise<void>;
|
||||
|
||||
@Command({
|
||||
name: 'migrate-0.23:update-activities-type',
|
||||
description: 'Migrate Activity object to Note and Task objects',
|
||||
})
|
||||
export class UpdateActivitiesCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(UpdateActivitiesCommand.name);
|
||||
|
||||
constructor(
|
||||
private readonly workspaceStatusService: WorkspaceStatusService,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id. Command runs on all workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: UpdateActivitiesCommandOptions,
|
||||
): Promise<void> {
|
||||
const updateActivities = async ({
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
schema,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
queryRunner: QueryRunner;
|
||||
schema: string;
|
||||
}): Promise<void> => {
|
||||
/***********************
|
||||
// Transfer Activities to NOTE + Tasks
|
||||
***********************/
|
||||
|
||||
const activityRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ActivityWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'activity',
|
||||
);
|
||||
const noteRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<NoteWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'note',
|
||||
);
|
||||
const noteTargetRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<NoteTargetWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'noteTarget',
|
||||
);
|
||||
const taskRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<TaskWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'task',
|
||||
);
|
||||
const taskTargetRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<TaskTargetWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'taskTarget',
|
||||
);
|
||||
const timelineActivityRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<TimelineActivityWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'timelineActivity',
|
||||
);
|
||||
const attachmentRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<AttachmentWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'attachment',
|
||||
);
|
||||
|
||||
const objectMetadata =
|
||||
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
|
||||
|
||||
const noteObjectMetadataId = objectMetadata.find(
|
||||
(object) => object.nameSingular === 'note',
|
||||
)?.id;
|
||||
|
||||
const taskObjectMetadataId = objectMetadata.find(
|
||||
(object) => object.nameSingular === 'task',
|
||||
)?.id;
|
||||
|
||||
const activityObjectMetadataId = objectMetadata.find(
|
||||
(object) => object.nameSingular === 'activity',
|
||||
)?.id;
|
||||
|
||||
const activitiesToTransfer = await activityRepository.find({
|
||||
order: { createdAt: 'ASC' },
|
||||
relations: ['activityTargets'],
|
||||
});
|
||||
|
||||
for (let i = 0; i < activitiesToTransfer.length; i++) {
|
||||
const activity = activitiesToTransfer[i];
|
||||
|
||||
if (activity.type === 'Note') {
|
||||
const note = noteRepository.create({
|
||||
id: activity.id,
|
||||
title: activity.title,
|
||||
body: activity.body,
|
||||
createdAt: activity.createdAt,
|
||||
updatedAt: activity.updatedAt,
|
||||
position: i,
|
||||
});
|
||||
|
||||
await noteRepository.save(note);
|
||||
|
||||
if (activity.activityTargets && activity.activityTargets.length > 0) {
|
||||
const noteTargets = activity.activityTargets.map(
|
||||
(activityTarget) => {
|
||||
const { activityId, ...activityTargetData } = activityTarget;
|
||||
|
||||
return noteTargetRepository.create({
|
||||
noteId: activityId,
|
||||
...activityTargetData,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await noteTargetRepository.save(noteTargets);
|
||||
}
|
||||
|
||||
await timelineActivityRepository.update(
|
||||
{
|
||||
linkedObjectMetadataId: activityObjectMetadataId,
|
||||
linkedRecordId: activity.id,
|
||||
},
|
||||
{
|
||||
linkedObjectMetadataId: noteObjectMetadataId,
|
||||
},
|
||||
);
|
||||
|
||||
await attachmentRepository.update(
|
||||
{
|
||||
activityId: activity.id,
|
||||
},
|
||||
{
|
||||
activityId: null,
|
||||
noteId: activity.id,
|
||||
},
|
||||
);
|
||||
} else if (activity.type === 'Task') {
|
||||
const task = taskRepository.create({
|
||||
id: activity.id,
|
||||
title: activity.title,
|
||||
body: activity.body,
|
||||
status: activity.completedAt ? 'DONE' : 'TODO',
|
||||
dueAt: activity.dueAt,
|
||||
assigneeId: activity.assigneeId,
|
||||
position: i,
|
||||
createdAt: activity.createdAt,
|
||||
updatedAt: activity.updatedAt,
|
||||
});
|
||||
|
||||
await taskRepository.save(task);
|
||||
|
||||
if (activity.activityTargets && activity.activityTargets.length > 0) {
|
||||
const taskTargets = activity.activityTargets.map(
|
||||
(activityTarget) => {
|
||||
const { activityId, ...activityTargetData } = activityTarget;
|
||||
|
||||
return taskTargetRepository.create({
|
||||
taskId: activityId,
|
||||
...activityTargetData,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await taskTargetRepository.save(taskTargets);
|
||||
}
|
||||
|
||||
await timelineActivityRepository.update(
|
||||
{
|
||||
linkedObjectMetadataId: activityObjectMetadataId,
|
||||
linkedRecordId: activity.id,
|
||||
},
|
||||
{
|
||||
linkedObjectMetadataId: taskObjectMetadataId,
|
||||
},
|
||||
);
|
||||
await attachmentRepository.update(
|
||||
{
|
||||
activityId: activity.id,
|
||||
},
|
||||
{
|
||||
activityId: null,
|
||||
taskId: activity.id,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Unknown activity type: ${activity.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Hack to make sure the command is indempotent and return if one of the view exists
|
||||
const viewExists = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select()
|
||||
.from(`${schema}.view`, 'view')
|
||||
.where('name = :name', { name: 'All Notes' })
|
||||
.getRawOne();
|
||||
|
||||
if (!viewExists) {
|
||||
await this.createViews(
|
||||
objectMetadata,
|
||||
queryRunner,
|
||||
schema,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return this.sharedBoilerplate(_passedParam, options, updateActivities);
|
||||
}
|
||||
|
||||
private async createViews(
|
||||
objectMetadata: ObjectMetadataEntity[],
|
||||
queryRunner: QueryRunner,
|
||||
schema: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const objectMetadataMap = objectMetadata.reduce((acc, object) => {
|
||||
acc[object.standardId ?? ''] = {
|
||||
id: object.id,
|
||||
fields: object.fields.reduce((acc, field) => {
|
||||
acc[field.standardId ?? ''] = field.id;
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {}) as Record<string, ObjectMetadataEntity>;
|
||||
|
||||
const viewDefinitions = [
|
||||
await notesAllView(objectMetadataMap),
|
||||
await tasksAllView(objectMetadataMap),
|
||||
await tasksByStatusView(objectMetadataMap),
|
||||
];
|
||||
|
||||
const viewDefinitionsWithId = viewDefinitions.map((viewDefinition) => ({
|
||||
...viewDefinition,
|
||||
id: v4(),
|
||||
}));
|
||||
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schema}.view`, [
|
||||
'id',
|
||||
'name',
|
||||
'objectMetadataId',
|
||||
'type',
|
||||
'key',
|
||||
'position',
|
||||
'icon',
|
||||
'kanbanFieldMetadataId',
|
||||
])
|
||||
.values(
|
||||
viewDefinitionsWithId.map(
|
||||
({
|
||||
id,
|
||||
name,
|
||||
objectMetadataId,
|
||||
type,
|
||||
key,
|
||||
position,
|
||||
icon,
|
||||
kanbanFieldMetadataId,
|
||||
}) => ({
|
||||
id,
|
||||
name,
|
||||
objectMetadataId,
|
||||
type,
|
||||
key,
|
||||
position,
|
||||
icon,
|
||||
kanbanFieldMetadataId,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.returning('*')
|
||||
.execute();
|
||||
|
||||
for (const viewDefinition of viewDefinitionsWithId) {
|
||||
if (viewDefinition.fields && viewDefinition.fields.length > 0) {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schema}.viewField`, [
|
||||
'fieldMetadataId',
|
||||
'position',
|
||||
'isVisible',
|
||||
'size',
|
||||
'viewId',
|
||||
])
|
||||
.values(
|
||||
viewDefinition.fields.map((field) => ({
|
||||
fieldMetadataId: field.fieldMetadataId,
|
||||
position: field.position,
|
||||
isVisible: field.isVisible,
|
||||
size: field.size,
|
||||
viewId: viewDefinition.id,
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
if (viewDefinition.filters && viewDefinition.filters.length > 0) {
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schema}.viewFilter`, [
|
||||
'fieldMetadataId',
|
||||
'displayValue',
|
||||
'operand',
|
||||
'value',
|
||||
'viewId',
|
||||
])
|
||||
.values(
|
||||
viewDefinition.filters.map((filter: any) => ({
|
||||
fieldMetadataId: filter.fieldMetadataId,
|
||||
displayValue: filter.displayValue,
|
||||
operand: filter.operand,
|
||||
value: filter.value,
|
||||
viewId: viewDefinition.id,
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
await this.workspaceCacheVersionService.incrementVersion(workspaceId);
|
||||
}
|
||||
}
|
||||
|
||||
// This is an attempt to do something more generic that could be reused in every command
|
||||
// Next step if it works well for a few command is to isolated it into a file so
|
||||
// it can be reused and not copy-pasted.
|
||||
async sharedBoilerplate(
|
||||
_passedParam: string[],
|
||||
options: UpdateActivitiesCommandOptions,
|
||||
coreLogic: CoreLogicFunction,
|
||||
) {
|
||||
const workspaceIds = options.workspaceId
|
||||
? [options.workspaceId]
|
||||
: await this.workspaceStatusService.getActiveWorkspaceIds();
|
||||
|
||||
if (!workspaceIds.length) {
|
||||
this.logger.log(chalk.yellow('No workspace found'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on ${workspaceIds.length} workspaces`),
|
||||
);
|
||||
|
||||
const requiresQueryRunner =
|
||||
coreLogic.toString().includes('queryRunner') ||
|
||||
coreLogic.toString().includes('schema');
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
try {
|
||||
if (requiresQueryRunner) {
|
||||
await this.executeWithQueryRunner(workspaceId, coreLogic);
|
||||
} else {
|
||||
await coreLogic({ workspaceId });
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Running command on workspace ${workspaceId} done`),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Migration failed for workspace ${workspaceId}: ${error.message}, ${error.stack}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
|
||||
private async executeWithQueryRunner(
|
||||
workspaceId: string,
|
||||
coreLogic: CoreLogicFunction,
|
||||
) {
|
||||
const dataSourceMetadatas =
|
||||
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
for (const dataSourceMetadata of dataSourceMetadatas) {
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
if (workspaceDataSource) {
|
||||
const queryRunner = workspaceDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await coreLogic({
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
schema: dataSourceMetadata.schema,
|
||||
});
|
||||
await queryRunner.commitTransaction();
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.log(
|
||||
chalk.red(`Running command on workspace ${workspaceId} failed`),
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/u
|
||||
import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command';
|
||||
import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command';
|
||||
import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command';
|
||||
import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command';
|
||||
|
||||
interface Options {
|
||||
workspaceId?: string;
|
||||
@ -19,6 +20,7 @@ export class UpgradeTo0_23Command extends CommandRunner {
|
||||
private readonly migrateDomainNameFromTextToLinks: MigrateDomainNameFromTextToLinksCommand,
|
||||
private readonly migrateMessageChannelSyncStatusEnumCommand: MigrateMessageChannelSyncStatusEnumCommand,
|
||||
private readonly setWorkspaceActivationStatusCommand: SetWorkspaceActivationStatusCommand,
|
||||
private readonly updateActivitiesCommand: UpdateActivitiesCommand,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@ -41,5 +43,6 @@ export class UpgradeTo0_23Command extends CommandRunner {
|
||||
options,
|
||||
);
|
||||
await this.setWorkspaceActivationStatusCommand.run(_passedParam, options);
|
||||
await this.updateActivitiesCommand.run(_passedParam, options);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/u
|
||||
import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command';
|
||||
import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command';
|
||||
import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command';
|
||||
import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command';
|
||||
import { UpgradeTo0_23Command } from 'src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
@ -13,6 +14,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
|
||||
import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module';
|
||||
import { ViewModule } from 'src/modules/view/view.module';
|
||||
@ -29,12 +31,14 @@ import { ViewModule } from 'src/modules/view/view.module';
|
||||
TypeORMModule,
|
||||
ViewModule,
|
||||
BillingModule,
|
||||
ObjectMetadataModule,
|
||||
],
|
||||
providers: [
|
||||
MigrateLinkFieldsToLinksCommand,
|
||||
MigrateDomainNameFromTextToLinksCommand,
|
||||
MigrateMessageChannelSyncStatusEnumCommand,
|
||||
SetWorkspaceActivationStatusCommand,
|
||||
UpdateActivitiesCommand,
|
||||
UpgradeTo0_23Command,
|
||||
],
|
||||
})
|
||||
|
||||
@ -0,0 +1,128 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { isUndefined } from '@nestjs/common/utils/shared.utils';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command';
|
||||
import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command';
|
||||
import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command';
|
||||
import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command';
|
||||
import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command';
|
||||
|
||||
interface UpgradeCommandOptions {
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
type VersionUpgradeMap = {
|
||||
[version: string]: CommandRunner[];
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-version',
|
||||
description: 'Upgrade to a specific version',
|
||||
})
|
||||
export class UpgradeVersionCommand extends CommandRunner {
|
||||
private readonly logger = new Logger(UpgradeVersionCommand.name);
|
||||
|
||||
constructor(
|
||||
private readonly migrateLinkFieldsToLinksCommand: MigrateLinkFieldsToLinksCommand,
|
||||
private readonly migrateDomainNameFromTextToLinksCommand: MigrateDomainNameFromTextToLinksCommand,
|
||||
private readonly migrateMessageChannelSyncStatusEnumCommand: MigrateMessageChannelSyncStatusEnumCommand,
|
||||
private readonly setWorkspaceActivationStatusCommand: SetWorkspaceActivationStatusCommand,
|
||||
private readonly updateActivitiesCommand: UpdateActivitiesCommand,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-v, --version <version>',
|
||||
description: 'Version to upgrade to',
|
||||
required: true,
|
||||
})
|
||||
parseVersion(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id. Command runs on all workspaces if not provided',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
async run(
|
||||
passedParams: string[],
|
||||
options: UpgradeCommandOptions & { version: string },
|
||||
): Promise<void> {
|
||||
const { version, ...upgradeOptions } = options;
|
||||
|
||||
const versionUpgradeMap = {
|
||||
'0.23': [
|
||||
this.migrateLinkFieldsToLinksCommand,
|
||||
this.migrateDomainNameFromTextToLinksCommand,
|
||||
this.migrateMessageChannelSyncStatusEnumCommand,
|
||||
this.setWorkspaceActivationStatusCommand,
|
||||
this.updateActivitiesCommand,
|
||||
],
|
||||
};
|
||||
|
||||
await this.validateVersions(version, versionUpgradeMap);
|
||||
|
||||
if (!versionUpgradeMap[version]) {
|
||||
throw new Error(
|
||||
`No migration commands found for version ${version}. This could mean there were no database changes required for this version.`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const command of versionUpgradeMap[version]) {
|
||||
await command.run(passedParams, upgradeOptions);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Successfully upgraded to version ${version}`));
|
||||
}
|
||||
|
||||
private async getCurrentCodeVersion(): Promise<string> {
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
private async validateVersions(
|
||||
targetVersion: string,
|
||||
versionUpgradeMap: VersionUpgradeMap,
|
||||
): Promise<void> {
|
||||
const currentVersion = await this.getCurrentCodeVersion();
|
||||
|
||||
const cleanCurrentVersion = semver.coerce(currentVersion);
|
||||
const cleanTargetVersion = semver.coerce(targetVersion);
|
||||
|
||||
if (!cleanCurrentVersion || !cleanTargetVersion) {
|
||||
throw new Error(
|
||||
`Invalid version format. Current Code: ${currentVersion}, Target: ${targetVersion}`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetMajorMinor = `${cleanTargetVersion.major}.${cleanTargetVersion.minor}`;
|
||||
|
||||
if (
|
||||
semver.gt(cleanTargetVersion, cleanCurrentVersion) &&
|
||||
isUndefined(versionUpgradeMap[targetMajorMinor])
|
||||
) {
|
||||
throw new Error(
|
||||
`Cannot upgrade to ${cleanTargetVersion}. Your current code version is ${cleanCurrentVersion}. Please update your codebase or upgrade your Docker image first.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Current Code Version: ${currentVersion}, Target: ${targetVersion}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
|
||||
import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command';
|
||||
import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command';
|
||||
import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command';
|
||||
import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command';
|
||||
import { UpdateActivitiesCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-activities.command';
|
||||
import { UpgradeVersionCommand } from 'src/database/commands/upgrade-version/upgrade-version.command';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||
import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
import { ViewModule } from 'src/modules/view/view.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceManagerModule,
|
||||
DataSourceModule,
|
||||
TypeORMModule,
|
||||
TypeOrmModule.forFeature(
|
||||
[Workspace, BillingSubscription, FeatureFlagEntity],
|
||||
'core',
|
||||
),
|
||||
TypeOrmModule.forFeature(
|
||||
[FieldMetadataEntity, ObjectMetadataEntity],
|
||||
'metadata',
|
||||
),
|
||||
WorkspaceModule,
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceSyncMetadataModule,
|
||||
WorkspaceStatusModule,
|
||||
ObjectMetadataModule,
|
||||
DataSeedDemoWorkspaceModule,
|
||||
WorkspaceCacheVersionModule,
|
||||
FieldMetadataModule,
|
||||
ViewModule,
|
||||
BillingModule,
|
||||
],
|
||||
providers: [
|
||||
UpgradeVersionCommand,
|
||||
MigrateLinkFieldsToLinksCommand,
|
||||
MigrateDomainNameFromTextToLinksCommand,
|
||||
MigrateMessageChannelSyncStatusEnumCommand,
|
||||
SetWorkspaceActivationStatusCommand,
|
||||
UpdateActivitiesCommand,
|
||||
],
|
||||
})
|
||||
export class UpgradeVersionModule {}
|
||||
@ -1,5 +1,7 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { WorkspaceActivationStatus } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
const tableName = 'workspace';
|
||||
|
||||
export const seedWorkspaces = async (
|
||||
@ -16,6 +18,7 @@ export const seedWorkspaces = async (
|
||||
'domainName',
|
||||
'inviteHash',
|
||||
'logo',
|
||||
'activationStatus',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
@ -25,6 +28,7 @@ export const seedWorkspaces = async (
|
||||
domainName: 'demo.dev',
|
||||
inviteHash: 'demo.dev-invite-hash',
|
||||
logo: 'https://twentyhq.github.io/placeholder-images/workspaces/apple-logo.png',
|
||||
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
|
||||
Reference in New Issue
Block a user