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:
Charles Bochet
2025-02-28 19:51:32 +01:00
committed by GitHub
parent 194b5889fe
commit baa3043954
44 changed files with 714 additions and 2212 deletions

View File

@ -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;
startFromWorkspaceId?: string;
workspaceCountLimit?: number;
};
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,
options,
activeWorkspaceIds,
);
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,
workspaceId,
dataSource,
index: index,
total: activeWorkspaceIds.length,
});
} catch (error) {
this.logger.warn(
chalk.red(`Error in workspace ${workspaceId}: ${error.message}`),
);
}
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
workspaceId,
);
}
} catch (error) {
this.logger.error(error);
}
}
protected abstract runMigrationCommandOnMaintainedWorkspaces(
passedParams: string[],
options: Options,
activeWorkspaceIds: string[],
): Promise<void>;
protected abstract runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void>;
}

View File

@ -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>;
}

View File

@ -1 +0,0 @@
export const dataSeedDemoWorkspaceCronPattern = '0 22 * * *'; // Every day at 10pm

View File

@ -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,
},
},
});
}
}

View File

@ -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,
});
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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 {}

View File

@ -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>;
}

View File

@ -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;
}

View File

@ -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) || [];
}

View File

@ -1,5 +0,0 @@
export interface MigrationCommandInterface<
Options extends Record<string, unknown> = Record<string, unknown>,
> {
runMigrationCommand(passedParams: string[], options: Options): Promise<void>;
}

View File

@ -1 +0,0 @@
export const MIGRATION_COMMAND_INJECTION_TOKEN = 'MIGRATION_COMMANDS';

View File

@ -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,
],
};
}
}

View File

@ -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');
override async runOnWorkspace({
index,
total,
workspaceId,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
if (isCommandLogger(this.logger)) {
this.logger.setVerbose(options.verbose ?? false);
}
await this.createTasksAssignedToMeView(workspaceId);
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 viewId = await this.createTasksAssignedToMeView(workspaceId);
await this.createTasksAssignedToMeViewGroups(workspaceId, viewId);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}.`),
);
} catch {
this.logger.log(chalk.red(`Error in workspace ${workspaceId}.`));
}
this.logger.log(
chalk.green(`Command completed for 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(
`"Assigned to Me" view already exists for workspace ${workspaceId}`,
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(

View File

@ -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,
},
);
}
}
}

View File

@ -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,91 +56,58 @@ 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 MigrateRichTextContentPatchCommand for workspace ${workspaceId} ${index + 1}/${total}`,
);
if (await this.hasRichTextV2FeatureFlag(workspaceId)) {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
chalk.yellow(
'Rich text v2 feature flag is enabled, skipping migration',
),
);
if (await this.hasRichTextV2FeatureFlag(workspaceId)) {
throw new Error(
'Rich text v2 feature flag is enabled, skipping migration',
);
}
return;
}
const richTextFields = await this.fieldMetadataRepository.find({
where: {
workspaceId,
type: FieldMetadataType.RICH_TEXT,
},
});
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'),
);
if (!richTextFields.length) {
this.logger.log(
chalk.yellow('No RICH_TEXT fields found in this workspace'),
);
return;
}
return;
}
this.logger.log(`Found ${richTextFields.length} RICH_TEXT fields`);
this.logger.log(`Found ${richTextFields.length} RICH_TEXT fields`);
const richTextFieldsWithObjectMetadata =
await this.getRichTextFieldsWithObjectMetadata({
richTextFields,
workspaceId,
});
await this.migrateToNewRichTextFieldsColumn({
richTextFieldsWithObjectMetadata,
const richTextFieldsWithObjectMetadata =
await this.getRichTextFieldsWithObjectMetadata({
richTextFields,
workspaceId,
});
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}`),
);
} catch (error) {
this.logger.log(chalk.red(`Error in workspace ${workspaceId}: ${error}`));
}
await this.migrateToNewRichTextFieldsColumn({
richTextFieldsWithObjectMetadata,
workspaceId,
options,
});
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}`),
);
}
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) {
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],
);
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}`,
),
);
}
}
}
}

View File

@ -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,73 +37,60 @@ export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends MaintainedW
super(workspaceRepository, twentyORMGlobalManager);
}
async runMigrationCommandOnMaintainedWorkspaces(
_passedParam: string[],
options: MaintainedWorkspacesMigrationCommandOptions,
workspaceIds: string[],
): Promise<void> {
override async runOnWorkspace({
index,
total,
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
'Running command to migrate search vector on note and task entities',
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
for (const [index, workspaceId] of workspaceIds.entries()) {
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 {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
const noteObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
select: ['id'],
where: {
workspaceId,
nameSingular: 'note',
},
});
const noteObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
select: ['id'],
where: {
workspaceId,
nameSingular: 'note',
},
});
if (!options.dryRun) {
await this.searchService.updateSearchVector(
noteObjectMetadata.id,
SEARCH_FIELDS_FOR_NOTES,
workspaceId,
);
}
const taskObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
select: ['id'],
where: {
workspaceId,
nameSingular: 'task',
},
});
const taskObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
select: ['id'],
where: {
workspaceId,
nameSingular: 'task',
},
});
if (!options.dryRun) {
await this.searchService.updateSearchVector(
taskObjectMetadata.id,
SEARCH_FIELDS_FOR_TASKS,
workspaceId,
);
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
} catch (error) {
this.logger.log(
chalk.red(`Error in workspace ${workspaceId} - ${error.message}`),
);
}
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
this.logger.log(
`Migrated search vector on note and task entities for workspace ${workspaceId}`,
);
}
}

View File

@ -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,51 +30,46 @@ 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}`,
);
const workflowObjectsMetadata = await this.objectMetadataRepository.find({
select: ['id'],
where: {
workspaceId,
standardId: In([
STANDARD_OBJECT_IDS.workflow,
STANDARD_OBJECT_IDS.workflowVersion,
STANDARD_OBJECT_IDS.workflowRun,
]),
},
});
if (workflowObjectsMetadata.length === 0) {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
chalk.yellow(`No workflow objects found for workspace ${workspaceId}`),
);
const workflowObjectsMetadata = await this.objectMetadataRepository.find({
select: ['id'],
where: {
workspaceId,
standardId: In([
STANDARD_OBJECT_IDS.workflow,
STANDARD_OBJECT_IDS.workflowVersion,
STANDARD_OBJECT_IDS.workflowRun,
]),
},
});
if (workflowObjectsMetadata.length === 0) {
this.logger.log(
chalk.yellow(
`No workflow objects found for workspace ${workspaceId}`,
),
);
return;
}
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}`),
);
}
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}.`),
);
}
private async updateDefaultViewsRecordOpening(

View File

@ -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 {}

View File

@ -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;
}

View File

@ -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,
};
}
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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.`),
);
}
}

View File

@ -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,
);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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],
);
}
}
}
}
}

View File

@ -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,
);
}
}

View File

@ -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 {}

View File

@ -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}`),
);
}
}
}

View File

@ -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 {}

View File

@ -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,
};
}
}

View File

@ -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 {}

View File

@ -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,

View File

@ -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';

View File

@ -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,

View File

@ -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,

View File

@ -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 },

View File

@ -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,

View File

@ -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';

View File

@ -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,109 +30,40 @@ 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.`);
override async runOnWorkspace({
workspaceId,
options,
index,
total,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running workspace sync for workspace: ${workspaceId} (${index} out of ${total})`,
);
let count = 1;
const errorsDuringSync: string[] = [];
for (const workspaceId of workspaceIds) {
this.logger.log(
`Running workspace sync for workspace: ${workspaceId} (${count} out of ${workspaceIds.length})`,
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
);
count++;
if (!options.force) {
try {
const issues =
await this.workspaceHealthService.healthCheck(workspaceId);
const { storage, workspaceMigrations } =
await this.workspaceSyncMetadataService.synchronize(
{
workspaceId,
dataSourceId: dataSourceMetadata.id,
},
{ applyChanges: !options.dryRun },
);
// Security: abort if there are issues.
if (issues.length > 0) {
if (!options.force) {
this.logger.error(
`Workspace contains ${issues.length} issues, aborting.`,
);
this.logger.log(
'If you want to force the migration, use --force flag',
);
this.logger.log(
'Please use `workspace:health` command to check issues and fix them before running this command.',
);
continue;
}
this.logger.warn(
`Workspace contains ${issues.length} issues, sync has been forced.`,
);
}
} catch (error) {
if (!options.force) {
throw error;
}
this.logger.warn(
`Workspace health check failed with error, but sync has been forced.`,
error,
);
}
}
try {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
);
const { storage, workspaceMigrations } =
await this.workspaceSyncMetadataService.synchronize(
{
workspaceId,
dataSourceId: dataSourceMetadata.id,
},
{ applyChanges: !options.dryRun },
);
if (options.dryRun) {
await this.syncWorkspaceLoggerService.saveLogs(
workspaceId,
storage,
workspaceMigrations,
);
}
} catch (error) {
errorsDuringSync.push(
`Failed to synchronize workspace ${workspaceId}: ${error.message}`,
);
continue;
}
if (options.dryRun) {
await this.syncWorkspaceLoggerService.saveLogs(
workspaceId,
storage,
workspaceMigrations,
);
}
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;
}
}

View File

@ -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 {}

View File

@ -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