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 { In, MoreThanOrEqual, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
MigrationCommandOptions,
|
||||
MigrationCommandRunner,
|
||||
} from 'src/database/commands/migration-command/migration-command.runner';
|
||||
import { MigrationCommandRunner } from 'src/database/commands/command-runners/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 type MaintainedWorkspacesMigrationCommandOptions =
|
||||
MigrationCommandOptions & {
|
||||
workspaceId?: string;
|
||||
export type ActiveOrSuspendedWorkspacesMigrationCommandOptions = {
|
||||
workspaceIds: string[];
|
||||
startFromWorkspaceId?: string;
|
||||
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
|
||||
MaintainedWorkspacesMigrationCommandOptions = MaintainedWorkspacesMigrationCommandOptions,
|
||||
> extends MigrationCommandRunner<Options> {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandOptions = ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
||||
> extends MigrationCommandRunner {
|
||||
private workspaceIds: string[] = [];
|
||||
private startFromWorkspaceId: string | undefined;
|
||||
private workspaceCountLimit: number | undefined;
|
||||
@ -97,20 +104,8 @@ export abstract class MaintainedWorkspacesMigrationCommandRunner<
|
||||
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(
|
||||
passedParams: string[],
|
||||
_passedParams: string[],
|
||||
options: Options,
|
||||
): Promise<void> {
|
||||
const activeWorkspaceIds =
|
||||
@ -118,22 +113,44 @@ export abstract class MaintainedWorkspacesMigrationCommandRunner<
|
||||
? this.workspaceIds
|
||||
: await this.fetchActiveWorkspaceIds();
|
||||
|
||||
this.logWorkspaceCount(activeWorkspaceIds);
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
|
||||
}
|
||||
|
||||
await this.runMigrationCommandOnMaintainedWorkspaces(
|
||||
passedParams,
|
||||
try {
|
||||
for (const [index, workspaceId] of activeWorkspaceIds.entries()) {
|
||||
this.logger.log(
|
||||
`Running command on workspace ${workspaceId} ${index + 1}/${activeWorkspaceIds.length}`,
|
||||
);
|
||||
|
||||
const dataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
false,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.runOnWorkspace({
|
||||
options,
|
||||
activeWorkspaceIds,
|
||||
workspaceId,
|
||||
dataSource,
|
||||
index: index,
|
||||
total: activeWorkspaceIds.length,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
chalk.red(`Error in workspace ${workspaceId}: ${error.message}`),
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract runMigrationCommandOnMaintainedWorkspaces(
|
||||
passedParams: string[],
|
||||
options: Options,
|
||||
activeWorkspaceIds: string[],
|
||||
): Promise<void>;
|
||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void>;
|
||||
}
|
||||
@ -3,23 +3,16 @@ import { Logger } from '@nestjs/common';
|
||||
import chalk from 'chalk';
|
||||
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';
|
||||
|
||||
export type MigrationCommandOptions = {
|
||||
workspaceId?: string;
|
||||
dryRun?: boolean;
|
||||
verbose?: boolean;
|
||||
};
|
||||
|
||||
export abstract class MigrationCommandRunner<
|
||||
Options extends MigrationCommandOptions = MigrationCommandOptions,
|
||||
>
|
||||
extends CommandRunner
|
||||
implements MigrationCommandInterface<Options>
|
||||
{
|
||||
export abstract class MigrationCommandRunner extends CommandRunner {
|
||||
protected logger: CommandLogger | Logger;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.logger = new CommandLogger({
|
||||
@ -46,7 +39,10 @@ export abstract class MigrationCommandRunner<
|
||||
return true;
|
||||
}
|
||||
|
||||
override async run(passedParams: string[], options: Options): Promise<void> {
|
||||
override async run(
|
||||
passedParams: string[],
|
||||
options: MigrationCommandOptions,
|
||||
): Promise<void> {
|
||||
if (options.verbose) {
|
||||
this.logger = new CommandLogger({
|
||||
verbose: true,
|
||||
@ -64,8 +60,8 @@ export abstract class MigrationCommandRunner<
|
||||
}
|
||||
}
|
||||
|
||||
abstract runMigrationCommand(
|
||||
protected abstract runMigrationCommand(
|
||||
passedParams: string[],
|
||||
options: Options,
|
||||
options: MigrationCommandOptions,
|
||||
): 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 { 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 { 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 { 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 { UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/upgrade-version-command.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 { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.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 { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UpgradeVersionCommandModule,
|
||||
|
||||
// Only needed for the data seed command
|
||||
TypeORMModule,
|
||||
FieldMetadataModule,
|
||||
ObjectMetadataModule,
|
||||
SeederModule,
|
||||
WorkspaceManagerModule,
|
||||
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 {}
|
||||
|
||||
@ -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 chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
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';
|
||||
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 { FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
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 { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
@MigrationCommand({
|
||||
name: 'add-tasks-assigned-to-me-view',
|
||||
@Command({
|
||||
name: 'upgrade:0-43: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(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@ -43,57 +41,25 @@ export class AddTasksAssignedToMeViewCommand extends MaintainedWorkspacesMigrati
|
||||
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 {
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
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,
|
||||
);
|
||||
await this.createTasksAssignedToMeView(workspaceId);
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Command completed for workspace ${workspaceId}.`),
|
||||
);
|
||||
} catch {
|
||||
this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`));
|
||||
}
|
||||
}
|
||||
|
||||
private async createTasksAssignedToMeView(
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
): Promise<void> {
|
||||
const objectMetadata = await this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
relations: ['fields'],
|
||||
@ -135,9 +101,13 @@ export class AddTasksAssignedToMeViewCommand extends MaintainedWorkspacesMigrati
|
||||
});
|
||||
|
||||
if (existingView) {
|
||||
throw new Error(
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`"Assigned to Me" view already exists for workspace ${workspaceId}`,
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const viewDefinition = tasksAssignedToMeView(objectMetadataMap);
|
||||
@ -191,7 +161,7 @@ export class AddTasksAssignedToMeViewCommand extends MaintainedWorkspacesMigrati
|
||||
await viewFilterRepository.save(viewFilters);
|
||||
}
|
||||
|
||||
return insertedView.id;
|
||||
await this.createTasksAssignedToMeViewGroups(workspaceId, insertedView.id);
|
||||
}
|
||||
|
||||
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 chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } 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';
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
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 { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.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 = {
|
||||
richTextFieldsWithObjectMetadata: RichTextFieldsWithObjectMetadata[];
|
||||
workspaceId: string;
|
||||
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||
};
|
||||
|
||||
type RichTextFieldsWithObjectMetadata = {
|
||||
@ -30,25 +31,16 @@ type RichTextFieldsWithObjectMetadata = {
|
||||
objectMetadata: ObjectMetadataEntity | null;
|
||||
};
|
||||
|
||||
type ProcessWorkspaceArgs = {
|
||||
workspaceId: string;
|
||||
index: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
type ProcessRichTextFieldsArgs = {
|
||||
richTextFields: FieldMetadataEntity[];
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
@MigrationCommand({
|
||||
name: 'migrate-rich-text-content-patch',
|
||||
@Command({
|
||||
name: 'upgrade:0-43:migrate-rich-text-content-patch',
|
||||
description: 'Migrate RICH_TEXT content from v1 to v2',
|
||||
version: '0.43',
|
||||
})
|
||||
export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigrationCommandRunner<MaintainedWorkspacesMigrationCommandOptions> {
|
||||
private options: MaintainedWorkspacesMigrationCommandOptions;
|
||||
|
||||
export class MigrateRichTextContentPatchCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@ -64,55 +56,24 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
async runMigrationCommandOnMaintainedWorkspaces(
|
||||
_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({
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
options,
|
||||
workspaceId,
|
||||
}: ProcessWorkspaceArgs): Promise<void> {
|
||||
try {
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
`Running MigrateRichTextContentPatchCommand for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
|
||||
if (await this.hasRichTextV2FeatureFlag(workspaceId)) {
|
||||
throw new Error(
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
'Rich text v2 feature flag is enabled, skipping migration',
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const richTextFields = await this.fieldMetadataRepository.find({
|
||||
@ -141,14 +102,12 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
||||
await this.migrateToNewRichTextFieldsColumn({
|
||||
richTextFieldsWithObjectMetadata,
|
||||
workspaceId,
|
||||
options,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Command completed for workspace ${workspaceId}`),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.log(chalk.red(`Error in workspace ${workspaceId}: ${error}`));
|
||||
}
|
||||
}
|
||||
|
||||
private async hasRichTextV2FeatureFlag(
|
||||
@ -179,7 +138,7 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
||||
});
|
||||
|
||||
if (objectMetadata === null) {
|
||||
this.logger.warn(
|
||||
this.logger.log(
|
||||
`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)) {
|
||||
this.logger.warn(
|
||||
this.logger.log(
|
||||
`blocknoteFieldValue is defined and is not an array got ${blocknoteFieldValue}`,
|
||||
);
|
||||
|
||||
@ -239,7 +198,7 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
||||
jsonParsedblocknoteFieldValue,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
this.logger.log(
|
||||
`Error converting blocknote to markdown for ${blocknoteFieldValue}`,
|
||||
);
|
||||
}
|
||||
@ -250,6 +209,7 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
||||
private async migrateToNewRichTextFieldsColumn({
|
||||
richTextFieldsWithObjectMetadata,
|
||||
workspaceId,
|
||||
options,
|
||||
}: MigrateRichTextContentArgs) {
|
||||
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
|
||||
|
||||
@ -285,11 +245,19 @@ export class MigrateRichTextContentPatchCommand extends MaintainedWorkspacesMigr
|
||||
serverBlockNoteEditor,
|
||||
});
|
||||
|
||||
if (!this.options.dryRun) {
|
||||
if (!options.dryRun) {
|
||||
try {
|
||||
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],
|
||||
);
|
||||
} 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 chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
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';
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
RunOnWorkspaceArgs,
|
||||
} 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 { Workspace } from 'src/engine/core-modules/workspace/workspace.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_TASKS } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
|
||||
@MigrationCommand({
|
||||
name: 'migrate-search-vector-on-note-and-task-entities',
|
||||
@Command({
|
||||
name: 'upgrade:0-43: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(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@ -39,28 +37,12 @@ export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends MaintainedW
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
async runMigrationCommandOnMaintainedWorkspaces(
|
||||
_passedParam: string[],
|
||||
options: MaintainedWorkspacesMigrationCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
'Running command to migrate search vector on note and task entities',
|
||||
);
|
||||
|
||||
for (const [index, workspaceId] of workspaceIds.entries()) {
|
||||
await this.processWorkspace(workspaceId, index, workspaceIds.length);
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green('Command completed!'));
|
||||
}
|
||||
|
||||
async processWorkspace(
|
||||
workspaceId: string,
|
||||
index: number,
|
||||
total: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
@ -74,11 +56,13 @@ export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends MaintainedW
|
||||
},
|
||||
});
|
||||
|
||||
if (!options.dryRun) {
|
||||
await this.searchService.updateSearchVector(
|
||||
noteObjectMetadata.id,
|
||||
SEARCH_FIELDS_FOR_NOTES,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
const taskObjectMetadata =
|
||||
await this.objectMetadataRepository.findOneOrFail({
|
||||
@ -89,11 +73,13 @@ export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends MaintainedW
|
||||
},
|
||||
});
|
||||
|
||||
if (!options.dryRun) {
|
||||
await this.searchService.updateSearchVector(
|
||||
taskObjectMetadata.id,
|
||||
SEARCH_FIELDS_FOR_TASKS,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
@ -102,10 +88,9 @@ export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends MaintainedW
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
this.logger.log(
|
||||
chalk.red(`Error in workspace ${workspaceId} - ${error.message}`),
|
||||
`Migrated search vector on note and task entities for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,25 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { BatchMaintainedWorkspacesMigrationCommandRunner } from 'src/database/commands/migration-command/batch-maintained-workspaces-migration-command.runner';
|
||||
import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator';
|
||||
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';
|
||||
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';
|
||||
|
||||
@MigrationCommand({
|
||||
name: 'update-default-view-record-opening-on-workflow-objects',
|
||||
@Command({
|
||||
name: 'upgrade:0-43:update-default-view-record-opening-on-workflow-objects',
|
||||
description:
|
||||
'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(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@ -28,12 +30,12 @@ export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends Batc
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
async runMigrationCommandOnWorkspace(
|
||||
workspaceId: string,
|
||||
index: number,
|
||||
total: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
);
|
||||
@ -52,27 +54,22 @@ export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends Batc
|
||||
|
||||
if (workflowObjectsMetadata.length === 0) {
|
||||
this.logger.log(
|
||||
chalk.yellow(
|
||||
`No workflow objects found for workspace ${workspaceId}`,
|
||||
),
|
||||
chalk.yellow(`No workflow objects found for workspace ${workspaceId}`),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
await this.updateDefaultViewsRecordOpening(
|
||||
workflowObjectsMetadata.map((metadata) => metadata.id),
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
chalk.green(`Command completed for workspace ${workspaceId}.`),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.log(
|
||||
chalk.red(`Error in workspace ${workspaceId} - ${error.message}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { IsNull, 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';
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@MigrationCommand({
|
||||
name: 'initialize-permissions',
|
||||
@Command({
|
||||
name: 'upgrade:0-44:initialize-permissions',
|
||||
description: 'Initialize permissions',
|
||||
version: '0.44',
|
||||
})
|
||||
export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
||||
private options: MaintainedWorkspacesMigrationCommandOptions;
|
||||
export class InitializePermissionsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@ -36,26 +35,12 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
async runMigrationCommandOnMaintainedWorkspaces(
|
||||
_passedParam: string[],
|
||||
options: MaintainedWorkspacesMigrationCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(chalk.green('Running command to initialize permissions'));
|
||||
|
||||
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> {
|
||||
override async runOnWorkspace({
|
||||
index,
|
||||
total,
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
try {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
@ -71,12 +56,16 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
||||
)?.id;
|
||||
|
||||
if (!isDefined(adminRoleId)) {
|
||||
adminRoleId = await this.createAdminRole({ workspaceId });
|
||||
adminRoleId = await this.createAdminRole({
|
||||
workspaceId,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
await this.assignAdminRole({
|
||||
workspaceId,
|
||||
adminRoleId,
|
||||
options,
|
||||
});
|
||||
|
||||
let memberRoleId: string | undefined;
|
||||
@ -88,17 +77,20 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
||||
if (!isDefined(memberRoleId)) {
|
||||
memberRoleId = await this.createMemberRole({
|
||||
workspaceId,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
await this.setMemberRoleAsDefaultRole({
|
||||
workspaceId,
|
||||
memberRoleId,
|
||||
options,
|
||||
});
|
||||
|
||||
await this.assignMemberRoleToUserWorkspacesWithoutRole({
|
||||
workspaceId,
|
||||
memberRoleId,
|
||||
options,
|
||||
});
|
||||
} catch (error) {
|
||||
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(
|
||||
chalk.green(
|
||||
`Creating admin role ${this.options.dryRun ? '(dry run)' : ''}`,
|
||||
),
|
||||
chalk.green(`Creating admin role ${options.dryRun ? '(dry run)' : ''}`),
|
||||
);
|
||||
|
||||
if (this.options.dryRun) {
|
||||
if (options.dryRun) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -125,14 +121,18 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
||||
return adminRole.id;
|
||||
}
|
||||
|
||||
private async createMemberRole({ workspaceId }: { workspaceId: string }) {
|
||||
private async createMemberRole({
|
||||
workspaceId,
|
||||
options,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||
}) {
|
||||
this.logger.log(
|
||||
chalk.green(
|
||||
`Creating member role ${this.options.dryRun ? '(dry run)' : ''}`,
|
||||
),
|
||||
chalk.green(`Creating member role ${options.dryRun ? '(dry run)' : ''}`),
|
||||
);
|
||||
|
||||
if (this.options.dryRun) {
|
||||
if (options.dryRun) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -146,9 +146,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
||||
private async setMemberRoleAsDefaultRole({
|
||||
workspaceId,
|
||||
memberRoleId,
|
||||
options,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
memberRoleId: string;
|
||||
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||
}) {
|
||||
const workspaceDefaultRole = await this.workspaceRepository.findOne({
|
||||
where: {
|
||||
@ -159,11 +161,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
||||
if (!isDefined(workspaceDefaultRole?.defaultRoleId)) {
|
||||
this.logger.log(
|
||||
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;
|
||||
}
|
||||
|
||||
@ -176,9 +178,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
||||
private async assignAdminRole({
|
||||
workspaceId,
|
||||
adminRoleId,
|
||||
options,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
adminRoleId: string;
|
||||
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||
}) {
|
||||
const oldestUserWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: {
|
||||
@ -201,11 +205,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
||||
|
||||
this.logger.log(
|
||||
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;
|
||||
}
|
||||
|
||||
@ -219,9 +223,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
||||
private async assignMemberRoleToUserWorkspacesWithoutRole({
|
||||
workspaceId,
|
||||
memberRoleId,
|
||||
options,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
memberRoleId: string;
|
||||
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
|
||||
}) {
|
||||
const userWorkspaces = await this.userWorkspaceRepository.find({
|
||||
where: {
|
||||
@ -257,11 +263,11 @@ export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationC
|
||||
|
||||
this.logger.log(
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
MaintainedWorkspacesMigrationCommandOptions,
|
||||
MaintainedWorkspacesMigrationCommandRunner,
|
||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
RunOnWorkspaceArgs,
|
||||
} 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 { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
interface SyncCustomerDataCommandOptions
|
||||
extends MaintainedWorkspacesMigrationCommandOptions {}
|
||||
|
||||
@Command({
|
||||
name: 'billing:sync-customer-data',
|
||||
description: 'Sync customer data from Stripe for all active workspaces',
|
||||
})
|
||||
export class BillingSyncCustomerDataCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
||||
export class BillingSyncCustomerDataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@ -34,39 +31,10 @@ export class BillingSyncCustomerDataCommand extends MaintainedWorkspacesMigratio
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
async runMigrationCommandOnMaintainedWorkspaces(
|
||||
_passedParam: string[],
|
||||
options: SyncCustomerDataCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): 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> {
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const billingCustomer = await this.billingCustomerRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
|
||||
@ -9,7 +9,7 @@ import { Repository } from 'typeorm';
|
||||
import {
|
||||
MigrationCommandOptions,
|
||||
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 { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.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 { 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 { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
@ -62,7 +61,6 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||
providers: [
|
||||
CleanSuspendedWorkspacesJob,
|
||||
EmailSenderJob,
|
||||
DataSeedDemoWorkspaceJob,
|
||||
UpdateSubscriptionQuantityJob,
|
||||
HandleWorkspaceMemberDeletedJob,
|
||||
CleanWorkspaceDeletionWarningUserVarsJob,
|
||||
|
||||
@ -4,7 +4,6 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
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 { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
|
||||
import {
|
||||
@ -25,7 +24,6 @@ export class WorkspaceMetadataCacheService {
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
) {}
|
||||
|
||||
@LogExecutionTime()
|
||||
async recomputeMetadataCache({
|
||||
workspaceId,
|
||||
ignoreLock = false,
|
||||
|
||||
@ -4,13 +4,11 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
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 {
|
||||
WorkspaceMetadataVersionException,
|
||||
WorkspaceMetadataVersionExceptionCode,
|
||||
} 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()
|
||||
export class WorkspaceMetadataVersionService {
|
||||
@ -20,10 +18,8 @@ export class WorkspaceMetadataVersionService {
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
@LogExecutionTime()
|
||||
async incrementMetadataVersion(workspaceId: string): Promise<void> {
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
|
||||
@ -68,10 +68,6 @@ export class WorkspaceDatasourceFactory {
|
||||
const result = await this.cacheManager.execute(
|
||||
cacheKey,
|
||||
async () => {
|
||||
this.logger.log(
|
||||
`Creating workspace data source for workspace ${workspaceId} and metadata version ${cachedWorkspaceMetadataVersion}`,
|
||||
);
|
||||
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
|
||||
workspaceId,
|
||||
|
||||
@ -7,7 +7,7 @@ import { In, Repository } from 'typeorm';
|
||||
import {
|
||||
MigrationCommandOptions,
|
||||
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 { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service';
|
||||
|
||||
|
||||
@ -1,35 +1,28 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command, Option } from 'nest-commander';
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
MaintainedWorkspacesMigrationCommandOptions,
|
||||
MaintainedWorkspacesMigrationCommandRunner,
|
||||
} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner';
|
||||
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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
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 { SyncWorkspaceLoggerService } from './services/sync-workspace-logger.service';
|
||||
|
||||
interface RunWorkspaceMigrationsOptions
|
||||
extends MaintainedWorkspacesMigrationCommandOptions {
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'workspace:sync-metadata',
|
||||
description: 'Sync metadata',
|
||||
})
|
||||
export class SyncWorkspaceMetadataCommand extends MaintainedWorkspacesMigrationCommandRunner {
|
||||
export class SyncWorkspaceMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
|
||||
private readonly workspaceHealthService: WorkspaceHealthService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@ -37,62 +30,16 @@ export class SyncWorkspaceMetadataCommand extends MaintainedWorkspacesMigrationC
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
async runMigrationCommandOnMaintainedWorkspaces(
|
||||
_passedParam: string[],
|
||||
options: RunWorkspaceMigrationsOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(`Attempting to sync ${workspaceIds.length} workspaces.`);
|
||||
|
||||
let count = 1;
|
||||
|
||||
const errorsDuringSync: string[] = [];
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
index,
|
||||
total,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
`Running workspace sync for workspace: ${workspaceId} (${count} out of ${workspaceIds.length})`,
|
||||
);
|
||||
count++;
|
||||
|
||||
if (!options.force) {
|
||||
try {
|
||||
const issues =
|
||||
await this.workspaceHealthService.healthCheck(workspaceId);
|
||||
|
||||
// Security: abort if there are issues.
|
||||
if (issues.length > 0) {
|
||||
if (!options.force) {
|
||||
this.logger.error(
|
||||
`Workspace contains ${issues.length} issues, aborting.`,
|
||||
`Running workspace sync for workspace: ${workspaceId} (${index} out of ${total})`,
|
||||
);
|
||||
|
||||
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,
|
||||
@ -114,32 +61,9 @@ export class SyncWorkspaceMetadataCommand extends MaintainedWorkspacesMigrationC
|
||||
workspaceMigrations,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
errorsDuringSync.push(
|
||||
`Failed to synchronize workspace ${workspaceId}: ${error.message}`,
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Finished synchronizing all active workspaces (${
|
||||
workspaceIds.length
|
||||
} workspaces). ${
|
||||
errorsDuringSync.length > 0
|
||||
? 'Errors during sync:\n' + errorsDuringSync.join('.\n')
|
||||
: ''
|
||||
}`,
|
||||
`Finished synchronizing all active workspaces (${total} workspaces).`,
|
||||
);
|
||||
}
|
||||
|
||||
@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 { 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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-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 { 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 { 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 { 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';
|
||||
@ -34,7 +38,8 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
|
||||
],
|
||||
'metadata',
|
||||
),
|
||||
TypeOrmModule.forFeature([FeatureFlag], 'core'),
|
||||
DataSourceModule,
|
||||
TypeOrmModule.forFeature([Workspace, FeatureFlag], 'core'),
|
||||
WorkspaceMetadataVersionModule,
|
||||
],
|
||||
providers: [
|
||||
@ -47,7 +52,13 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
|
||||
WorkspaceSyncFieldMetadataService,
|
||||
WorkspaceSyncMetadataService,
|
||||
WorkspaceSyncIndexMetadataService,
|
||||
SyncWorkspaceLoggerService,
|
||||
SyncWorkspaceMetadataCommand,
|
||||
],
|
||||
exports: [
|
||||
...workspaceSyncMetadataFactories,
|
||||
WorkspaceSyncMetadataService,
|
||||
SyncWorkspaceMetadataCommand,
|
||||
],
|
||||
exports: [...workspaceSyncMetadataFactories, WorkspaceSyncMetadataService],
|
||||
})
|
||||
export class WorkspaceSyncMetadataModule {}
|
||||
|
||||
@ -32,7 +32,7 @@ Upgrade your Twenty instance to use v0.43.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.43
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
### v0.41.0 to v0.42.0
|
||||
|
||||
Reference in New Issue
Block a user