Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RICH_TEXT_V2 upgrade command #9704

Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import { UpgradeTo0_33CommandModule } from 'src/database/commands/upgrade-versio
import { UpgradeTo0_34CommandModule } from 'src/database/commands/upgrade-version/0-34/0-34-upgrade-version.module';
import { UpgradeTo0_35CommandModule } from 'src/database/commands/upgrade-version/0-35/0-35-upgrade-version.module';
import { UpgradeTo0_40CommandModule } from 'src/database/commands/upgrade-version/0-40/0-40-upgrade-version.module';
import { UpgradeTo0_41CommandModule } from 'src/database/commands/upgrade-version/0-41/0-41-upgrade-version.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
@@ -57,6 +58,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
UpgradeTo0_34CommandModule,
UpgradeTo0_35CommandModule,
UpgradeTo0_40CommandModule,
UpgradeTo0_41CommandModule,
FeatureFlagModule,
],
providers: [
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
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 {
ActiveWorkspacesCommandOptions,
ActiveWorkspacesCommandRunner,
} from 'src/database/commands/active-workspaces.command';
import { isCommandLogger } from 'src/database/commands/logger';
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 { isDefined } from 'src/utils/is-defined';

@Command({
name: 'upgrade-0.41:migrate-rich-text-field',
description: 'Migrate RICH_TEXT fields to new composite structure',
})
export class MigrateRichTextFieldCommand extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository);
}

async executeActiveWorkspacesCommand(
_passedParam: string[],
options: ActiveWorkspacesCommandOptions,
workspaceIds: string[],
): Promise<void> {
this.logger.log(
'Running command to migrate RICH_TEXT fields to new composite structure',
);

if (isCommandLogger(this.logger)) {
this.logger.setVerbose(options.verbose ?? false);
}

let workspaceIterator = 1;

for (const workspaceId of workspaceIds) {
this.logger.log(
`Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`,
);

const richTextFields = await this.fieldMetadataRepository.find({
where: {
workspaceId,
type: FieldMetadataType.RICH_TEXT,
},
});

if (!richTextFields.length) {
this.logger.log('No RICH_TEXT fields found in this workspace');
workspaceIterator++;
continue;
}

this.logger.log(`Found ${richTextFields.length} RICH_TEXT fields`);

for (const richTextField of richTextFields) {
const newRichTextField: Partial<FieldMetadataEntity> = {
...richTextField,
name: `${richTextField.name}V2`,
id: undefined,
type: FieldMetadataType.RICH_TEXT_V2,
defaultValue: null,
};

await this.fieldMetadataRepository.insert(newRichTextField);

const objectMetadata = await this.objectMetadataRepository.findOne({
where: { id: richTextField.objectMetadataId },
});

if (!isDefined(objectMetadata)) {
this.logger.log(
`Object metadata not found for rich text field ${richTextField.name} in workspace ${workspaceId}`,
);
continue;
}

await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(
`migrate-rich-text-field-${objectMetadata.nameSingular}-${richTextField.name}`,
),
workspaceId,
[
{
name: computeObjectTargetTable(objectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${richTextField.name}V2Blocknote`,
columnType: 'text',
isNullable: true,
defaultValue: null,
} satisfies WorkspaceMigrationColumnCreate,
{
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: `${richTextField.name}V2Markdown`,
columnType: 'text',
isNullable: true,
defaultValue: null,
} satisfies WorkspaceMigrationColumnCreate,
],
} satisfies WorkspaceMigrationTableAction,
],
);
}

await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);

await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);

const serverBlockNoteEditor = ServerBlockNoteEditor.create();

for (const richTextField of richTextFields) {
const objectMetadata = await this.objectMetadataRepository.findOne({
where: { id: richTextField.objectMetadataId },
});

if (!isDefined(objectMetadata)) {
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 = blocknoteFieldValue
? await serverBlockNoteEditor.blocksToMarkdownLossy(
JSON.parse(blocknoteFieldValue),
)
: null;
Comment on lines +177 to +183
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: No error handling for JSON.parse() - could throw exception and halt migration if blocknoteFieldValue contains invalid JSON


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

workspaceIterator++;
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}`),
);
}

this.logger.log(chalk.green('Command completed!'));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { InjectRepository } from '@nestjs/typeorm';

import { Command } from 'nest-commander';
import { Repository } from 'typeorm';

import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
import { BaseCommandOptions } from 'src/database/commands/base.command';
import { MigrateRichTextFieldCommand } from 'src/database/commands/upgrade-version/0-41/0-41-migrate-rich-text-field.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';

@Command({
name: 'upgrade-0.41',
description: 'Upgrade to 0.41',
})
export class UpgradeTo0_41Command extends ActiveWorkspacesCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
private readonly migrateRichTextFieldCommand: MigrateRichTextFieldCommand,
) {
super(workspaceRepository);
}

async executeActiveWorkspacesCommand(
passedParam: string[],
options: BaseCommandOptions,
workspaceIds: string[],
): Promise<void> {
Comment on lines +26 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: passedParam is unused in this implementation but passed through to migrateRichTextFieldCommand. Consider documenting the expected parameters or removing if not needed.

this.logger.log('Running command to upgrade to 0.41');

await this.migrateRichTextFieldCommand.executeActiveWorkspacesCommand(
passedParam,
options,
workspaceIds,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { MigrateRichTextFieldCommand } from 'src/database/commands/upgrade-version/0-41/0-41-migrate-rich-text-field.command';
import { UpgradeTo0_41Command } from 'src/database/commands/upgrade-version/0-41/0-41-upgrade-version.command';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
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: [
TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature(
[ObjectMetadataEntity, FieldMetadataEntity],
'metadata',
),
WorkspaceMigrationRunnerModule,
WorkspaceMigrationModule,
WorkspaceMetadataVersionModule,
WorkspaceDataSourceModule,
],
providers: [UpgradeTo0_41Command, MigrateRichTextFieldCommand],
})
export class UpgradeTo0_41CommandModule {}
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import { emailsCompositeType } from 'src/engine/metadata-modules/field-metadata/
import { fullNameCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
import { linksCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
import { phonesCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
import { richTextV2CompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/rich-text-v2.composite-type';

export const compositeTypeDefinitions = new Map<
FieldMetadataType,
@@ -21,4 +22,5 @@ export const compositeTypeDefinitions = new Map<
[FieldMetadataType.ACTOR, actorCompositeType],
[FieldMetadataType.EMAILS, emailsCompositeType],
[FieldMetadataType.PHONES, phonesCompositeType],
[FieldMetadataType.RICH_TEXT_V2, richTextV2CompositeType],
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { FieldMetadataType } from 'twenty-shared';
import { z } from 'zod';

import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';

export const richTextV2CompositeType: CompositeType = {
type: FieldMetadataType.RICH_TEXT_V2,
properties: [
{
name: 'blocknote',
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
},
{
name: 'markdown',
type: FieldMetadataType.TEXT,
hidden: false,
isRequired: false,
},
],
};

export const richTextV2ValueSchema = z.object({
blocknote: z.string().nullable(),
markdown: z.string().nullable(),
});

export type RichTextV2Metadata = z.infer<typeof richTextV2ValueSchema>;
Original file line number Diff line number Diff line change
@@ -9,7 +9,8 @@ export const isCompositeFieldMetadataType = (
| FieldMetadataType.LINKS
| FieldMetadataType.ACTOR
| FieldMetadataType.EMAILS
| FieldMetadataType.PHONES => {
| FieldMetadataType.PHONES
| FieldMetadataType.RICH_TEXT_V2 => {
return [
FieldMetadataType.CURRENCY,
FieldMetadataType.FULL_NAME,
@@ -18,5 +19,6 @@ export const isCompositeFieldMetadataType = (
FieldMetadataType.ACTOR,
FieldMetadataType.EMAILS,
FieldMetadataType.PHONES,
FieldMetadataType.RICH_TEXT_V2,
].includes(type);
};
1 change: 1 addition & 0 deletions packages/twenty-shared/src/types/FieldMetadataType.ts
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ export enum FieldMetadataType {
ADDRESS = 'ADDRESS',
RAW_JSON = 'RAW_JSON',
RICH_TEXT = 'RICH_TEXT',
RICH_TEXT_V2 = 'RICH_TEXT_V2',
ACTOR = 'ACTOR',
ARRAY = 'ARRAY',
TS_VECTOR = 'TS_VECTOR',