diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-21/2-21-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-21/2-21-upgrade-version-command.module.ts new file mode 100644 index 0000000000..ce1e916550 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-21/2-21-upgrade-version-command.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module'; +import { BackfillSystemFieldIsSystemSideEffectCommand } from 'src/database/commands/upgrade-version-command/2-21/2-21-workspace-command-1783925862946-backfill-system-field-is-system-side-effect.command'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; +import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ApplicationEntity, FieldMetadataEntity]), + WorkspaceCacheModule, + WorkspaceIteratorModule, + WorkspaceMigrationRunnerModule, + ], + providers: [BackfillSystemFieldIsSystemSideEffectCommand], +}) +export class V2_21_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-21/2-21-workspace-command-1783925862946-backfill-system-field-is-system-side-effect.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-21/2-21-workspace-command-1783925862946-backfill-system-field-is-system-side-effect.command.ts new file mode 100644 index 0000000000..73b1db68d5 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-21/2-21-workspace-command-1783925862946-backfill-system-field-is-system-side-effect.command.ts @@ -0,0 +1,153 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { getFieldUniversalIdentifier } from 'twenty-shared/application'; +import { isDefined } from 'twenty-shared/utils'; +import { In, Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; +import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; +import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; +import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service'; + +const SYSTEM_FIELD_NAMES = new Set([ + PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.id.name, + PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.createdAt.name, + PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.updatedAt.name, + PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.deletedAt.name, + PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.createdBy.name, + PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.updatedBy.name, + PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.position.name, + PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.searchVector.name, +]); + +@RegisteredWorkspaceCommand('2.21.0', 1783925862946) +@Command({ + name: 'upgrade:2-21:backfill-system-field-is-system-side-effect', + description: + 'Flag existing system fields (id, createdAt, updatedAt, deletedAt, createdBy, updatedBy, position, searchVector) as isSystemSideEffect: true so manifest sync deletion inference excludes them.', +}) +export class BackfillSystemFieldIsSystemSideEffectCommand extends ActiveOrSuspendedWorkspaceCommandRunner { + constructor( + protected readonly workspaceIteratorService: WorkspaceIteratorService, + private readonly workspaceCacheService: WorkspaceCacheService, + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, + @InjectRepository(ApplicationEntity) + private readonly applicationRepository: Repository, + @InjectRepository(FieldMetadataEntity) + private readonly fieldMetadataRepository: Repository, + ) { + super(workspaceIteratorService); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options.dryRun ?? false; + + const applications = await this.applicationRepository.find({ + select: ['id', 'universalIdentifier'], + where: { workspaceId }, + withDeleted: true, + }); + const applicationUniversalIdentifierById = new Map( + applications.map((application) => [ + application.id, + application.universalIdentifier, + ]), + ); + + const { flatFieldMetadataMaps, flatObjectMetadataMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatFieldMetadataMaps', + 'flatObjectMetadataMaps', + ]); + + const fieldMetadataIdsToFlag: string[] = []; + + for (const flatFieldMetadata of Object.values( + flatFieldMetadataMaps.byUniversalIdentifier, + )) { + if ( + !isDefined(flatFieldMetadata) || + !SYSTEM_FIELD_NAMES.has(flatFieldMetadata.name) || + flatFieldMetadata.isSystemSideEffect + ) { + continue; + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: flatFieldMetadata.objectMetadataId, + }); + const applicationUniversalIdentifier = + applicationUniversalIdentifierById.get(flatFieldMetadata.applicationId); + + if ( + !isDefined(flatObjectMetadata) || + !isDefined(applicationUniversalIdentifier) + ) { + this.logger.warn( + `Missing object or application for field ${flatFieldMetadata.name} (${flatFieldMetadata.id}) in workspace ${workspaceId}, skipping`, + ); + continue; + } + + const systemFieldUniversalIdentifier = getFieldUniversalIdentifier({ + applicationUniversalIdentifier, + objectUniversalIdentifier: flatObjectMetadata.universalIdentifier, + name: flatFieldMetadata.name, + }); + + if ( + systemFieldUniversalIdentifier !== flatFieldMetadata.universalIdentifier + ) { + this.logger.warn( + `System field ${flatFieldMetadata.name} (${flatFieldMetadata.id}) does not carry its deterministic universal identifier in workspace ${workspaceId}, skipping`, + ); + + continue; + } + + fieldMetadataIdsToFlag.push(flatFieldMetadata.id); + } + + if (fieldMetadataIdsToFlag.length === 0) { + this.logger.log( + `No system field isSystemSideEffect flag to backfill for workspace ${workspaceId}`, + ); + + return; + } + + this.logger.log( + `${isDryRun ? '[DRY RUN] ' : ''}Flagging ${fieldMetadataIdsToFlag.length} system field(s) as isSystemSideEffect for workspace ${workspaceId}`, + ); + + if (isDryRun) { + return; + } + + await this.fieldMetadataRepository.update( + { id: In(fieldMetadataIdsToFlag), workspaceId }, + { isSystemSideEffect: true }, + ); + + await this.workspaceMigrationRunnerService.invalidateCache({ + allFlatEntityMapsKeys: [getMetadataFlatEntityMapsKey('fieldMetadata')], + workspaceId, + }); + + this.logger.log( + `Flagged ${fieldMetadataIdsToFlag.length} system field(s) as isSystemSideEffect for workspace ${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/workspace-command-provider.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/workspace-command-provider.module.ts index 87e10edd2e..631ab4fe43 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/workspace-command-provider.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/workspace-command-provider.module.ts @@ -21,6 +21,7 @@ import { V2_17_UpgradeVersionCommandModule } from 'src/database/commands/upgrade import { V2_18_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-18/2-18-upgrade-version-command.module'; import { V2_19_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-19/2-19-upgrade-version-command.module'; import { V2_20_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-20/2-20-upgrade-version-command.module'; +import { V2_21_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-21/2-21-upgrade-version-command.module'; @Module({ imports: [ @@ -45,6 +46,7 @@ import { V2_20_UpgradeVersionCommandModule } from 'src/database/commands/upgrade V2_18_UpgradeVersionCommandModule, V2_19_UpgradeVersionCommandModule, V2_20_UpgradeVersionCommandModule, + V2_21_UpgradeVersionCommandModule, ], }) export class WorkspaceCommandProviderModule {}