Refactor upgrade commands (#10592)
Simplifying a lot the upgrade system.
New way to upgrade:
`yarn command:prod upgrade`
New way to write upgrade commands (all wrapping is done for you)
```
override async runOnWorkspace({
index,
total,
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {}
```
Also cleaning CommandModule imports to make it lighter
This commit is contained in:
@ -3,24 +3,31 @@ import { Option } from 'nest-commander';
|
|||||||
import { WorkspaceActivationStatus } from 'twenty-shared';
|
import { WorkspaceActivationStatus } from 'twenty-shared';
|
||||||
import { In, MoreThanOrEqual, Repository } from 'typeorm';
|
import { In, MoreThanOrEqual, Repository } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import { MigrationCommandRunner } from 'src/database/commands/command-runners/migration.command-runner';
|
||||||
MigrationCommandOptions,
|
|
||||||
MigrationCommandRunner,
|
|
||||||
} from 'src/database/commands/migration-command/migration-command.runner';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
|
||||||
export type MaintainedWorkspacesMigrationCommandOptions =
|
export type ActiveOrSuspendedWorkspacesMigrationCommandOptions = {
|
||||||
MigrationCommandOptions & {
|
workspaceIds: string[];
|
||||||
workspaceId?: string;
|
startFromWorkspaceId?: string;
|
||||||
startFromWorkspaceId?: string;
|
workspaceCountLimit?: number;
|
||||||
workspaceCountLimit?: number;
|
dryRun?: boolean;
|
||||||
};
|
verbose?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export abstract class MaintainedWorkspacesMigrationCommandRunner<
|
export type RunOnWorkspaceArgs = {
|
||||||
|
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||||
|
workspaceId: string;
|
||||||
|
dataSource: WorkspaceDataSource;
|
||||||
|
index: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
|
||||||
Options extends
|
Options extends
|
||||||
MaintainedWorkspacesMigrationCommandOptions = MaintainedWorkspacesMigrationCommandOptions,
|
ActiveOrSuspendedWorkspacesMigrationCommandOptions = ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
||||||
> extends MigrationCommandRunner<Options> {
|
> extends MigrationCommandRunner {
|
||||||
private workspaceIds: string[] = [];
|
private workspaceIds: string[] = [];
|
||||||
private startFromWorkspaceId: string | undefined;
|
private startFromWorkspaceId: string | undefined;
|
||||||
private workspaceCountLimit: number | undefined;
|
private workspaceCountLimit: number | undefined;
|
||||||
@ -97,20 +104,8 @@ export abstract class MaintainedWorkspacesMigrationCommandRunner<
|
|||||||
return activeWorkspaces.map((workspace) => workspace.id);
|
return activeWorkspaces.map((workspace) => workspace.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected logWorkspaceCount(activeWorkspaceIds: string[]): void {
|
|
||||||
if (!activeWorkspaceIds.length) {
|
|
||||||
this.logger.log(chalk.yellow('No workspace found'));
|
|
||||||
} else {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.green(
|
|
||||||
`Running command on ${activeWorkspaceIds.length} workspaces`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override async runMigrationCommand(
|
override async runMigrationCommand(
|
||||||
passedParams: string[],
|
_passedParams: string[],
|
||||||
options: Options,
|
options: Options,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const activeWorkspaceIds =
|
const activeWorkspaceIds =
|
||||||
@ -118,22 +113,44 @@ export abstract class MaintainedWorkspacesMigrationCommandRunner<
|
|||||||
? this.workspaceIds
|
? this.workspaceIds
|
||||||
: await this.fetchActiveWorkspaceIds();
|
: await this.fetchActiveWorkspaceIds();
|
||||||
|
|
||||||
this.logWorkspaceCount(activeWorkspaceIds);
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
if (options.dryRun) {
|
||||||
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
|
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.runMigrationCommandOnMaintainedWorkspaces(
|
try {
|
||||||
passedParams,
|
for (const [index, workspaceId] of activeWorkspaceIds.entries()) {
|
||||||
options,
|
this.logger.log(
|
||||||
activeWorkspaceIds,
|
`Running command on workspace ${workspaceId} ${index + 1}/${activeWorkspaceIds.length}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dataSource =
|
||||||
|
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||||
|
workspaceId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.runOnWorkspace({
|
||||||
|
options,
|
||||||
|
workspaceId,
|
||||||
|
dataSource,
|
||||||
|
index: index,
|
||||||
|
total: activeWorkspaceIds.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
chalk.red(`Error in workspace ${workspaceId}: ${error.message}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract runMigrationCommandOnMaintainedWorkspaces(
|
protected abstract runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void>;
|
||||||
passedParams: string[],
|
|
||||||
options: Options,
|
|
||||||
activeWorkspaceIds: string[],
|
|
||||||
): Promise<void>;
|
|
||||||
}
|
}
|
||||||
@ -3,23 +3,16 @@ import { Logger } from '@nestjs/common';
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { CommandRunner, Option } from 'nest-commander';
|
import { CommandRunner, Option } from 'nest-commander';
|
||||||
|
|
||||||
import { MigrationCommandInterface } from 'src/database/commands/migration-command/interfaces/migration-command.interface';
|
|
||||||
|
|
||||||
import { CommandLogger } from 'src/database/commands/logger';
|
import { CommandLogger } from 'src/database/commands/logger';
|
||||||
|
|
||||||
export type MigrationCommandOptions = {
|
export type MigrationCommandOptions = {
|
||||||
workspaceId?: string;
|
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export abstract class MigrationCommandRunner<
|
export abstract class MigrationCommandRunner extends CommandRunner {
|
||||||
Options extends MigrationCommandOptions = MigrationCommandOptions,
|
|
||||||
>
|
|
||||||
extends CommandRunner
|
|
||||||
implements MigrationCommandInterface<Options>
|
|
||||||
{
|
|
||||||
protected logger: CommandLogger | Logger;
|
protected logger: CommandLogger | Logger;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.logger = new CommandLogger({
|
this.logger = new CommandLogger({
|
||||||
@ -46,7 +39,10 @@ export abstract class MigrationCommandRunner<
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
override async run(passedParams: string[], options: Options): Promise<void> {
|
override async run(
|
||||||
|
passedParams: string[],
|
||||||
|
options: MigrationCommandOptions,
|
||||||
|
): Promise<void> {
|
||||||
if (options.verbose) {
|
if (options.verbose) {
|
||||||
this.logger = new CommandLogger({
|
this.logger = new CommandLogger({
|
||||||
verbose: true,
|
verbose: true,
|
||||||
@ -64,8 +60,8 @@ export abstract class MigrationCommandRunner<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract runMigrationCommand(
|
protected abstract runMigrationCommand(
|
||||||
passedParams: string[],
|
passedParams: string[],
|
||||||
options: Options,
|
options: MigrationCommandOptions,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
}
|
}
|
||||||
@ -1 +0,0 @@
|
|||||||
export const dataSeedDemoWorkspaceCronPattern = '0 22 * * *'; // Every day at 10pm
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { Command, CommandRunner } from 'nest-commander';
|
|
||||||
|
|
||||||
import { dataSeedDemoWorkspaceCronPattern } from 'src/database/commands/data-seed-demo-workspace/crons/data-seed-demo-workspace-cron-pattern';
|
|
||||||
import { DataSeedDemoWorkspaceJob } from 'src/database/commands/data-seed-demo-workspace/jobs/data-seed-demo-workspace.job';
|
|
||||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
|
||||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'workspace-seed-demo:cron:start',
|
|
||||||
description: 'Start cron to seed workspace with demo data.',
|
|
||||||
})
|
|
||||||
export class StartDataSeedDemoWorkspaceCronCommand extends CommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectMessageQueue(MessageQueue.cronQueue)
|
|
||||||
private readonly messageQueueService: MessageQueueService,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(): Promise<void> {
|
|
||||||
await this.messageQueueService.addCron<undefined>({
|
|
||||||
jobName: DataSeedDemoWorkspaceJob.name,
|
|
||||||
data: undefined,
|
|
||||||
options: {
|
|
||||||
repeat: {
|
|
||||||
pattern: dataSeedDemoWorkspaceCronPattern,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { Command, CommandRunner } from 'nest-commander';
|
|
||||||
|
|
||||||
import { DataSeedDemoWorkspaceJob } from 'src/database/commands/data-seed-demo-workspace/jobs/data-seed-demo-workspace.job';
|
|
||||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
|
||||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'workspace-seed-demo:cron:stop',
|
|
||||||
description: 'Stop cron to seed workspace with demo data.',
|
|
||||||
})
|
|
||||||
export class StopDataSeedDemoWorkspaceCronCommand extends CommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectMessageQueue(MessageQueue.cronQueue)
|
|
||||||
private readonly messageQueueService: MessageQueueService,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(): Promise<void> {
|
|
||||||
await this.messageQueueService.removeCron({
|
|
||||||
jobName: DataSeedDemoWorkspaceJob.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import { Command, CommandRunner } from 'nest-commander';
|
|
||||||
|
|
||||||
import { DataSeedDemoWorkspaceService } from 'src/database/commands/data-seed-demo-workspace/services/data-seed-demo-workspace.service';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'workspace:seed:demo',
|
|
||||||
description: 'Seed workspace with demo data. Use in development only.',
|
|
||||||
})
|
|
||||||
export class DataSeedDemoWorkspaceCommand extends CommandRunner {
|
|
||||||
constructor(
|
|
||||||
private readonly dataSeedDemoWorkspaceService: DataSeedDemoWorkspaceService,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(): Promise<void> {
|
|
||||||
await this.dataSeedDemoWorkspaceService.seedDemo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { DataSeedDemoWorkspaceService } from 'src/database/commands/data-seed-demo-workspace/services/data-seed-demo-workspace.service';
|
|
||||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
|
||||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
|
||||||
|
|
||||||
@Processor(MessageQueue.cronQueue)
|
|
||||||
export class DataSeedDemoWorkspaceJob {
|
|
||||||
constructor(
|
|
||||||
private readonly dataSeedDemoWorkspaceService: DataSeedDemoWorkspaceService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Process(DataSeedDemoWorkspaceJob.name)
|
|
||||||
async handle(): Promise<void> {
|
|
||||||
await this.dataSeedDemoWorkspaceService.seedDemo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,66 +1,27 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { StartDataSeedDemoWorkspaceCronCommand } from 'src/database/commands/data-seed-demo-workspace/crons/start-data-seed-demo-workspace.cron.command';
|
|
||||||
import { StopDataSeedDemoWorkspaceCronCommand } from 'src/database/commands/data-seed-demo-workspace/crons/stop-data-seed-demo-workspace.cron.command';
|
|
||||||
import { DataSeedDemoWorkspaceCommand } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace-command';
|
|
||||||
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
|
|
||||||
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
|
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
|
||||||
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
|
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
|
||||||
import { UpgradeTo0_42CommandModule } from 'src/database/commands/upgrade-version/0-42/0-42-upgrade-version.module';
|
import { UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/upgrade-version-command.module';
|
||||||
import { UpgradeTo0_43CommandModule } from 'src/database/commands/upgrade-version/0-43/0-43-upgrade-version.module';
|
|
||||||
import { UpgradeTo0_44CommandModule } from 'src/database/commands/upgrade-version/0-44/0-44-upgrade-version.module';
|
|
||||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
|
||||||
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 { 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 { 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 { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
|
||||||
import { SeederModule } from 'src/engine/seeder/seeder.module';
|
import { SeederModule } from 'src/engine/seeder/seeder.module';
|
||||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
|
||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
|
||||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
UpgradeVersionCommandModule,
|
||||||
|
|
||||||
|
// Only needed for the data seed command
|
||||||
|
TypeORMModule,
|
||||||
|
FieldMetadataModule,
|
||||||
|
ObjectMetadataModule,
|
||||||
SeederModule,
|
SeederModule,
|
||||||
WorkspaceManagerModule,
|
WorkspaceManagerModule,
|
||||||
DataSourceModule,
|
DataSourceModule,
|
||||||
TypeORMModule,
|
|
||||||
TypeOrmModule.forFeature(
|
|
||||||
[Workspace, BillingSubscription, FeatureFlag],
|
|
||||||
'core',
|
|
||||||
),
|
|
||||||
TypeOrmModule.forFeature(
|
|
||||||
[FieldMetadataEntity, ObjectMetadataEntity],
|
|
||||||
'metadata',
|
|
||||||
),
|
|
||||||
WorkspaceModule,
|
|
||||||
WorkspaceDataSourceModule,
|
|
||||||
WorkspaceSyncMetadataModule,
|
|
||||||
ObjectMetadataModule,
|
|
||||||
FieldMetadataModule,
|
|
||||||
DataSeedDemoWorkspaceModule,
|
|
||||||
WorkspaceCacheStorageModule,
|
|
||||||
WorkspaceMetadataVersionModule,
|
|
||||||
UpgradeTo0_42CommandModule,
|
|
||||||
UpgradeTo0_43CommandModule,
|
|
||||||
UpgradeTo0_44CommandModule,
|
|
||||||
FeatureFlagModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
DataSeedWorkspaceCommand,
|
|
||||||
DataSeedDemoWorkspaceCommand,
|
|
||||||
ConfirmationQuestion,
|
|
||||||
StartDataSeedDemoWorkspaceCronCommand,
|
|
||||||
StopDataSeedDemoWorkspaceCronCommand,
|
|
||||||
],
|
],
|
||||||
|
providers: [DataSeedWorkspaceCommand, ConfirmationQuestion],
|
||||||
})
|
})
|
||||||
export class DatabaseCommandModule {}
|
export class DatabaseCommandModule {}
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
import chalk from 'chalk';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import {
|
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
|
||||||
|
|
||||||
export abstract class BatchMaintainedWorkspacesMigrationCommandRunner<
|
|
||||||
Options extends
|
|
||||||
MaintainedWorkspacesMigrationCommandOptions = MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
> extends MaintainedWorkspacesMigrationCommandRunner<Options> {
|
|
||||||
constructor(
|
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
|
||||||
_passedParams: string[],
|
|
||||||
_options: Options,
|
|
||||||
activeWorkspaceIds: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.green(`Running command on ${activeWorkspaceIds.length} workspaces`),
|
|
||||||
);
|
|
||||||
for (const [index, workspaceId] of activeWorkspaceIds.entries()) {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.green(
|
|
||||||
`Processing workspace ${workspaceId} (${index + 1}/${
|
|
||||||
activeWorkspaceIds.length
|
|
||||||
})`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const dataSource =
|
|
||||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.runMigrationCommandOnWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
index,
|
|
||||||
activeWorkspaceIds.length,
|
|
||||||
dataSource,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error in workspace ${workspaceId}: ${error}`);
|
|
||||||
}
|
|
||||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract runMigrationCommandOnWorkspace(
|
|
||||||
workspaceId: string,
|
|
||||||
index: number,
|
|
||||||
total: number,
|
|
||||||
dataSource: WorkspaceDataSource,
|
|
||||||
): Promise<void>;
|
|
||||||
}
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
import { Inject } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { MigrationCommandInterface } from 'src/database/commands/migration-command/interfaces/migration-command.interface';
|
|
||||||
|
|
||||||
import { MaintainedWorkspacesMigrationCommandRunner } from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
|
||||||
import { MIGRATION_COMMAND_INJECTION_TOKEN } from 'src/database/commands/migration-command/migration-command.constants';
|
|
||||||
import { MigrationCommandRunner } from 'src/database/commands/migration-command/migration-command.runner';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
|
||||||
import { SyncWorkspaceLoggerService } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.service';
|
|
||||||
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
|
|
||||||
|
|
||||||
export function createUpgradeAllCommand(
|
|
||||||
version: string,
|
|
||||||
): new (...args: unknown[]) => MigrationCommandRunner {
|
|
||||||
@Command({
|
|
||||||
name: `upgrade-${version}`,
|
|
||||||
description: `Upgrade to version ${version}`,
|
|
||||||
})
|
|
||||||
class UpgradeCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(Workspace, 'core')
|
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
|
||||||
@Inject(MIGRATION_COMMAND_INJECTION_TOKEN)
|
|
||||||
private readonly subCommands: MigrationCommandInterface[],
|
|
||||||
private readonly dataSourceService: DataSourceService,
|
|
||||||
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
|
|
||||||
private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Remove and avoid duplicated synchronize logic with SyncWorkspaceMetadataCommand after command refactoring
|
|
||||||
private async synchronizeWorkspaceMetadata({
|
|
||||||
workspaceIds,
|
|
||||||
options,
|
|
||||||
}: {
|
|
||||||
workspaceIds: string[];
|
|
||||||
options: Record<string, unknown>;
|
|
||||||
}) {
|
|
||||||
this.logger.log(`Attempting to sync ${workspaceIds.length} workspaces.`);
|
|
||||||
const errorsDuringSync: string[] = [];
|
|
||||||
|
|
||||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
|
||||||
try {
|
|
||||||
this.logger.log(
|
|
||||||
`Running workspace sync for workspace: ${workspaceId} (${index + 1} out of ${workspaceIds.length})`,
|
|
||||||
);
|
|
||||||
const dataSourceMetadata =
|
|
||||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { storage, workspaceMigrations } =
|
|
||||||
await this.workspaceSyncMetadataService.synchronize(
|
|
||||||
{
|
|
||||||
workspaceId,
|
|
||||||
dataSourceId: dataSourceMetadata.id,
|
|
||||||
},
|
|
||||||
{ applyChanges: !options.dryRun },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
await this.syncWorkspaceLoggerService.saveLogs(
|
|
||||||
workspaceId,
|
|
||||||
storage,
|
|
||||||
workspaceMigrations,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = `Failed to synchronize workspace ${workspaceId}: ${error.message}`;
|
|
||||||
|
|
||||||
this.logger.error(errorMessage);
|
|
||||||
errorsDuringSync.push(errorMessage);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.logger.log(
|
|
||||||
`Finished synchronizing all active workspaces (${
|
|
||||||
workspaceIds.length
|
|
||||||
} workspaces). ${
|
|
||||||
errorsDuringSync.length > 0
|
|
||||||
? 'Errors during sync:\n' + errorsDuringSync.join('.\n')
|
|
||||||
: ''
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
|
||||||
passedParams: string[],
|
|
||||||
options: Record<string, unknown>,
|
|
||||||
workspaceIds: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.log(`Running upgrade command for version ${version}`);
|
|
||||||
|
|
||||||
for (const command of this.subCommands) {
|
|
||||||
await command.runMigrationCommand(passedParams, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.synchronizeWorkspaceMetadata({ options, workspaceIds });
|
|
||||||
|
|
||||||
this.logger.log(`Upgrade ${version} command completed!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return UpgradeCommand;
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
// migration-command.decorator.ts
|
|
||||||
import { Type } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { Command, CommandMetadata } from 'nest-commander';
|
|
||||||
import 'reflect-metadata';
|
|
||||||
|
|
||||||
import { MigrationCommandRunner } from 'src/database/commands/migration-command/migration-command.runner';
|
|
||||||
|
|
||||||
export interface MigrationCommandMetadata extends CommandMetadata {
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MIGRATION_COMMANDS = new Map<
|
|
||||||
string,
|
|
||||||
Array<Type<MigrationCommandRunner>>
|
|
||||||
>();
|
|
||||||
|
|
||||||
export function MigrationCommand(
|
|
||||||
options: MigrationCommandMetadata,
|
|
||||||
): <T extends Type<MigrationCommandRunner>>(target: T) => T | void {
|
|
||||||
return <T extends Type<MigrationCommandRunner>>(target: T): T | void => {
|
|
||||||
const { version, name, ...commandOptions } = options;
|
|
||||||
|
|
||||||
if (!MIGRATION_COMMANDS.has(version)) {
|
|
||||||
MIGRATION_COMMANDS.set(version, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
MIGRATION_COMMANDS.get(version)?.push(target);
|
|
||||||
|
|
||||||
return Command({
|
|
||||||
name: `upgrade-${version}:${name}`,
|
|
||||||
...commandOptions,
|
|
||||||
})(target);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMigrationCommandsForVersion(
|
|
||||||
version: string,
|
|
||||||
): Array<Type<MigrationCommandRunner>> {
|
|
||||||
return MIGRATION_COMMANDS.get(version) || [];
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export interface MigrationCommandInterface<
|
|
||||||
Options extends Record<string, unknown> = Record<string, unknown>,
|
|
||||||
> {
|
|
||||||
runMigrationCommand(passedParams: string[], options: Options): Promise<void>;
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export const MIGRATION_COMMAND_INJECTION_TOKEN = 'MIGRATION_COMMANDS';
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import { DynamicModule, Module, ModuleMetadata } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { MigrationCommandInterface } from 'src/database/commands/migration-command/interfaces/migration-command.interface';
|
|
||||||
|
|
||||||
import { createUpgradeAllCommand } from 'src/database/commands/migration-command/create-upgrade-all-command.factory';
|
|
||||||
import { getMigrationCommandsForVersion } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
|
||||||
import { MIGRATION_COMMAND_INJECTION_TOKEN } from 'src/database/commands/migration-command/migration-command.constants';
|
|
||||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
|
||||||
import { SyncWorkspaceLoggerModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.module';
|
|
||||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
|
||||||
|
|
||||||
@Module({})
|
|
||||||
export class MigrationCommandModule {
|
|
||||||
static register(
|
|
||||||
version: string,
|
|
||||||
moduleMetadata: ModuleMetadata,
|
|
||||||
): DynamicModule {
|
|
||||||
const commandClasses = getMigrationCommandsForVersion(version);
|
|
||||||
const upgradeAllCommand = createUpgradeAllCommand(version);
|
|
||||||
|
|
||||||
return {
|
|
||||||
module: MigrationCommandModule,
|
|
||||||
imports: [
|
|
||||||
SyncWorkspaceLoggerModule,
|
|
||||||
...(moduleMetadata.imports ?? []),
|
|
||||||
WorkspaceSyncMetadataModule,
|
|
||||||
DataSourceModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
...(moduleMetadata.providers ?? []),
|
|
||||||
...commandClasses,
|
|
||||||
{
|
|
||||||
provide: MIGRATION_COMMAND_INJECTION_TOKEN,
|
|
||||||
useFactory: (
|
|
||||||
...instances: MigrationCommandInterface[]
|
|
||||||
): MigrationCommandInterface[] => {
|
|
||||||
return instances;
|
|
||||||
},
|
|
||||||
inject: commandClasses,
|
|
||||||
},
|
|
||||||
upgradeAllCommand,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
...(moduleMetadata.exports ?? []),
|
|
||||||
MIGRATION_COMMAND_INJECTION_TOKEN,
|
|
||||||
...commandClasses,
|
|
||||||
upgradeAllCommand,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +1,14 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import { Command } from 'nest-commander';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { isCommandLogger } from 'src/database/commands/logger';
|
|
||||||
import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
|
||||||
import {
|
import {
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
RunOnWorkspaceArgs,
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
import { FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
@ -24,12 +23,11 @@ import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/vie
|
|||||||
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
|
|
||||||
@MigrationCommand({
|
@Command({
|
||||||
name: 'add-tasks-assigned-to-me-view',
|
name: 'upgrade:0-43:add-tasks-assigned-to-me-view',
|
||||||
description: 'Add tasks assigned to me view',
|
description: 'Add tasks assigned to me view',
|
||||||
version: '0.43',
|
|
||||||
})
|
})
|
||||||
export class AddTasksAssignedToMeViewCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
@ -43,57 +41,25 @@ export class AddTasksAssignedToMeViewCommand extends MaintainedWorkspacesMigrati
|
|||||||
super(workspaceRepository, twentyORMGlobalManager);
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
override async runOnWorkspace({
|
||||||
_passedParam: string[],
|
index,
|
||||||
options: MaintainedWorkspacesMigrationCommandOptions,
|
total,
|
||||||
workspaceIds: string[],
|
workspaceId,
|
||||||
): Promise<void> {
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
this.logger.log('Running command to create many to one relations');
|
this.logger.log(
|
||||||
|
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||||
|
);
|
||||||
|
|
||||||
if (isCommandLogger(this.logger)) {
|
await this.createTasksAssignedToMeView(workspaceId);
|
||||||
this.logger.setVerbose(options.verbose ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
this.logger.log(
|
||||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
chalk.green(`Command completed for workspace ${workspaceId}.`),
|
||||||
await this.processWorkspace(workspaceId, index, workspaceIds.length);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(chalk.green('Command completed!'));
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(chalk.red('Error in workspace'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processWorkspace(
|
|
||||||
workspaceId: string,
|
|
||||||
index: number,
|
|
||||||
total: number,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.logger.log(
|
|
||||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewId = await this.createTasksAssignedToMeView(workspaceId);
|
|
||||||
|
|
||||||
await this.createTasksAssignedToMeViewGroups(workspaceId, viewId);
|
|
||||||
|
|
||||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
chalk.green(`Command completed for workspace ${workspaceId}.`),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createTasksAssignedToMeView(
|
private async createTasksAssignedToMeView(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<string> {
|
): Promise<void> {
|
||||||
const objectMetadata = await this.objectMetadataRepository.find({
|
const objectMetadata = await this.objectMetadataRepository.find({
|
||||||
where: { workspaceId },
|
where: { workspaceId },
|
||||||
relations: ['fields'],
|
relations: ['fields'],
|
||||||
@ -135,9 +101,13 @@ export class AddTasksAssignedToMeViewCommand extends MaintainedWorkspacesMigrati
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (existingView) {
|
if (existingView) {
|
||||||
throw new Error(
|
this.logger.log(
|
||||||
`"Assigned to Me" view already exists for workspace ${workspaceId}`,
|
chalk.yellow(
|
||||||
|
`"Assigned to Me" view already exists for workspace ${workspaceId}`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewDefinition = tasksAssignedToMeView(objectMetadataMap);
|
const viewDefinition = tasksAssignedToMeView(objectMetadataMap);
|
||||||
@ -191,7 +161,7 @@ export class AddTasksAssignedToMeViewCommand extends MaintainedWorkspacesMigrati
|
|||||||
await viewFilterRepository.save(viewFilters);
|
await viewFilterRepository.save(viewFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
return insertedView.id;
|
await this.createTasksAssignedToMeViewGroups(workspaceId, insertedView.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createTasksAssignedToMeViewGroups(
|
private async createTasksAssignedToMeViewGroups(
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { Command } from 'nest-commander';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
|
RunOnWorkspaceArgs,
|
||||||
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
|
||||||
|
@Command({
|
||||||
|
name: 'upgrade:0-43:migrate-is-searchable-for-custom-object-metadata',
|
||||||
|
description: 'Set isSearchable true for custom object metadata',
|
||||||
|
})
|
||||||
|
export class MigrateIsSearchableForCustomObjectMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
|
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
|
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||||
|
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||||
|
) {
|
||||||
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async runOnWorkspace({
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
workspaceId,
|
||||||
|
options,
|
||||||
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
|
this.logger.log(
|
||||||
|
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!options.dryRun) {
|
||||||
|
await this.objectMetadataRepository.update(
|
||||||
|
{
|
||||||
|
workspaceId,
|
||||||
|
isCustom: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isSearchable: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,15 +2,15 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { ServerBlockNoteEditor } from '@blocknote/server-util';
|
import { ServerBlockNoteEditor } from '@blocknote/server-util';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import { Command } from 'nest-commander';
|
||||||
import { FieldMetadataType } from 'twenty-shared';
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { isCommandLogger } from 'src/database/commands/logger';
|
|
||||||
import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
|
||||||
import {
|
import {
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
RunOnWorkspaceArgs,
|
||||||
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
@ -23,6 +23,7 @@ import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/work
|
|||||||
type MigrateRichTextContentArgs = {
|
type MigrateRichTextContentArgs = {
|
||||||
richTextFieldsWithObjectMetadata: RichTextFieldsWithObjectMetadata[];
|
richTextFieldsWithObjectMetadata: RichTextFieldsWithObjectMetadata[];
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RichTextFieldsWithObjectMetadata = {
|
type RichTextFieldsWithObjectMetadata = {
|
||||||
@ -30,25 +31,16 @@ type RichTextFieldsWithObjectMetadata = {
|
|||||||
objectMetadata: ObjectMetadataEntity | null;
|
objectMetadata: ObjectMetadataEntity | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProcessWorkspaceArgs = {
|
|
||||||
workspaceId: string;
|
|
||||||
index: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProcessRichTextFieldsArgs = {
|
type ProcessRichTextFieldsArgs = {
|
||||||
richTextFields: FieldMetadataEntity[];
|
richTextFields: FieldMetadataEntity[];
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@MigrationCommand({
|
@Command({
|
||||||
name: 'migrate-rich-text-content-patch',
|
name: 'upgrade:0-43:migrate-rich-text-content-patch',
|
||||||
description: 'Migrate RICH_TEXT content from v1 to v2',
|
description: 'Migrate RICH_TEXT content from v1 to v2',
|
||||||
version: '0.43',
|
|
||||||
})
|
})
|
||||||
export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigrationCommandRunner<MaintainedWorkspacesMigrationCommandOptions> {
|
export class MigrateRichTextContentPatchCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
private options: MaintainedWorkspacesMigrationCommandOptions;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
@ -64,91 +56,58 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
|||||||
super(workspaceRepository, twentyORMGlobalManager);
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
override async runOnWorkspace({
|
||||||
_passedParam: string[],
|
|
||||||
options: MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
workspaceIds: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
'Running command to migrate RICH_TEXT contents from v1 to v2',
|
|
||||||
);
|
|
||||||
|
|
||||||
this.options = options;
|
|
||||||
if (isCommandLogger(this.logger)) {
|
|
||||||
this.logger.setVerbose(options.verbose ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
|
||||||
try {
|
|
||||||
await this.processWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
index,
|
|
||||||
total: workspaceIds.length,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.red(`Error in workspace ${workspaceId}: ${error}`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(chalk.green('Command completed!'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processWorkspace({
|
|
||||||
index,
|
index,
|
||||||
total,
|
total,
|
||||||
|
options,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: ProcessWorkspaceArgs): Promise<void> {
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
try {
|
this.logger.log(
|
||||||
|
`Running MigrateRichTextContentPatchCommand for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (await this.hasRichTextV2FeatureFlag(workspaceId)) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
chalk.yellow(
|
||||||
|
'Rich text v2 feature flag is enabled, skipping migration',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (await this.hasRichTextV2FeatureFlag(workspaceId)) {
|
return;
|
||||||
throw new Error(
|
}
|
||||||
'Rich text v2 feature flag is enabled, skipping migration',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const richTextFields = await this.fieldMetadataRepository.find({
|
const richTextFields = await this.fieldMetadataRepository.find({
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
type: FieldMetadataType.RICH_TEXT,
|
type: FieldMetadataType.RICH_TEXT,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!richTextFields.length) {
|
if (!richTextFields.length) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
chalk.yellow('No RICH_TEXT fields found in this workspace'),
|
chalk.yellow('No RICH_TEXT fields found in this workspace'),
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Found ${richTextFields.length} RICH_TEXT fields`);
|
this.logger.log(`Found ${richTextFields.length} RICH_TEXT fields`);
|
||||||
|
|
||||||
const richTextFieldsWithObjectMetadata =
|
const richTextFieldsWithObjectMetadata =
|
||||||
await this.getRichTextFieldsWithObjectMetadata({
|
await this.getRichTextFieldsWithObjectMetadata({
|
||||||
richTextFields,
|
richTextFields,
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.migrateToNewRichTextFieldsColumn({
|
|
||||||
richTextFieldsWithObjectMetadata,
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(
|
await this.migrateToNewRichTextFieldsColumn({
|
||||||
chalk.green(`Command completed for workspace ${workspaceId}`),
|
richTextFieldsWithObjectMetadata,
|
||||||
);
|
workspaceId,
|
||||||
} catch (error) {
|
options,
|
||||||
this.logger.log(chalk.red(`Error in workspace ${workspaceId}: ${error}`));
|
});
|
||||||
}
|
|
||||||
|
this.logger.log(
|
||||||
|
chalk.green(`Command completed for workspace ${workspaceId}`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async hasRichTextV2FeatureFlag(
|
private async hasRichTextV2FeatureFlag(
|
||||||
@ -179,7 +138,7 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (objectMetadata === null) {
|
if (objectMetadata === null) {
|
||||||
this.logger.warn(
|
this.logger.log(
|
||||||
`Object metadata not found for rich text field ${richTextField.name} in workspace ${workspaceId}`,
|
`Object metadata not found for rich text field ${richTextField.name} in workspace ${workspaceId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -225,7 +184,7 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(jsonParsedblocknoteFieldValue)) {
|
if (!Array.isArray(jsonParsedblocknoteFieldValue)) {
|
||||||
this.logger.warn(
|
this.logger.log(
|
||||||
`blocknoteFieldValue is defined and is not an array got ${blocknoteFieldValue}`,
|
`blocknoteFieldValue is defined and is not an array got ${blocknoteFieldValue}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -239,7 +198,7 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
|||||||
jsonParsedblocknoteFieldValue,
|
jsonParsedblocknoteFieldValue,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn(
|
this.logger.log(
|
||||||
`Error converting blocknote to markdown for ${blocknoteFieldValue}`,
|
`Error converting blocknote to markdown for ${blocknoteFieldValue}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -250,6 +209,7 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
|||||||
private async migrateToNewRichTextFieldsColumn({
|
private async migrateToNewRichTextFieldsColumn({
|
||||||
richTextFieldsWithObjectMetadata,
|
richTextFieldsWithObjectMetadata,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
options,
|
||||||
}: MigrateRichTextContentArgs) {
|
}: MigrateRichTextContentArgs) {
|
||||||
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
|
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
|
||||||
|
|
||||||
@ -285,11 +245,19 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
|||||||
serverBlockNoteEditor,
|
serverBlockNoteEditor,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.options.dryRun) {
|
if (!options.dryRun) {
|
||||||
await workspaceDataSource.query(
|
try {
|
||||||
`UPDATE "${schemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" SET "${richTextField.name}V2Blocknote" = $1, "${richTextField.name}V2Markdown" = $2 WHERE id = $3`,
|
await workspaceDataSource.query(
|
||||||
[blocknoteFieldValue, markdownFieldValue, row.id],
|
`UPDATE "${schemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" SET "${richTextField.name}V2Blocknote" = $1, "${richTextField.name}V2Markdown" = $2 WHERE id = $3`,
|
||||||
);
|
[blocknoteFieldValue, markdownFieldValue, row.id],
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.log(
|
||||||
|
chalk.red(
|
||||||
|
`Error updating rich text field ${richTextField.name} for record ${row.id} in workspace ${workspaceId}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,13 +1,12 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import chalk from 'chalk';
|
import { Command } from 'nest-commander';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
|
||||||
import {
|
import {
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
RunOnWorkspaceArgs,
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
@ -18,12 +17,11 @@ import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/wo
|
|||||||
import { SEARCH_FIELDS_FOR_NOTES } from 'src/modules/note/standard-objects/note.workspace-entity';
|
import { SEARCH_FIELDS_FOR_NOTES } from 'src/modules/note/standard-objects/note.workspace-entity';
|
||||||
import { SEARCH_FIELDS_FOR_TASKS } from 'src/modules/task/standard-objects/task.workspace-entity';
|
import { SEARCH_FIELDS_FOR_TASKS } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||||
|
|
||||||
@MigrationCommand({
|
@Command({
|
||||||
name: 'migrate-search-vector-on-note-and-task-entities',
|
name: 'upgrade:0-43:migrate-search-vector-on-note-and-task-entities',
|
||||||
description: 'Migrate search vector on note and task entities',
|
description: 'Migrate search vector on note and task entities',
|
||||||
version: '0.43',
|
|
||||||
})
|
})
|
||||||
export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
@ -39,73 +37,60 @@ export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends MaintainedW
|
|||||||
super(workspaceRepository, twentyORMGlobalManager);
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
override async runOnWorkspace({
|
||||||
_passedParam: string[],
|
index,
|
||||||
options: MaintainedWorkspacesMigrationCommandOptions,
|
total,
|
||||||
workspaceIds: string[],
|
workspaceId,
|
||||||
): Promise<void> {
|
options,
|
||||||
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
'Running command to migrate search vector on note and task entities',
|
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
const noteObjectMetadata =
|
||||||
await this.processWorkspace(workspaceId, index, workspaceIds.length);
|
await this.objectMetadataRepository.findOneOrFail({
|
||||||
}
|
select: ['id'],
|
||||||
|
where: {
|
||||||
this.logger.log(chalk.green('Command completed!'));
|
workspaceId,
|
||||||
}
|
nameSingular: 'note',
|
||||||
|
},
|
||||||
async processWorkspace(
|
});
|
||||||
workspaceId: string,
|
|
||||||
index: number,
|
|
||||||
total: number,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.logger.log(
|
|
||||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const noteObjectMetadata =
|
|
||||||
await this.objectMetadataRepository.findOneOrFail({
|
|
||||||
select: ['id'],
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
nameSingular: 'note',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if (!options.dryRun) {
|
||||||
await this.searchService.updateSearchVector(
|
await this.searchService.updateSearchVector(
|
||||||
noteObjectMetadata.id,
|
noteObjectMetadata.id,
|
||||||
SEARCH_FIELDS_FOR_NOTES,
|
SEARCH_FIELDS_FOR_NOTES,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const taskObjectMetadata =
|
const taskObjectMetadata =
|
||||||
await this.objectMetadataRepository.findOneOrFail({
|
await this.objectMetadataRepository.findOneOrFail({
|
||||||
select: ['id'],
|
select: ['id'],
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
nameSingular: 'task',
|
nameSingular: 'task',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!options.dryRun) {
|
||||||
await this.searchService.updateSearchVector(
|
await this.searchService.updateSearchVector(
|
||||||
taskObjectMetadata.id,
|
taskObjectMetadata.id,
|
||||||
SEARCH_FIELDS_FOR_TASKS,
|
SEARCH_FIELDS_FOR_TASKS,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.red(`Error in workspace ${workspaceId} - ${error.message}`),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Migrated search vector on note and task entities for workspace ${workspaceId}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,23 +1,25 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import { Command } from 'nest-commander';
|
||||||
import { In, Repository } from 'typeorm';
|
import { In, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { BatchMaintainedWorkspacesMigrationCommandRunner } from 'src/database/commands/migration-command/batch-maintained-workspaces-migration-command.runner';
|
import {
|
||||||
import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
|
RunOnWorkspaceArgs,
|
||||||
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
import { ViewOpenRecordInType } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewOpenRecordInType } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
|
|
||||||
@MigrationCommand({
|
@Command({
|
||||||
name: 'update-default-view-record-opening-on-workflow-objects',
|
name: 'upgrade:0-43:update-default-view-record-opening-on-workflow-objects',
|
||||||
description:
|
description:
|
||||||
'Update default view record opening on workflow objects to record page',
|
'Update default view record opening on workflow objects to record page',
|
||||||
version: '0.43',
|
|
||||||
})
|
})
|
||||||
export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends BatchMaintainedWorkspacesMigrationCommandRunner {
|
export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
@ -28,51 +30,46 @@ export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends Batc
|
|||||||
super(workspaceRepository, twentyORMGlobalManager);
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMigrationCommandOnWorkspace(
|
override async runOnWorkspace({
|
||||||
workspaceId: string,
|
index,
|
||||||
index: number,
|
total,
|
||||||
total: number,
|
workspaceId,
|
||||||
): Promise<void> {
|
options,
|
||||||
try {
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
|
this.logger.log(
|
||||||
|
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const workflowObjectsMetadata = await this.objectMetadataRepository.find({
|
||||||
|
select: ['id'],
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
standardId: In([
|
||||||
|
STANDARD_OBJECT_IDS.workflow,
|
||||||
|
STANDARD_OBJECT_IDS.workflowVersion,
|
||||||
|
STANDARD_OBJECT_IDS.workflowRun,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (workflowObjectsMetadata.length === 0) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
chalk.yellow(`No workflow objects found for workspace ${workspaceId}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
const workflowObjectsMetadata = await this.objectMetadataRepository.find({
|
return;
|
||||||
select: ['id'],
|
}
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
standardId: In([
|
|
||||||
STANDARD_OBJECT_IDS.workflow,
|
|
||||||
STANDARD_OBJECT_IDS.workflowVersion,
|
|
||||||
STANDARD_OBJECT_IDS.workflowRun,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (workflowObjectsMetadata.length === 0) {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.yellow(
|
|
||||||
`No workflow objects found for workspace ${workspaceId}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!options.dryRun) {
|
||||||
await this.updateDefaultViewsRecordOpening(
|
await this.updateDefaultViewsRecordOpening(
|
||||||
workflowObjectsMetadata.map((metadata) => metadata.id),
|
workflowObjectsMetadata.map((metadata) => metadata.id),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
chalk.green(`Command completed for workspace ${workspaceId}.`),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.red(`Error in workspace ${workspaceId} - ${error.message}`),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
chalk.green(`Command completed for workspace ${workspaceId}.`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateDefaultViewsRecordOpening(
|
private async updateDefaultViewsRecordOpening(
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command';
|
||||||
|
import { MigrateIsSearchableForCustomObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-is-searchable-for-custom-object-metadata.command';
|
||||||
|
import { MigrateRichTextContentPatchCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command';
|
||||||
|
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
|
||||||
|
import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command';
|
||||||
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
|
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 { SearchModule } from 'src/engine/metadata-modules/search/search.module';
|
||||||
|
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||||
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
|
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Workspace, FeatureFlag], 'core'),
|
||||||
|
TypeOrmModule.forFeature(
|
||||||
|
[FieldMetadataEntity, ObjectMetadataEntity],
|
||||||
|
'metadata',
|
||||||
|
),
|
||||||
|
WorkspaceDataSourceModule,
|
||||||
|
SearchModule,
|
||||||
|
WorkspaceMigrationRunnerModule,
|
||||||
|
WorkspaceMetadataVersionModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
MigrateRichTextContentPatchCommand,
|
||||||
|
MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
|
||||||
|
UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
|
||||||
|
MigrateIsSearchableForCustomObjectMetadataCommand,
|
||||||
|
AddTasksAssignedToMeViewCommand,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
MigrateRichTextContentPatchCommand,
|
||||||
|
MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
|
||||||
|
UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
|
||||||
|
MigrateIsSearchableForCustomObjectMetadataCommand,
|
||||||
|
AddTasksAssignedToMeViewCommand,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class V0_43_UpgradeVersionCommandModule {}
|
||||||
@ -1,14 +1,15 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import { Command } from 'nest-commander';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { IsNull, Repository } from 'typeorm';
|
import { IsNull, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
|
||||||
import {
|
import {
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
RunOnWorkspaceArgs,
|
||||||
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
|
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
|
||||||
@ -17,13 +18,11 @@ import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
|||||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
|
||||||
@MigrationCommand({
|
@Command({
|
||||||
name: 'initialize-permissions',
|
name: 'upgrade:0-44:initialize-permissions',
|
||||||
description: 'Initialize permissions',
|
description: 'Initialize permissions',
|
||||||
version: '0.44',
|
|
||||||
})
|
})
|
||||||
export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
export class InitializePermissionsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
private options: MaintainedWorkspacesMigrationCommandOptions;
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
@ -36,26 +35,12 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
|||||||
super(workspaceRepository, twentyORMGlobalManager);
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
override async runOnWorkspace({
|
||||||
_passedParam: string[],
|
index,
|
||||||
options: MaintainedWorkspacesMigrationCommandOptions,
|
total,
|
||||||
workspaceIds: string[],
|
workspaceId,
|
||||||
): Promise<void> {
|
options,
|
||||||
this.logger.log(chalk.green('Running command to initialize permissions'));
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
|
|
||||||
this.options = options;
|
|
||||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
|
||||||
await this.processWorkspace(workspaceId, index, workspaceIds.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(chalk.green('Command completed!'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processWorkspace(
|
|
||||||
workspaceId: string,
|
|
||||||
index: number,
|
|
||||||
total: number,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||||
@ -71,12 +56,16 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
|||||||
)?.id;
|
)?.id;
|
||||||
|
|
||||||
if (!isDefined(adminRoleId)) {
|
if (!isDefined(adminRoleId)) {
|
||||||
adminRoleId = await this.createAdminRole({ workspaceId });
|
adminRoleId = await this.createAdminRole({
|
||||||
|
workspaceId,
|
||||||
|
options,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.assignAdminRole({
|
await this.assignAdminRole({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
adminRoleId,
|
adminRoleId,
|
||||||
|
options,
|
||||||
});
|
});
|
||||||
|
|
||||||
let memberRoleId: string | undefined;
|
let memberRoleId: string | undefined;
|
||||||
@ -88,17 +77,20 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
|||||||
if (!isDefined(memberRoleId)) {
|
if (!isDefined(memberRoleId)) {
|
||||||
memberRoleId = await this.createMemberRole({
|
memberRoleId = await this.createMemberRole({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setMemberRoleAsDefaultRole({
|
await this.setMemberRoleAsDefaultRole({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
memberRoleId,
|
memberRoleId,
|
||||||
|
options,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.assignMemberRoleToUserWorkspacesWithoutRole({
|
await this.assignMemberRoleToUserWorkspacesWithoutRole({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
memberRoleId,
|
memberRoleId,
|
||||||
|
options,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
@ -107,14 +99,18 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createAdminRole({ workspaceId }: { workspaceId: string }) {
|
private async createAdminRole({
|
||||||
|
workspaceId,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||||
|
}) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
chalk.green(
|
chalk.green(`Creating admin role ${options.dryRun ? '(dry run)' : ''}`),
|
||||||
`Creating admin role ${this.options.dryRun ? '(dry run)' : ''}`,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.options.dryRun) {
|
if (options.dryRun) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,14 +121,18 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
|||||||
return adminRole.id;
|
return adminRole.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createMemberRole({ workspaceId }: { workspaceId: string }) {
|
private async createMemberRole({
|
||||||
|
workspaceId,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||||
|
}) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
chalk.green(
|
chalk.green(`Creating member role ${options.dryRun ? '(dry run)' : ''}`),
|
||||||
`Creating member role ${this.options.dryRun ? '(dry run)' : ''}`,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.options.dryRun) {
|
if (options.dryRun) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,9 +146,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
|||||||
private async setMemberRoleAsDefaultRole({
|
private async setMemberRoleAsDefaultRole({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
memberRoleId,
|
memberRoleId,
|
||||||
|
options,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
memberRoleId: string;
|
memberRoleId: string;
|
||||||
|
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||||
}) {
|
}) {
|
||||||
const workspaceDefaultRole = await this.workspaceRepository.findOne({
|
const workspaceDefaultRole = await this.workspaceRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
@ -159,11 +161,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
|||||||
if (!isDefined(workspaceDefaultRole?.defaultRoleId)) {
|
if (!isDefined(workspaceDefaultRole?.defaultRoleId)) {
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
chalk.green(
|
chalk.green(
|
||||||
`Setting member role as default role ${this.options.dryRun ? '(dry run)' : ''}`,
|
`Setting member role as default role ${options.dryRun ? '(dry run)' : ''}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.options.dryRun) {
|
if (options.dryRun) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,9 +178,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
|||||||
private async assignAdminRole({
|
private async assignAdminRole({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
adminRoleId,
|
adminRoleId,
|
||||||
|
options,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
adminRoleId: string;
|
adminRoleId: string;
|
||||||
|
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||||
}) {
|
}) {
|
||||||
const oldestUserWorkspace = await this.userWorkspaceRepository.findOne({
|
const oldestUserWorkspace = await this.userWorkspaceRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
@ -201,11 +205,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
|||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
chalk.green(
|
chalk.green(
|
||||||
`Assigning admin role to user ${oldestUserWorkspace.id} ${this.options.dryRun ? '(dry run)' : ''}`,
|
`Assigning admin role to user ${oldestUserWorkspace.id} ${options.dryRun ? '(dry run)' : ''}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.options.dryRun) {
|
if (options.dryRun) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,9 +223,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
|||||||
private async assignMemberRoleToUserWorkspacesWithoutRole({
|
private async assignMemberRoleToUserWorkspacesWithoutRole({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
memberRoleId,
|
memberRoleId,
|
||||||
|
options,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
memberRoleId: string;
|
memberRoleId: string;
|
||||||
|
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||||
}) {
|
}) {
|
||||||
const userWorkspaces = await this.userWorkspaceRepository.find({
|
const userWorkspaces = await this.userWorkspaceRepository.find({
|
||||||
where: {
|
where: {
|
||||||
@ -257,11 +263,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
|||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
chalk.green(
|
chalk.green(
|
||||||
`Assigning member role to user workspace ${userWorkspace.id} ${this.options.dryRun ? '(dry run)' : ''}`,
|
`Assigning member role to user workspace ${userWorkspace.id} ${options.dryRun ? '(dry run)' : ''}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.options.dryRun) {
|
if (options.dryRun) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { Command } from 'nest-commander';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
import { In, Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
|
RunOnWorkspaceArgs,
|
||||||
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
import {
|
||||||
|
RelationDirection,
|
||||||
|
deduceRelationDirection,
|
||||||
|
} from 'src/engine/utils/deduce-relation-direction.util';
|
||||||
|
import { isFieldMetadataOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||||
|
|
||||||
|
@Command({
|
||||||
|
name: 'upgrade:0-44:migrate-relations-to-field-metadata',
|
||||||
|
description: 'Migrate relations to field metadata',
|
||||||
|
})
|
||||||
|
export class MigrateRelationsToFieldMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
|
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||||
|
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||||
|
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
|
) {
|
||||||
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async runOnWorkspace({
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
workspaceId,
|
||||||
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
|
this.logger.log(
|
||||||
|
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldMetadataCollection = await this.fieldMetadataRepository.find({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
type: In([FieldMetadataType.RELATION, FieldMetadataType.UUID]),
|
||||||
|
},
|
||||||
|
relations: ['fromRelationMetadata', 'toRelationMetadata'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fieldMetadataCollection.length) {
|
||||||
|
this.logger.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`No relation field metadata found for workspace ${workspaceId}.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinColumnFieldMetadataCollection = fieldMetadataCollection.filter(
|
||||||
|
(fieldMetadata) =>
|
||||||
|
isFieldMetadataOfType(fieldMetadata, FieldMetadataType.UUID),
|
||||||
|
// TODO: Fix this, it's working in other places but not here
|
||||||
|
) as FieldMetadataEntity<FieldMetadataType.UUID>[];
|
||||||
|
|
||||||
|
const fieldMetadataToUpdateCollection = fieldMetadataCollection
|
||||||
|
.filter((fieldMetadata) =>
|
||||||
|
isFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION),
|
||||||
|
)
|
||||||
|
.map((fieldMetadata) =>
|
||||||
|
this.updateRelationFieldMetadata(
|
||||||
|
joinColumnFieldMetadataCollection,
|
||||||
|
// TODO: Fix this, it's working in other places but not here
|
||||||
|
fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fieldMetadataToUpdateCollection.length > 0) {
|
||||||
|
await this.fieldMetadataRepository.save(fieldMetadataToUpdateCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
chalk.green(`Command completed for workspace ${workspaceId}.`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateRelationFieldMetadata(
|
||||||
|
joinColumnFieldMetadataCollection: FieldMetadataEntity<FieldMetadataType.UUID>[],
|
||||||
|
fieldMetadata: FieldMetadataEntity<FieldMetadataType.RELATION>,
|
||||||
|
): FieldMetadataEntity<FieldMetadataType.RELATION> {
|
||||||
|
const relationMetadata =
|
||||||
|
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
||||||
|
const joinColumnFieldMetadata = joinColumnFieldMetadataCollection.find(
|
||||||
|
(joinColumnFieldMetadata) =>
|
||||||
|
// We're deducing the field based on the name of the relation field
|
||||||
|
// This is not the best way to do this but we don't have a better way
|
||||||
|
joinColumnFieldMetadata.name === `${fieldMetadata.name}Id`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const relationDirection = deduceRelationDirection(
|
||||||
|
fieldMetadata,
|
||||||
|
relationMetadata,
|
||||||
|
);
|
||||||
|
let relationType = relationMetadata.relationType as unknown as RelationType;
|
||||||
|
|
||||||
|
if (
|
||||||
|
relationDirection === RelationDirection.TO &&
|
||||||
|
relationType === RelationType.ONE_TO_MANY
|
||||||
|
) {
|
||||||
|
relationType = RelationType.MANY_TO_ONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationTargetFieldMetadataId =
|
||||||
|
relationDirection === RelationDirection.FROM
|
||||||
|
? relationMetadata.toFieldMetadataId
|
||||||
|
: relationMetadata.fromFieldMetadataId;
|
||||||
|
|
||||||
|
const relationTargetObjectMetadataId =
|
||||||
|
relationDirection === RelationDirection.FROM
|
||||||
|
? relationMetadata.toObjectMetadataId
|
||||||
|
: relationMetadata.fromObjectMetadataId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...fieldMetadata,
|
||||||
|
settings: {
|
||||||
|
relationType,
|
||||||
|
onDelete: relationMetadata.onDeleteAction,
|
||||||
|
joinColumnName: joinColumnFieldMetadata?.name,
|
||||||
|
},
|
||||||
|
relationTargetFieldMetadataId,
|
||||||
|
relationTargetObjectMetadataId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { InitializePermissionsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-initialize-permissions.command';
|
||||||
|
import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-migrate-relations-to-field-metadata.command';
|
||||||
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
|
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 { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
||||||
|
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||||
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Workspace, UserWorkspace], 'core'),
|
||||||
|
TypeOrmModule.forFeature(
|
||||||
|
[FieldMetadataEntity, ObjectMetadataEntity],
|
||||||
|
'metadata',
|
||||||
|
),
|
||||||
|
WorkspaceDataSourceModule,
|
||||||
|
RoleModule,
|
||||||
|
UserRoleModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
InitializePermissionsCommand,
|
||||||
|
MigrateRelationsToFieldMetadataCommand,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
InitializePermissionsCommand,
|
||||||
|
MigrateRelationsToFieldMetadataCommand,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class V0_44_UpgradeVersionCommandModule {}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { V0_43_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-43/0-43-upgrade-version-command.module';
|
||||||
|
import { V0_44_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-44/0-44-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';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||||
|
V0_43_UpgradeVersionCommandModule,
|
||||||
|
V0_44_UpgradeVersionCommandModule,
|
||||||
|
WorkspaceSyncMetadataModule,
|
||||||
|
],
|
||||||
|
providers: [UpgradeCommand],
|
||||||
|
})
|
||||||
|
export class UpgradeVersionCommandModule {}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { Command } from 'nest-commander';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
|
RunOnWorkspaceArgs,
|
||||||
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
|
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command';
|
||||||
|
import { MigrateIsSearchableForCustomObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-is-searchable-for-custom-object-metadata.command';
|
||||||
|
import { MigrateRichTextContentPatchCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command';
|
||||||
|
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
|
||||||
|
import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command';
|
||||||
|
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';
|
||||||
|
|
||||||
|
@Command({
|
||||||
|
name: 'upgrade',
|
||||||
|
description: 'Upgrade workspaces to the latest version',
|
||||||
|
})
|
||||||
|
export class UpgradeCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Workspace, 'core')
|
||||||
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
|
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
|
protected readonly migrateRichTextContentPatchCommand: MigrateRichTextContentPatchCommand,
|
||||||
|
protected readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand,
|
||||||
|
protected readonly migrateIsSearchableForCustomObjectMetadataCommand: MigrateIsSearchableForCustomObjectMetadataCommand,
|
||||||
|
protected readonly updateDefaultViewRecordOpeningOnWorkflowObjectsCommand: UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
|
||||||
|
protected readonly migrateSearchVectorOnNoteAndTaskEntitiesCommand: MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
|
||||||
|
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||||
|
) {
|
||||||
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void> {
|
||||||
|
this.logger.log(
|
||||||
|
chalk.blue(
|
||||||
|
`${args.options.dryRun ? '(dry run)' : ''} Upgrading workspace ${args.workspaceId} ${args.index + 1}/${args.total}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.migrateRichTextContentPatchCommand.runOnWorkspace(args);
|
||||||
|
|
||||||
|
await this.migrateIsSearchableForCustomObjectMetadataCommand.runOnWorkspace(
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.migrateSearchVectorOnNoteAndTaskEntitiesCommand.runOnWorkspace(
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.migrateIsSearchableForCustomObjectMetadataCommand.runOnWorkspace(
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.syncWorkspaceMetadataCommand.runOnWorkspace(args);
|
||||||
|
|
||||||
|
await this.updateDefaultViewRecordOpeningOnWorkflowObjectsCommand.runOnWorkspace(
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.addTasksAssignedToMeViewCommand.runOnWorkspace(args);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
chalk.blue(`Upgrade for workspace ${args.workspaceId} completed.`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,193 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { Command } from 'nest-commander';
|
|
||||||
import { In, Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { isCommandLogger } from 'src/database/commands/logger';
|
|
||||||
import {
|
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
||||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
|
||||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
|
||||||
|
|
||||||
@Command({
|
|
||||||
name: 'upgrade-0.42:fix-body-v2-view-field-position',
|
|
||||||
description: 'Make bodyV2 field position to match body field position',
|
|
||||||
})
|
|
||||||
export class FixBodyV2ViewFieldPositionCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(Workspace, 'core')
|
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
|
||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
|
||||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
|
||||||
_passedParam: string[],
|
|
||||||
options: MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
workspaceIds: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.log('Running command to fix bodyV2 field position');
|
|
||||||
|
|
||||||
if (isCommandLogger(this.logger)) {
|
|
||||||
this.logger.setVerbose(options.verbose ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
|
||||||
await this.processWorkspace(workspaceId, index, workspaceIds.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(chalk.green('Command completed!'));
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(chalk.red('Error executing command'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processWorkspace(
|
|
||||||
workspaceId: string,
|
|
||||||
index: number,
|
|
||||||
total: number,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.logger.log(
|
|
||||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewRepository =
|
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
|
||||||
workspaceId,
|
|
||||||
'view',
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewFieldRepository =
|
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
|
|
||||||
workspaceId,
|
|
||||||
'viewField',
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const taskAndNoteObjectMetadatas =
|
|
||||||
await this.objectMetadataRepository.find({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
nameSingular: In(['note', 'task']),
|
|
||||||
},
|
|
||||||
relations: ['fields'],
|
|
||||||
});
|
|
||||||
|
|
||||||
const taskAndNoteViews = await viewRepository.find({
|
|
||||||
where: {
|
|
||||||
objectMetadataId: In(
|
|
||||||
taskAndNoteObjectMetadatas.map((object) => object.id),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const fieldMetadatas = taskAndNoteObjectMetadatas.flatMap(
|
|
||||||
(objectMetadata) => objectMetadata.fields,
|
|
||||||
);
|
|
||||||
|
|
||||||
const fieldNameByMetadataId: Record<string, string> =
|
|
||||||
fieldMetadatas.reduce(
|
|
||||||
(fieldNameByMetadataId, fieldMetadata) => ({
|
|
||||||
...fieldNameByMetadataId,
|
|
||||||
[fieldMetadata.id]: fieldMetadata.name,
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const view of taskAndNoteViews) {
|
|
||||||
this.logger.log(
|
|
||||||
`Updating bodyV2 field position for view ${view.id} - ${view.name}`,
|
|
||||||
);
|
|
||||||
const viewFields = await viewFieldRepository.find({
|
|
||||||
where: {
|
|
||||||
viewId: view.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bodyViewField = viewFields.find(
|
|
||||||
(viewField) =>
|
|
||||||
fieldNameByMetadataId[viewField.fieldMetadataId] === 'body',
|
|
||||||
);
|
|
||||||
const bodyV2ViewField = viewFields.find(
|
|
||||||
(viewField) =>
|
|
||||||
fieldNameByMetadataId[viewField.fieldMetadataId] === 'bodyV2',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (bodyViewField && bodyV2ViewField) {
|
|
||||||
this.logger.log(
|
|
||||||
`Setting body field position to ${bodyV2ViewField?.position} and bodyV2 field position to ${bodyViewField?.position}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await viewFieldRepository.update(
|
|
||||||
{ id: bodyViewField.id },
|
|
||||||
{
|
|
||||||
position: bodyV2ViewField.position,
|
|
||||||
isVisible: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await viewFieldRepository.update(
|
|
||||||
{ id: bodyV2ViewField.id },
|
|
||||||
{
|
|
||||||
position: bodyViewField.position,
|
|
||||||
isVisible: bodyViewField.isVisible,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (bodyViewField && !bodyV2ViewField) {
|
|
||||||
this.logger.log(
|
|
||||||
`Creating bodyV2 view field for view ${view.id} with position ${viewFields.length}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const bodyV2FieldMetadataId = fieldMetadatas.find(
|
|
||||||
(field) => field.name === 'bodyV2',
|
|
||||||
)?.id;
|
|
||||||
|
|
||||||
const viewFieldToCreate = viewFieldRepository.create({
|
|
||||||
fieldMetadataId: bodyV2FieldMetadataId,
|
|
||||||
viewId: view.id,
|
|
||||||
position: bodyViewField.position,
|
|
||||||
isVisible: bodyViewField.isVisible,
|
|
||||||
size: bodyViewField.size,
|
|
||||||
aggregateOperation: bodyViewField.aggregateOperation,
|
|
||||||
});
|
|
||||||
|
|
||||||
await viewFieldRepository.save(viewFieldToCreate);
|
|
||||||
|
|
||||||
await viewFieldRepository.update(
|
|
||||||
{ id: bodyViewField.id },
|
|
||||||
{
|
|
||||||
position: viewFields.length,
|
|
||||||
isVisible: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
chalk.green(`Command completed for workspace ${workspaceId}`),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(chalk.red(`Error in workspace ${workspaceId}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { CommandLogger } from 'src/database/commands/logger';
|
|
||||||
import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
|
||||||
import {
|
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
|
||||||
import { settings } from 'src/engine/constants/settings';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.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 { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
|
||||||
|
|
||||||
@MigrationCommand({
|
|
||||||
name: 'limit-amount-of-view-field',
|
|
||||||
description: 'Limit amount of view field.',
|
|
||||||
version: '0.42',
|
|
||||||
})
|
|
||||||
export class LimitAmountOfViewFieldCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
|
||||||
protected readonly logger: CommandLogger;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(Workspace, 'core')
|
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager);
|
|
||||||
this.logger = new CommandLogger({
|
|
||||||
constructorName: this.constructor.name,
|
|
||||||
verbose: false,
|
|
||||||
});
|
|
||||||
this.logger.setVerbose(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async runOnWorkspace(workspaceId: string, dryRun?: boolean): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
`Processing workspace ${workspaceId} for view field limitation`,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
const viewRepository =
|
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
ViewWorkspaceEntity,
|
|
||||||
);
|
|
||||||
|
|
||||||
const views = await viewRepository.find({});
|
|
||||||
|
|
||||||
for (const view of views) {
|
|
||||||
const viewFieldRepository =
|
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
ViewFieldWorkspaceEntity,
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewFields = await viewFieldRepository.find({
|
|
||||||
where: {
|
|
||||||
viewId: view.id,
|
|
||||||
isVisible: true,
|
|
||||||
},
|
|
||||||
order: {
|
|
||||||
position: 'ASC',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (viewFields.length > settings.maxVisibleViewFields) {
|
|
||||||
const extraFields = viewFields.slice(settings.maxVisibleViewFields);
|
|
||||||
|
|
||||||
for (const field of extraFields) {
|
|
||||||
this.logger.log(
|
|
||||||
`Workspace ${workspaceId} - Hiding field ${field.id} in view ${view.id} (position ${field.position})`,
|
|
||||||
);
|
|
||||||
if (!dryRun) {
|
|
||||||
await viewFieldRepository.update(
|
|
||||||
{ id: field.id },
|
|
||||||
{ isVisible: false },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Error limiting view fields in workspace ${workspaceId}`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
|
||||||
_passedParam: string[],
|
|
||||||
options: MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
workspaceIds: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.log(`Running limit-amount-of-view-field command`);
|
|
||||||
|
|
||||||
if (options?.dryRun) {
|
|
||||||
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
|
||||||
try {
|
|
||||||
await this.runOnWorkspace(workspaceId, options?.dryRun);
|
|
||||||
this.logger.verbose(
|
|
||||||
`Processed workspace: ${workspaceId} (${index + 1}/${
|
|
||||||
workspaceIds.length
|
|
||||||
})`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error for workspace: ${workspaceId}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,462 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { ServerBlockNoteEditor } from '@blocknote/server-util';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { Option } from 'nest-commander';
|
|
||||||
import { FieldMetadataType, isDefined } from 'twenty-shared';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { isCommandLogger } from 'src/database/commands/logger';
|
|
||||||
import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
|
||||||
import {
|
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
|
||||||
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 { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
|
||||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
|
||||||
import {
|
|
||||||
WorkspaceMigrationColumnActionType,
|
|
||||||
WorkspaceMigrationColumnCreate,
|
|
||||||
WorkspaceMigrationTableAction,
|
|
||||||
WorkspaceMigrationTableActionType,
|
|
||||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
|
||||||
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
|
||||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
|
||||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
|
||||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
|
||||||
import {
|
|
||||||
NOTE_STANDARD_FIELD_IDS,
|
|
||||||
TASK_STANDARD_FIELD_IDS,
|
|
||||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
|
||||||
|
|
||||||
type MigrateRichTextContentArgs = {
|
|
||||||
richTextFieldsWithHasCreatedColumns: RichTextFieldWithHasCreatedColumnsAndObjectMetadata[];
|
|
||||||
workspaceId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RichTextFieldWithHasCreatedColumnsAndObjectMetadata = {
|
|
||||||
richTextField: FieldMetadataEntity;
|
|
||||||
hasCreatedColumns: boolean;
|
|
||||||
objectMetadata: ObjectMetadataEntity | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProcessWorkspaceArgs = {
|
|
||||||
workspaceId: string;
|
|
||||||
index: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProcessRichTextFieldsArgs = {
|
|
||||||
richTextFields: FieldMetadataEntity[];
|
|
||||||
workspaceId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MigrateRichTextFieldCommandOptions =
|
|
||||||
MaintainedWorkspacesMigrationCommandOptions & {
|
|
||||||
force?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
@MigrationCommand({
|
|
||||||
name: 'migrate-rich-text-field',
|
|
||||||
description: 'Migrate RICH_TEXT fields to new composite structure',
|
|
||||||
version: '0.42',
|
|
||||||
})
|
|
||||||
export class MigrateRichTextFieldCommand extends MaintainedWorkspacesMigrationCommandRunner<MigrateRichTextFieldCommandOptions> {
|
|
||||||
private options: MigrateRichTextFieldCommandOptions;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(Workspace, 'core')
|
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
|
||||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
|
||||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
|
||||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
|
||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
|
||||||
@InjectRepository(FeatureFlag, 'core')
|
|
||||||
protected readonly featureFlagRepository: Repository<FeatureFlag>,
|
|
||||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
|
||||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
|
||||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
|
||||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Option({
|
|
||||||
flags: '-f, --force [boolean]',
|
|
||||||
description:
|
|
||||||
'Force RICH_TEXT_FIELD value update even if column migration has already be run',
|
|
||||||
required: false,
|
|
||||||
})
|
|
||||||
parseForceValue(val?: boolean): boolean {
|
|
||||||
return val ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
|
||||||
_passedParam: string[],
|
|
||||||
options: MigrateRichTextFieldCommandOptions,
|
|
||||||
workspaceIds: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
'Running command to migrate RICH_TEXT fields to new composite structure',
|
|
||||||
);
|
|
||||||
if (options.force) {
|
|
||||||
this.logger.warn('Running in force mode');
|
|
||||||
}
|
|
||||||
this.options = options;
|
|
||||||
if (isCommandLogger(this.logger)) {
|
|
||||||
this.logger.setVerbose(options.verbose ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
|
||||||
try {
|
|
||||||
await this.processWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
index,
|
|
||||||
total: workspaceIds.length,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.red(`Error in workspace ${workspaceId}: ${error}`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(chalk.green('Command completed!'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processWorkspace({
|
|
||||||
index,
|
|
||||||
total,
|
|
||||||
workspaceId,
|
|
||||||
}: ProcessWorkspaceArgs): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.logger.log(
|
|
||||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const richTextFields = await this.fieldMetadataRepository.find({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
type: FieldMetadataType.RICH_TEXT,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!richTextFields.length) {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.yellow('No RICH_TEXT fields found in this workspace'),
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(`Found ${richTextFields.length} RICH_TEXT fields`);
|
|
||||||
|
|
||||||
const richTextFieldsWithHasCreatedColumns =
|
|
||||||
await this.createIfMissingNewRichTextFieldsColumn({
|
|
||||||
richTextFields,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.migrateToNewRichTextFieldsColumn({
|
|
||||||
richTextFieldsWithHasCreatedColumns,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this.options.dryRun) {
|
|
||||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
chalk.green(`Command completed for workspace ${workspaceId}`),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(chalk.red(`Error in workspace ${workspaceId}: ${error}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildRichTextFieldStandardId(richTextField: FieldMetadataEntity) {
|
|
||||||
switch (true) {
|
|
||||||
case richTextField.standardId === TASK_STANDARD_FIELD_IDS.body: {
|
|
||||||
return TASK_STANDARD_FIELD_IDS.bodyV2;
|
|
||||||
}
|
|
||||||
case richTextField.standardId === NOTE_STANDARD_FIELD_IDS.body: {
|
|
||||||
return NOTE_STANDARD_FIELD_IDS.bodyV2;
|
|
||||||
}
|
|
||||||
case richTextField.isCustom: {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
throw new Error(
|
|
||||||
`RICH_TEXT does not belong to a Task or a Note standard objects: ${richTextField.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createMarkdownBlockNoteV2Columns({
|
|
||||||
richTextField,
|
|
||||||
workspaceId,
|
|
||||||
objectMetadata,
|
|
||||||
fieldMetadataAlreadyExisting,
|
|
||||||
}: {
|
|
||||||
objectMetadata: ObjectMetadataEntity;
|
|
||||||
richTextField: FieldMetadataEntity;
|
|
||||||
workspaceId: string;
|
|
||||||
fieldMetadataAlreadyExisting: boolean;
|
|
||||||
}) {
|
|
||||||
const columnsToCreate: WorkspaceMigrationColumnCreate[] = [
|
|
||||||
{
|
|
||||||
action: WorkspaceMigrationColumnActionType.CREATE,
|
|
||||||
columnName: `${richTextField.name}V2Blocknote`,
|
|
||||||
columnType: 'text',
|
|
||||||
isNullable: true,
|
|
||||||
defaultValue: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: WorkspaceMigrationColumnActionType.CREATE,
|
|
||||||
columnName: `${richTextField.name}V2Markdown`,
|
|
||||||
columnType: 'text',
|
|
||||||
isNullable: true,
|
|
||||||
defaultValue: null,
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const shouldForceCreateColumns =
|
|
||||||
this.options.force && fieldMetadataAlreadyExisting;
|
|
||||||
|
|
||||||
if (shouldForceCreateColumns) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Force creating V2 columns for workspaceId: ${workspaceId} objectMetadaId: ${objectMetadata.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const shouldCreateColumns =
|
|
||||||
!fieldMetadataAlreadyExisting || shouldForceCreateColumns;
|
|
||||||
|
|
||||||
if (!this.options.dryRun && shouldCreateColumns) {
|
|
||||||
await this.workspaceMigrationService.createCustomMigration(
|
|
||||||
generateMigrationName(
|
|
||||||
`migrate-rich-text-field-${objectMetadata.nameSingular}-${richTextField.name}`,
|
|
||||||
),
|
|
||||||
workspaceId,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: computeObjectTargetTable(objectMetadata),
|
|
||||||
action: WorkspaceMigrationTableActionType.ALTER,
|
|
||||||
columns: columnsToCreate,
|
|
||||||
} satisfies WorkspaceMigrationTableAction,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return shouldCreateColumns;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createIfMissingNewRichTextFieldsColumn({
|
|
||||||
richTextFields,
|
|
||||||
workspaceId,
|
|
||||||
}: ProcessRichTextFieldsArgs): Promise<
|
|
||||||
RichTextFieldWithHasCreatedColumnsAndObjectMetadata[]
|
|
||||||
> {
|
|
||||||
const richTextFieldsWithHasCreatedColumns: RichTextFieldWithHasCreatedColumnsAndObjectMetadata[] =
|
|
||||||
[];
|
|
||||||
|
|
||||||
for (const richTextField of richTextFields) {
|
|
||||||
const standardId = this.buildRichTextFieldStandardId(richTextField);
|
|
||||||
|
|
||||||
const newRichTextField: Partial<FieldMetadataEntity> = {
|
|
||||||
...richTextField,
|
|
||||||
name: `${richTextField.name}V2`,
|
|
||||||
id: undefined,
|
|
||||||
type: FieldMetadataType.RICH_TEXT_V2,
|
|
||||||
defaultValue: null,
|
|
||||||
standardId,
|
|
||||||
workspaceId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const existingFieldMetadata =
|
|
||||||
await this.fieldMetadataRepository.findOneBy({
|
|
||||||
name: newRichTextField.name,
|
|
||||||
type: newRichTextField.type,
|
|
||||||
standardId: newRichTextField.standardId ?? undefined,
|
|
||||||
workspaceId,
|
|
||||||
});
|
|
||||||
const fieldMetadataAlreadyExisting = isDefined(existingFieldMetadata);
|
|
||||||
|
|
||||||
if (fieldMetadataAlreadyExisting) {
|
|
||||||
this.logger.warn(
|
|
||||||
`FieldMetadata already exists in fieldMetadataRepository name: ${newRichTextField.name} standardId: ${newRichTextField.standardId} type: ${newRichTextField.type} workspaceId: ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.options.dryRun && !fieldMetadataAlreadyExisting) {
|
|
||||||
await this.fieldMetadataRepository.insert(newRichTextField);
|
|
||||||
}
|
|
||||||
|
|
||||||
const objectMetadata = await this.objectMetadataRepository.findOne({
|
|
||||||
where: { id: richTextField.objectMetadataId },
|
|
||||||
relations: {
|
|
||||||
fields: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (objectMetadata === null) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Object metadata not found for rich text field ${richTextField.name} in workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
richTextFieldsWithHasCreatedColumns.push({
|
|
||||||
hasCreatedColumns: false,
|
|
||||||
richTextField,
|
|
||||||
objectMetadata,
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasCreatedColumns = await this.createMarkdownBlockNoteV2Columns({
|
|
||||||
objectMetadata,
|
|
||||||
richTextField,
|
|
||||||
workspaceId,
|
|
||||||
fieldMetadataAlreadyExisting,
|
|
||||||
});
|
|
||||||
|
|
||||||
richTextFieldsWithHasCreatedColumns.push({
|
|
||||||
hasCreatedColumns: hasCreatedColumns ?? false,
|
|
||||||
richTextField,
|
|
||||||
objectMetadata,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasAtLeastOnePendingMigration =
|
|
||||||
richTextFieldsWithHasCreatedColumns.some(
|
|
||||||
({ hasCreatedColumns }) => hasCreatedColumns,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!this.options.dryRun && hasAtLeastOnePendingMigration) {
|
|
||||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return richTextFieldsWithHasCreatedColumns;
|
|
||||||
}
|
|
||||||
|
|
||||||
private jsonParseOrSilentlyFail(input: string): null | unknown {
|
|
||||||
try {
|
|
||||||
return JSON.parse(input);
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getMardownFieldValue({
|
|
||||||
blocknoteFieldValue,
|
|
||||||
serverBlockNoteEditor,
|
|
||||||
}: {
|
|
||||||
blocknoteFieldValue: string | null;
|
|
||||||
serverBlockNoteEditor: ServerBlockNoteEditor;
|
|
||||||
}): Promise<string | null> {
|
|
||||||
const blocknoteFieldValueIsDefined =
|
|
||||||
blocknoteFieldValue !== null &&
|
|
||||||
blocknoteFieldValue !== undefined &&
|
|
||||||
blocknoteFieldValue !== '{}';
|
|
||||||
|
|
||||||
if (!blocknoteFieldValueIsDefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonParsedblocknoteFieldValue =
|
|
||||||
this.jsonParseOrSilentlyFail(blocknoteFieldValue);
|
|
||||||
|
|
||||||
if (jsonParsedblocknoteFieldValue === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(jsonParsedblocknoteFieldValue)) {
|
|
||||||
this.logger.warn(
|
|
||||||
`blocknoteFieldValue is defined and is not an array got ${blocknoteFieldValue}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let markdown: string | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
markdown = await serverBlockNoteEditor.blocksToMarkdownLossy(
|
|
||||||
jsonParsedblocknoteFieldValue,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Error converting blocknote to markdown for ${blocknoteFieldValue}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return markdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async migrateToNewRichTextFieldsColumn({
|
|
||||||
richTextFieldsWithHasCreatedColumns,
|
|
||||||
workspaceId,
|
|
||||||
}: MigrateRichTextContentArgs) {
|
|
||||||
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
|
|
||||||
|
|
||||||
for (const {
|
|
||||||
richTextField,
|
|
||||||
hasCreatedColumns,
|
|
||||||
objectMetadata,
|
|
||||||
} of richTextFieldsWithHasCreatedColumns) {
|
|
||||||
if (objectMetadata === null) {
|
|
||||||
this.logger.log(
|
|
||||||
`Object metadata not found for rich text field ${richTextField.name} in workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schemaName =
|
|
||||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
|
||||||
|
|
||||||
const workspaceDataSource =
|
|
||||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const rows = await workspaceDataSource.query(
|
|
||||||
`SELECT id, "${richTextField.name}" FROM "${schemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}"`,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.log(`Generating markdown for ${rows.length} records`);
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const blocknoteFieldValue = row[richTextField.name];
|
|
||||||
const markdownFieldValue = await this.getMardownFieldValue({
|
|
||||||
blocknoteFieldValue,
|
|
||||||
serverBlockNoteEditor,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.options.force) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Force udpate rowId: ${row.id} RICH_TEXT_FIELD ${richTextField.id} objectMetadata ${objectMetadata.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!this.options.dryRun && (hasCreatedColumns || this.options.force)) {
|
|
||||||
await workspaceDataSource.query(
|
|
||||||
`UPDATE "${schemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" SET "${richTextField.name}V2Blocknote" = $1, "${richTextField.name}V2Markdown" = $2 WHERE id = $3`,
|
|
||||||
[blocknoteFieldValue, markdownFieldValue, row.id],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { FieldMetadataType } from 'twenty-shared';
|
|
||||||
import { IsNull, Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { CommandLogger } from 'src/database/commands/logger';
|
|
||||||
import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
|
||||||
import {
|
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
||||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
|
||||||
|
|
||||||
@MigrationCommand({
|
|
||||||
name: 'standardization-of-actor-composite-context-type',
|
|
||||||
description: 'Add context to actor composite type.',
|
|
||||||
version: '0.42',
|
|
||||||
})
|
|
||||||
export class StandardizationOfActorCompositeContextTypeCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
|
||||||
protected readonly logger;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(Workspace, 'core')
|
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
|
||||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
|
||||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
|
||||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager);
|
|
||||||
|
|
||||||
this.logger = new CommandLogger({
|
|
||||||
constructorName: this.constructor.name,
|
|
||||||
verbose: false,
|
|
||||||
});
|
|
||||||
this.logger.setVerbose(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
|
||||||
_passedParam: string[],
|
|
||||||
options: MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
workspaceIds: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.log(`Running add-context-to-actor-composite-type command`);
|
|
||||||
|
|
||||||
if (options?.dryRun) {
|
|
||||||
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
|
||||||
try {
|
|
||||||
await this.runOnWorkspace(workspaceId, options?.dryRun);
|
|
||||||
this.logger.verbose(
|
|
||||||
`[${index + 1}/${workspaceIds.length}] Added for workspace: ${workspaceId}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error for workspace: ${workspaceId}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runOnWorkspace(
|
|
||||||
workspaceId: string,
|
|
||||||
dryRun = false,
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.verbose(`Adding for workspace: ${workspaceId}`);
|
|
||||||
const actorFields = await this.fieldMetadataRepository.find({
|
|
||||||
where: {
|
|
||||||
type: FieldMetadataType.ACTOR,
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
relations: ['object'],
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const field of actorFields) {
|
|
||||||
if (!field || !field.object) {
|
|
||||||
this.logger.verbose(
|
|
||||||
'field.objectMetadata is null',
|
|
||||||
workspaceId,
|
|
||||||
field.id,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldRepository =
|
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
field.object.nameSingular,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!dryRun) {
|
|
||||||
const rowsToUpdate = await fieldRepository.update(
|
|
||||||
{
|
|
||||||
[field.name + 'Context']: IsNull(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[field.name + 'Context']: {},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.verbose(
|
|
||||||
`updated ${rowsToUpdate ? rowsToUpdate.affected : 0} rows`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dryRun) {
|
|
||||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { MigrationCommandModule } from 'src/database/commands/migration-command/migration-command.module';
|
|
||||||
import { FixBodyV2ViewFieldPositionCommand } from 'src/database/commands/upgrade-version/0-42/0-42-fix-body-v2-view-field-position.command';
|
|
||||||
import { LimitAmountOfViewFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-limit-amount-of-view-field';
|
|
||||||
import { MigrateRichTextFieldCommand } from 'src/database/commands/upgrade-version/0-42/0-42-migrate-rich-text-field.command';
|
|
||||||
import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type';
|
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
|
||||||
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 { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
|
||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
|
||||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
|
||||||
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
MigrationCommandModule.register('0.42', {
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([Workspace, FeatureFlag], 'core'),
|
|
||||||
TypeOrmModule.forFeature(
|
|
||||||
[ObjectMetadataEntity, FieldMetadataEntity],
|
|
||||||
'metadata',
|
|
||||||
),
|
|
||||||
WorkspaceSyncMetadataCommandsModule,
|
|
||||||
WorkspaceMigrationRunnerModule,
|
|
||||||
WorkspaceMigrationModule,
|
|
||||||
WorkspaceMetadataVersionModule,
|
|
||||||
WorkspaceDataSourceModule,
|
|
||||||
FeatureFlagModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
MigrateRichTextFieldCommand,
|
|
||||||
FixBodyV2ViewFieldPositionCommand,
|
|
||||||
LimitAmountOfViewFieldCommand,
|
|
||||||
StandardizationOfActorCompositeContextTypeCommand,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class UpgradeTo0_42CommandModule {}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
|
||||||
import {
|
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
|
||||||
|
|
||||||
@MigrationCommand({
|
|
||||||
name: 'migrate-is-searchable-for-custom-object-metadata',
|
|
||||||
description: 'Set isSearchable true for custom object metadata',
|
|
||||||
version: '0.43',
|
|
||||||
})
|
|
||||||
export class MigrateIsSearchableForCustomObjectMetadataCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(Workspace, 'core')
|
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
|
||||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
|
||||||
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
|
||||||
_passedParam: string[],
|
|
||||||
_options: MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
workspaceIds: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.green(
|
|
||||||
'Running command to set isSearchable true for custom object metadata',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
|
||||||
await this.processWorkspace(workspaceId, index, workspaceIds.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(chalk.green('Command completed!'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processWorkspace(
|
|
||||||
workspaceId: string,
|
|
||||||
index: number,
|
|
||||||
total: number,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.logger.log(
|
|
||||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.objectMetadataRepository.update(
|
|
||||||
{
|
|
||||||
workspaceId,
|
|
||||||
isCustom: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isSearchable: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.red(`Error in workspace ${workspaceId} - ${error.message}`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { MigrationCommandModule } from 'src/database/commands/migration-command/migration-command.module';
|
|
||||||
import { StandardizationOfActorCompositeContextTypeCommand } from 'src/database/commands/upgrade-version/0-42/0-42-standardization-of-actor-composite-context-type';
|
|
||||||
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version/0-43/0-43-add-tasks-assigned-to-me-view.command';
|
|
||||||
import { MigrateIsSearchableForCustomObjectMetadataCommand } from 'src/database/commands/upgrade-version/0-43/0-43-migrate-is-searchable-for-custom-object-metadata.command';
|
|
||||||
import { MigrateRichTextContentPatchCommand } from 'src/database/commands/upgrade-version/0-43/0-43-migrate-rich-text-content-patch.command';
|
|
||||||
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
|
|
||||||
import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command';
|
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
|
||||||
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 { SearchModule } from 'src/engine/metadata-modules/search/search.module';
|
|
||||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
|
||||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
|
||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
|
||||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
MigrationCommandModule.register('0.43', {
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([Workspace, FeatureFlag], 'core'),
|
|
||||||
TypeOrmModule.forFeature(
|
|
||||||
[ObjectMetadataEntity, FieldMetadataEntity],
|
|
||||||
'metadata',
|
|
||||||
),
|
|
||||||
SearchModule,
|
|
||||||
WorkspaceMigrationRunnerModule,
|
|
||||||
WorkspaceMigrationModule,
|
|
||||||
WorkspaceMetadataVersionModule,
|
|
||||||
WorkspaceDataSourceModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
AddTasksAssignedToMeViewCommand,
|
|
||||||
MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
|
|
||||||
MigrateIsSearchableForCustomObjectMetadataCommand,
|
|
||||||
UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
|
|
||||||
StandardizationOfActorCompositeContextTypeCommand,
|
|
||||||
MigrateRichTextContentPatchCommand,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class UpgradeTo0_43CommandModule {}
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import chalk from 'chalk';
|
|
||||||
import { FieldMetadataType } from 'twenty-shared';
|
|
||||||
import { In, Repository } from 'typeorm';
|
|
||||||
|
|
||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
|
||||||
|
|
||||||
import { isCommandLogger } from 'src/database/commands/logger';
|
|
||||||
import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
|
||||||
import {
|
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
|
||||||
import {
|
|
||||||
RelationDirection,
|
|
||||||
deduceRelationDirection,
|
|
||||||
} from 'src/engine/utils/deduce-relation-direction.util';
|
|
||||||
import { isFieldMetadataOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
|
||||||
|
|
||||||
@MigrationCommand({
|
|
||||||
name: 'migrate-relations-to-field-metadata',
|
|
||||||
description: 'Migrate relations to field metadata',
|
|
||||||
version: '0.44',
|
|
||||||
})
|
|
||||||
export class MigrateRelationsToFieldMetadataCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(Workspace, 'core')
|
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
|
||||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
|
||||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
|
||||||
) {
|
|
||||||
super(workspaceRepository, twentyORMGlobalManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
|
||||||
_passedParam: string[],
|
|
||||||
options: MaintainedWorkspacesMigrationCommandOptions,
|
|
||||||
workspaceIds: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.log('Running command to create many to one relations');
|
|
||||||
|
|
||||||
if (isCommandLogger(this.logger)) {
|
|
||||||
this.logger.setVerbose(options.verbose ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
|
||||||
await this.processWorkspace(workspaceId, index, workspaceIds.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(chalk.green('Command completed!'));
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(chalk.red('Error in workspace'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async processWorkspace(
|
|
||||||
workspaceId: string,
|
|
||||||
index: number,
|
|
||||||
total: number,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.logger.log(
|
|
||||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const fieldMetadataCollection = await this.fieldMetadataRepository.find({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
|
||||||
type: In([FieldMetadataType.RELATION, FieldMetadataType.UUID]),
|
|
||||||
},
|
|
||||||
relations: ['fromRelationMetadata', 'toRelationMetadata'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fieldMetadataCollection.length) {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.yellow(
|
|
||||||
`No relation field metadata found for workspace ${workspaceId}.`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const joinColumnFieldMetadataCollection = fieldMetadataCollection.filter(
|
|
||||||
(fieldMetadata) =>
|
|
||||||
isFieldMetadataOfType(fieldMetadata, FieldMetadataType.UUID),
|
|
||||||
// TODO: Fix this, it's working in other places but not here
|
|
||||||
) as FieldMetadataEntity<FieldMetadataType.UUID>[];
|
|
||||||
|
|
||||||
const fieldMetadataToUpdateCollection = fieldMetadataCollection
|
|
||||||
.filter((fieldMetadata) =>
|
|
||||||
isFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION),
|
|
||||||
)
|
|
||||||
.map((fieldMetadata) =>
|
|
||||||
this.updateRelationFieldMetadata(
|
|
||||||
joinColumnFieldMetadataCollection,
|
|
||||||
// TODO: Fix this, it's working in other places but not here
|
|
||||||
fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fieldMetadataToUpdateCollection.length > 0) {
|
|
||||||
await this.fieldMetadataRepository.save(
|
|
||||||
fieldMetadataToUpdateCollection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
chalk.green(`Command completed for workspace ${workspaceId}.`),
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateRelationFieldMetadata(
|
|
||||||
joinColumnFieldMetadataCollection: FieldMetadataEntity<FieldMetadataType.UUID>[],
|
|
||||||
fieldMetadata: FieldMetadataEntity<FieldMetadataType.RELATION>,
|
|
||||||
): FieldMetadataEntity<FieldMetadataType.RELATION> {
|
|
||||||
const relationMetadata =
|
|
||||||
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
|
||||||
const joinColumnFieldMetadata = joinColumnFieldMetadataCollection.find(
|
|
||||||
(joinColumnFieldMetadata) =>
|
|
||||||
// We're deducing the field based on the name of the relation field
|
|
||||||
// This is not the best way to do this but we don't have a better way
|
|
||||||
joinColumnFieldMetadata.name === `${fieldMetadata.name}Id`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const relationDirection = deduceRelationDirection(
|
|
||||||
fieldMetadata,
|
|
||||||
relationMetadata,
|
|
||||||
);
|
|
||||||
let relationType = relationMetadata.relationType as unknown as RelationType;
|
|
||||||
|
|
||||||
if (
|
|
||||||
relationDirection === RelationDirection.TO &&
|
|
||||||
relationType === RelationType.ONE_TO_MANY
|
|
||||||
) {
|
|
||||||
relationType = RelationType.MANY_TO_ONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relationTargetFieldMetadataId =
|
|
||||||
relationDirection === RelationDirection.FROM
|
|
||||||
? relationMetadata.toFieldMetadataId
|
|
||||||
: relationMetadata.fromFieldMetadataId;
|
|
||||||
|
|
||||||
const relationTargetObjectMetadataId =
|
|
||||||
relationDirection === RelationDirection.FROM
|
|
||||||
? relationMetadata.toObjectMetadataId
|
|
||||||
: relationMetadata.fromObjectMetadataId;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...fieldMetadata,
|
|
||||||
settings: {
|
|
||||||
relationType,
|
|
||||||
onDelete: relationMetadata.onDeleteAction,
|
|
||||||
joinColumnName: joinColumnFieldMetadata?.name,
|
|
||||||
},
|
|
||||||
relationTargetFieldMetadataId,
|
|
||||||
relationTargetObjectMetadataId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
import { MigrationCommandModule } from 'src/database/commands/migration-command/migration-command.module';
|
|
||||||
import { InitializePermissionsCommand } from 'src/database/commands/upgrade-version/0-44/0-44-initialize-permissions.command';
|
|
||||||
import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version/0-44/0-44-migrate-relations-to-field-metadata.command';
|
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
|
||||||
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 { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
|
||||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
|
||||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
|
||||||
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
|
|
||||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
MigrationCommandModule.register('0.44', {
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature(
|
|
||||||
[Workspace, FeatureFlag, UserWorkspace],
|
|
||||||
'core',
|
|
||||||
),
|
|
||||||
TypeOrmModule.forFeature(
|
|
||||||
[ObjectMetadataEntity, FieldMetadataEntity],
|
|
||||||
'metadata',
|
|
||||||
),
|
|
||||||
WorkspaceMigrationRunnerModule,
|
|
||||||
WorkspaceMigrationModule,
|
|
||||||
WorkspaceMetadataVersionModule,
|
|
||||||
UserRoleModule,
|
|
||||||
RoleModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
MigrateRelationsToFieldMetadataCommand,
|
|
||||||
InitializePermissionsCommand,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class UpgradeTo0_44CommandModule {}
|
|
||||||
@ -7,22 +7,19 @@ import { Command } from 'nest-commander';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
RunOnWorkspaceArgs,
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||||
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
|
||||||
interface SyncCustomerDataCommandOptions
|
|
||||||
extends MaintainedWorkspacesMigrationCommandOptions {}
|
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'billing:sync-customer-data',
|
name: 'billing:sync-customer-data',
|
||||||
description: 'Sync customer data from Stripe for all active workspaces',
|
description: 'Sync customer data from Stripe for all active workspaces',
|
||||||
})
|
})
|
||||||
export class BillingSyncCustomerDataCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
export class BillingSyncCustomerDataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
@ -34,39 +31,10 @@ export class BillingSyncCustomerDataCommand extends MaintainedWorkspacesMigratio
|
|||||||
super(workspaceRepository, twentyORMGlobalManager);
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
override async runOnWorkspace({
|
||||||
_passedParam: string[],
|
workspaceId,
|
||||||
options: SyncCustomerDataCommandOptions,
|
options,
|
||||||
workspaceIds: string[],
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
): Promise<void> {
|
|
||||||
this.logger.log('Running command to sync customer data');
|
|
||||||
|
|
||||||
for (const workspaceId of workspaceIds) {
|
|
||||||
this.logger.log(`Running command for workspace ${workspaceId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.syncCustomerDataForWorkspace(workspaceId, options);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.red(
|
|
||||||
`Running command on workspace ${workspaceId} failed with error: ${error}, ${error.stack}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
} finally {
|
|
||||||
this.logger.log(
|
|
||||||
chalk.green(`Finished running command for workspace ${workspaceId}.`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(chalk.green(`Command completed!`));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async syncCustomerDataForWorkspace(
|
|
||||||
workspaceId: string,
|
|
||||||
options: SyncCustomerDataCommandOptions,
|
|
||||||
): Promise<void> {
|
|
||||||
const billingCustomer = await this.billingCustomerRepository.findOne({
|
const billingCustomer = await this.billingCustomerRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { Repository } from 'typeorm';
|
|||||||
import {
|
import {
|
||||||
MigrationCommandOptions,
|
MigrationCommandOptions,
|
||||||
MigrationCommandRunner,
|
MigrationCommandRunner,
|
||||||
} from 'src/database/commands/migration-command/migration-command.runner';
|
} from 'src/database/commands/command-runners/migration.command-runner';
|
||||||
import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity';
|
import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity';
|
||||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||||
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { ModuleRef } from '@nestjs/core';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
|
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
|
||||||
import { DataSeedDemoWorkspaceJob } from 'src/database/commands/data-seed-demo-workspace/jobs/data-seed-demo-workspace.job';
|
|
||||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||||
@ -62,7 +61,6 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
|||||||
providers: [
|
providers: [
|
||||||
CleanSuspendedWorkspacesJob,
|
CleanSuspendedWorkspacesJob,
|
||||||
EmailSenderJob,
|
EmailSenderJob,
|
||||||
DataSeedDemoWorkspaceJob,
|
|
||||||
UpdateSubscriptionQuantityJob,
|
UpdateSubscriptionQuantityJob,
|
||||||
HandleWorkspaceMemberDeletedJob,
|
HandleWorkspaceMemberDeletedJob,
|
||||||
CleanWorkspaceDeletionWarningUserVarsJob,
|
CleanWorkspaceDeletionWarningUserVarsJob,
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator';
|
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
|
import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
|
||||||
import {
|
import {
|
||||||
@ -25,7 +24,6 @@ export class WorkspaceMetadataCacheService {
|
|||||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@LogExecutionTime()
|
|
||||||
async recomputeMetadataCache({
|
async recomputeMetadataCache({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
ignoreLock = false,
|
ignoreLock = false,
|
||||||
|
|||||||
@ -4,13 +4,11 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator';
|
|
||||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||||
import {
|
import {
|
||||||
WorkspaceMetadataVersionException,
|
WorkspaceMetadataVersionException,
|
||||||
WorkspaceMetadataVersionExceptionCode,
|
WorkspaceMetadataVersionExceptionCode,
|
||||||
} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception';
|
} from 'src/engine/metadata-modules/workspace-metadata-version/exceptions/workspace-metadata-version.exception';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceMetadataVersionService {
|
export class WorkspaceMetadataVersionService {
|
||||||
@ -20,10 +18,8 @@ export class WorkspaceMetadataVersionService {
|
|||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@LogExecutionTime()
|
|
||||||
async incrementMetadataVersion(workspaceId: string): Promise<void> {
|
async incrementMetadataVersion(workspaceId: string): Promise<void> {
|
||||||
const workspace = await this.workspaceRepository.findOne({
|
const workspace = await this.workspaceRepository.findOne({
|
||||||
where: { id: workspaceId },
|
where: { id: workspaceId },
|
||||||
|
|||||||
@ -68,10 +68,6 @@ export class WorkspaceDatasourceFactory {
|
|||||||
const result = await this.cacheManager.execute(
|
const result = await this.cacheManager.execute(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
async () => {
|
async () => {
|
||||||
this.logger.log(
|
|
||||||
`Creating workspace data source for workspace ${workspaceId} and metadata version ${cachedWorkspaceMetadataVersion}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const dataSourceMetadata =
|
const dataSourceMetadata =
|
||||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { In, Repository } from 'typeorm';
|
|||||||
import {
|
import {
|
||||||
MigrationCommandOptions,
|
MigrationCommandOptions,
|
||||||
MigrationCommandRunner,
|
MigrationCommandRunner,
|
||||||
} from 'src/database/commands/migration-command/migration-command.runner';
|
} from 'src/database/commands/command-runners/migration.command-runner';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service';
|
import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service';
|
||||||
|
|
||||||
|
|||||||
@ -1,35 +1,28 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { Command, Option } from 'nest-commander';
|
import { Command } from 'nest-commander';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MaintainedWorkspacesMigrationCommandOptions,
|
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||||
MaintainedWorkspacesMigrationCommandRunner,
|
RunOnWorkspaceArgs,
|
||||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service';
|
|
||||||
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
|
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
|
||||||
|
|
||||||
import { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.service';
|
import { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.service';
|
||||||
|
|
||||||
interface RunWorkspaceMigrationsOptions
|
|
||||||
extends MaintainedWorkspacesMigrationCommandOptions {
|
|
||||||
force?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'workspace:sync-metadata',
|
name: 'workspace:sync-metadata',
|
||||||
description: 'Sync metadata',
|
description: 'Sync metadata',
|
||||||
})
|
})
|
||||||
export class SyncWorkspaceMetadataCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
export class SyncWorkspaceMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
protected readonly workspaceRepository: Repository<Workspace>,
|
protected readonly workspaceRepository: Repository<Workspace>,
|
||||||
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
|
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
|
||||||
private readonly workspaceHealthService: WorkspaceHealthService,
|
|
||||||
private readonly dataSourceService: DataSourceService,
|
private readonly dataSourceService: DataSourceService,
|
||||||
private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService,
|
private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService,
|
||||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
@ -37,109 +30,40 @@ export class SyncWorkspaceMetadataCommand extends MaintainedWorkspacesMigrationC
|
|||||||
super(workspaceRepository, twentyORMGlobalManager);
|
super(workspaceRepository, twentyORMGlobalManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMigrationCommandOnMaintainedWorkspaces(
|
override async runOnWorkspace({
|
||||||
_passedParam: string[],
|
workspaceId,
|
||||||
options: RunWorkspaceMigrationsOptions,
|
options,
|
||||||
workspaceIds: string[],
|
index,
|
||||||
): Promise<void> {
|
total,
|
||||||
this.logger.log(`Attempting to sync ${workspaceIds.length} workspaces.`);
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
|
this.logger.log(
|
||||||
|
`Running workspace sync for workspace: ${workspaceId} (${index} out of ${total})`,
|
||||||
|
);
|
||||||
|
|
||||||
let count = 1;
|
const dataSourceMetadata =
|
||||||
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||||
const errorsDuringSync: string[] = [];
|
workspaceId,
|
||||||
|
|
||||||
for (const workspaceId of workspaceIds) {
|
|
||||||
this.logger.log(
|
|
||||||
`Running workspace sync for workspace: ${workspaceId} (${count} out of ${workspaceIds.length})`,
|
|
||||||
);
|
);
|
||||||
count++;
|
|
||||||
|
|
||||||
if (!options.force) {
|
const { storage, workspaceMigrations } =
|
||||||
try {
|
await this.workspaceSyncMetadataService.synchronize(
|
||||||
const issues =
|
{
|
||||||
await this.workspaceHealthService.healthCheck(workspaceId);
|
workspaceId,
|
||||||
|
dataSourceId: dataSourceMetadata.id,
|
||||||
|
},
|
||||||
|
{ applyChanges: !options.dryRun },
|
||||||
|
);
|
||||||
|
|
||||||
// Security: abort if there are issues.
|
if (options.dryRun) {
|
||||||
if (issues.length > 0) {
|
await this.syncWorkspaceLoggerService.saveLogs(
|
||||||
if (!options.force) {
|
workspaceId,
|
||||||
this.logger.error(
|
storage,
|
||||||
`Workspace contains ${issues.length} issues, aborting.`,
|
workspaceMigrations,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
'If you want to force the migration, use --force flag',
|
|
||||||
);
|
|
||||||
this.logger.log(
|
|
||||||
'Please use `workspace:health` command to check issues and fix them before running this command.',
|
|
||||||
);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.warn(
|
|
||||||
`Workspace contains ${issues.length} issues, sync has been forced.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (!options.force) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.warn(
|
|
||||||
`Workspace health check failed with error, but sync has been forced.`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const dataSourceMetadata =
|
|
||||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { storage, workspaceMigrations } =
|
|
||||||
await this.workspaceSyncMetadataService.synchronize(
|
|
||||||
{
|
|
||||||
workspaceId,
|
|
||||||
dataSourceId: dataSourceMetadata.id,
|
|
||||||
},
|
|
||||||
{ applyChanges: !options.dryRun },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (options.dryRun) {
|
|
||||||
await this.syncWorkspaceLoggerService.saveLogs(
|
|
||||||
workspaceId,
|
|
||||||
storage,
|
|
||||||
workspaceMigrations,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errorsDuringSync.push(
|
|
||||||
`Failed to synchronize workspace ${workspaceId}: ${error.message}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Finished synchronizing all active workspaces (${
|
`Finished synchronizing all active workspaces (${total} workspaces).`,
|
||||||
workspaceIds.length
|
|
||||||
} workspaces). ${
|
|
||||||
errorsDuringSync.length > 0
|
|
||||||
? 'Errors during sync:\n' + errorsDuringSync.join('.\n')
|
|
||||||
: ''
|
|
||||||
}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Option({
|
|
||||||
flags: '-f, --force',
|
|
||||||
description: 'Force migration',
|
|
||||||
required: false,
|
|
||||||
})
|
|
||||||
force(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
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 { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
@ -10,6 +12,8 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
|
|||||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||||
import { WorkspaceMigrationBuilderModule } from 'src/engine/workspace-manager/workspace-migration-builder/workspace-migration-builder.module';
|
import { WorkspaceMigrationBuilderModule } from 'src/engine/workspace-manager/workspace-migration-builder/workspace-migration-builder.module';
|
||||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||||
|
import { SyncWorkspaceLoggerService } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.service';
|
||||||
|
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||||
import { workspaceSyncMetadataComparators } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators';
|
import { workspaceSyncMetadataComparators } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators';
|
||||||
import { workspaceSyncMetadataFactories } from 'src/engine/workspace-manager/workspace-sync-metadata/factories';
|
import { workspaceSyncMetadataFactories } from 'src/engine/workspace-manager/workspace-sync-metadata/factories';
|
||||||
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
|
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
|
||||||
@ -34,7 +38,8 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
|
|||||||
],
|
],
|
||||||
'metadata',
|
'metadata',
|
||||||
),
|
),
|
||||||
TypeOrmModule.forFeature([FeatureFlag], 'core'),
|
DataSourceModule,
|
||||||
|
TypeOrmModule.forFeature([Workspace, FeatureFlag], 'core'),
|
||||||
WorkspaceMetadataVersionModule,
|
WorkspaceMetadataVersionModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
@ -47,7 +52,13 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
|
|||||||
WorkspaceSyncFieldMetadataService,
|
WorkspaceSyncFieldMetadataService,
|
||||||
WorkspaceSyncMetadataService,
|
WorkspaceSyncMetadataService,
|
||||||
WorkspaceSyncIndexMetadataService,
|
WorkspaceSyncIndexMetadataService,
|
||||||
|
SyncWorkspaceLoggerService,
|
||||||
|
SyncWorkspaceMetadataCommand,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
...workspaceSyncMetadataFactories,
|
||||||
|
WorkspaceSyncMetadataService,
|
||||||
|
SyncWorkspaceMetadataCommand,
|
||||||
],
|
],
|
||||||
exports: [...workspaceSyncMetadataFactories, WorkspaceSyncMetadataService],
|
|
||||||
})
|
})
|
||||||
export class WorkspaceSyncMetadataModule {}
|
export class WorkspaceSyncMetadataModule {}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ Upgrade your Twenty instance to use v0.43.0 image
|
|||||||
|
|
||||||
```
|
```
|
||||||
yarn database:migrate:prod
|
yarn database:migrate:prod
|
||||||
yarn command:prod upgrade-0.43
|
yarn command:prod upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
### v0.41.0 to v0.42.0
|
### v0.41.0 to v0.42.0
|
||||||
|
|||||||
Reference in New Issue
Block a user