Fix standard object computed metadata (#12883)

# Introduction
close https://github.com/twentyhq/twenty/issues/12879

This PR has a global impact all on workspaces
It should be crash tested in local using an anon extract of the db
This commit is contained in:
Paul Rastoin
2025-06-26 13:38:52 +02:00
committed by GitHub
parent 2ca43e18e9
commit 2d774767c0
9 changed files with 136 additions and 42 deletions

View File

@ -0,0 +1,67 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { IsNull, Not, 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 { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
name: 'upgrade:1-1:fix-update-standard-fields-is-label-synced-with-name',
description:
'Fix isLabelSyncedWithName property for standard fields to match actual label-name synchronization state',
})
export class FixUpdateStandardFieldsIsLabelSyncedWithName extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FieldMetadataEntity, 'core')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(`Updating standard fields for workspace ${workspaceId}`);
const workspaceStandardFields = await this.fieldMetadataRepository.find({
where: {
workspaceId,
isCustom: false,
standardId: Not(IsNull()),
},
});
let updatedFields = 0;
for (const field of workspaceStandardFields) {
const isLabelSyncedWithName =
computeMetadataNameFromLabel(field.label) === field.name;
if (field.isLabelSyncedWithName === isLabelSyncedWithName) {
continue;
}
if (!options.dryRun) {
await this.fieldMetadataRepository.update(field.id, {
isLabelSyncedWithName,
});
}
updatedFields++;
this.logger.log(`Updated isLabelSyncedMetadata for field ${field.id}`);
}
this.logger.log(
`Updated ${updatedFields} field.s for workspace ${workspaceId}`,
);
}
}

View File

@ -0,0 +1,35 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.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 { 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,
AppToken,
User,
UserWorkspace,
FieldMetadataEntity,
ObjectMetadataEntity,
],
'core',
),
WorkspaceDataSourceModule,
WorkspaceMigrationRunnerModule,
WorkspaceMetadataVersionModule,
],
providers: [FixUpdateStandardFieldsIsLabelSyncedWithName],
exports: [FixUpdateStandardFieldsIsLabelSyncedWithName],
})
export class V1_1_UpgradeVersionCommandModule {}

View File

@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { V0_54_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-54/0-54-upgrade-version-command.module';
import { V0_55_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-55/0-55-upgrade-version-command.module';
import { V1_1_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-1/1-1-upgrade-version-command.module';
import {
DatabaseMigrationService,
UpgradeCommand,
@ -15,6 +16,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
TypeOrmModule.forFeature([Workspace], 'core'),
V0_54_UpgradeVersionCommandModule,
V0_55_UpgradeVersionCommandModule,
V1_1_UpgradeVersionCommandModule,
WorkspaceSyncMetadataModule,
],
providers: [DatabaseMigrationService, UpgradeCommand],

View File

@ -175,12 +175,18 @@ export class UpgradeCommand extends UpgradeCommandRunner {
beforeSyncMetadata: [],
};
const commands_110: VersionCommands = {
afterSyncMetadata: [],
beforeSyncMetadata: [],
};
this.allCommands = {
'0.53.0': commands_053,
'0.54.0': commands_054,
'0.55.0': commands_055,
'0.60.0': commands_060,
'1.0.0': commands_100,
'1.1.0': commands_110,
};
}

View File

@ -6,7 +6,7 @@ import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect';
@ -19,12 +19,8 @@ export interface WorkspaceFieldOptions<
> {
standardId: string;
type: T;
label:
| MessageDescriptor
| ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor);
description?:
| MessageDescriptor
| ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor);
label: MessageDescriptor;
description?: MessageDescriptor;
icon?: string;
defaultValue?: FieldMetadataDefaultValue<T>;
options?: FieldMetadataOptions<T>;
@ -76,30 +72,18 @@ export function WorkspaceField<T extends FieldMetadataType>(
const defaultValue = (options.defaultValue ??
generateDefaultValue(options.type)) as FieldMetadataDefaultValue | null;
const name = propertyKey.toString();
const label = options.label.message ?? '';
const isLabelSyncedWithName = computeMetadataNameFromLabel(label) === name;
metadataArgsStorage.addFields({
target: object.constructor,
standardId: options.standardId,
name: propertyKey.toString(),
label:
typeof options.label === 'function'
? (objectMetadata: ObjectMetadataEntity) =>
(
options.label as (
obj: ObjectMetadataEntity,
) => MessageDescriptor
)(objectMetadata).message ?? ''
: (options.label.message ?? ''),
name,
label,
type: options.type,
description:
typeof options.description === 'function'
? (objectMetadata: ObjectMetadataEntity) =>
(
options.description as (
obj: ObjectMetadataEntity,
) => MessageDescriptor
)(objectMetadata).message ?? ''
: (options.description?.message ?? ''),
isLabelSyncedWithName,
description: options.description?.message ?? '',
icon: options.icon,
defaultValue,
options: options.options,

View File

@ -5,14 +5,13 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metada
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect';
interface WorkspaceRelationBaseOptions<TClass> {
standardId: string;
label:
| MessageDescriptor
| ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor);
label: MessageDescriptor;
description?:
| MessageDescriptor
| ((objectMetadata: ObjectMetadataEntity) => MessageDescriptor);
@ -64,20 +63,15 @@ export function WorkspaceRelation<TClass extends object>(
object,
propertyKey.toString(),
);
const name = propertyKey.toString();
const label = options.label.message ?? '';
const isLabelSyncedWithName = computeMetadataNameFromLabel(label) === name;
metadataArgsStorage.addRelations({
target: object.constructor,
standardId: options.standardId,
name: propertyKey.toString(),
label:
typeof options.label === 'function'
? (objectMetadata: ObjectMetadataEntity) =>
(
options.label as (
obj: ObjectMetadataEntity,
) => MessageDescriptor
)(objectMetadata).message ?? ''
: (options.label.message ?? ''),
name,
label,
type: options.type,
description:
typeof options.description === 'function'
@ -96,6 +90,7 @@ export function WorkspaceRelation<TClass extends object>(
isNullable,
isSystem,
gate,
isLabelSyncedWithName,
});
};
}

View File

@ -105,4 +105,6 @@ export interface WorkspaceFieldMetadataArgs {
* Is active field.
*/
readonly asExpression?: string;
readonly isLabelSyncedWithName: boolean;
}

View File

@ -84,4 +84,6 @@ export interface WorkspaceRelationMetadataArgs {
* Is active field.
*/
readonly isActive?: boolean;
readonly isLabelSyncedWithName: boolean;
}

View File

@ -121,7 +121,7 @@ export class StandardFieldFactory {
* Create field metadata
*/
private createFieldMetadata(
workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined,
_workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined,
workspaceFieldMetadataArgs: WorkspaceFieldMetadataArgs,
context: WorkspaceSyncContext,
): PartialFieldMetadata[] {
@ -153,7 +153,7 @@ export class StandardFieldFactory {
isActive: workspaceFieldMetadataArgs.isActive ?? true,
asExpression: workspaceFieldMetadataArgs.asExpression,
generatedType: workspaceFieldMetadataArgs.generatedType,
isLabelSyncedWithName: true,
isLabelSyncedWithName: workspaceFieldMetadataArgs.isLabelSyncedWithName,
},
];
}
@ -192,7 +192,8 @@ export class StandardFieldFactory {
isNullable: true,
isUnique: false,
isActive: workspaceRelationMetadataArgs.isActive ?? true,
isLabelSyncedWithName: true,
isLabelSyncedWithName:
workspaceRelationMetadataArgs.isLabelSyncedWithName,
});
return fieldMetadataCollection;