diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command.ts new file mode 100644 index 0000000000..025b3a71d2 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command.ts @@ -0,0 +1,240 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { isDefined } from 'twenty-shared/utils'; +import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; +import { IsNull, Repository } from 'typeorm'; +import { v4 } from 'uuid'; + +import { + RunOnWorkspaceArgs, + WorkspacesMigrationCommandRunner, +} from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util'; +import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util'; +import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant'; + +type CustomFieldMetadata = { + fieldMetadataEntity: FieldMetadataEntity; + fromStandard: boolean; +}; + +type StandardFieldMetadata = { + fieldMetadataEntity: FieldMetadataEntity; + universalIdentifier: string; +}; + +type AllWarnings = 'unknown_standard_id'; + +type FieldMetadataWarning = { + fieldMetadataEntity: FieldMetadataEntity; + warning: AllWarnings; +}; + +type AllExceptions = 'existing_universal_id_mismatch'; + +type FieldMetadataException = { + fieldMetadataEntity: FieldMetadataEntity; + exception: AllExceptions; +}; + +@Command({ + name: 'upgrade:1-16:identify-field-metadata', + description: 'Identify standard field metadata', +}) +export class IdentifyFieldMetadataCommand extends WorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + @InjectRepository(FieldMetadataEntity) + private readonly fieldMetadataRepository: Repository, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + protected readonly applicationService: ApplicationService, + protected readonly workspaceCacheService: WorkspaceCacheService, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService, [ + WorkspaceActivationStatus.ACTIVE, + WorkspaceActivationStatus.SUSPENDED, + WorkspaceActivationStatus.ONGOING_CREATION, + WorkspaceActivationStatus.PENDING_CREATION, + ]); + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + this.logger.log( + `Running identify standard field metadata for workspace ${workspaceId}`, + ); + + const { twentyStandardFlatApplication, workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const allFieldMetadataEntities = await this.fieldMetadataRepository.find({ + select: { + id: true, + universalIdentifier: true, + applicationId: true, + name: true, + standardId: true, + object: { + nameSingular: true, + }, + isCustom: true, + }, + relations: ['object'], + where: { + workspaceId, + applicationId: IsNull(), + }, + }); + + const customFieldMetadataEntities: CustomFieldMetadata[] = []; + const standardFieldMetadataEntities: StandardFieldMetadata[] = []; + const warnings: FieldMetadataWarning[] = []; + const exceptions: FieldMetadataException[] = []; + + for (const fieldMetadataEntity of allFieldMetadataEntities) { + const isStandardMetadataResult = isStandardMetadata(fieldMetadataEntity); + + if (!isStandardMetadataResult) { + customFieldMetadataEntities.push({ + fieldMetadataEntity, + fromStandard: false, + }); + + continue; + } + + const objectConfig = + STANDARD_OBJECTS[ + fieldMetadataEntity.object + .nameSingular as keyof typeof STANDARD_OBJECTS + ]; + const universalIdentifier = + objectConfig?.fields[ + fieldMetadataEntity.name as keyof typeof objectConfig.fields + ]?.universalIdentifier; + + if (!isDefined(universalIdentifier)) { + warnings.push({ + fieldMetadataEntity, + warning: 'unknown_standard_id', + }); + customFieldMetadataEntities.push({ + fieldMetadataEntity, + fromStandard: true, + }); + continue; + } + + if ( + isDefined(fieldMetadataEntity.universalIdentifier) && + fieldMetadataEntity.universalIdentifier !== universalIdentifier + ) { + exceptions.push({ + fieldMetadataEntity, + exception: 'existing_universal_id_mismatch', + }); + continue; + } + + standardFieldMetadataEntities.push({ + fieldMetadataEntity, + universalIdentifier: + fieldMetadataEntity.universalIdentifier ?? universalIdentifier, + }); + } + + const totalUpdates = + customFieldMetadataEntities.length + standardFieldMetadataEntities.length; + + if (warnings.length > 0) { + this.logger.warn( + `Found ${warnings.length} warning(s) while processing field metadata for workspace ${workspaceId}. These fields will become custom.`, + ); + + for (const { fieldMetadataEntity, warning } of warnings) { + this.logger.warn( + `Warning for field "${fieldMetadataEntity.name}" on object "${fieldMetadataEntity.object.nameSingular}" (id=${fieldMetadataEntity.id} standardId=${fieldMetadataEntity.standardId}): ${warning}`, + ); + } + } + + if (exceptions.length > 0) { + this.logger.error( + `Found ${exceptions.length} exception(s) while processing field metadata for workspace ${workspaceId}. No updates will be applied.`, + ); + + for (const { fieldMetadataEntity, exception } of exceptions) { + this.logger.error( + `Exception for field "${fieldMetadataEntity.name}" on object "${fieldMetadataEntity.object.nameSingular}" (id=${fieldMetadataEntity.id} standardId=${fieldMetadataEntity.standardId}): ${exception}`, + ); + } + + throw new Error( + `Aborting migration for workspace ${workspaceId} due to ${exceptions.length} exception(s). See logs above for details.`, + ); + } + + this.logger.log( + `Successfully validated ${totalUpdates}/${allFieldMetadataEntities.length} field metadata update(s) for workspace ${workspaceId} (${customFieldMetadataEntities.length} custom, ${standardFieldMetadataEntities.length} standard)`, + ); + + if (!options.dryRun) { + const customUpdates = customFieldMetadataEntities.map( + ({ fieldMetadataEntity }) => ({ + id: fieldMetadataEntity.id, + universalIdentifier: fieldMetadataEntity.universalIdentifier ?? v4(), + applicationId: workspaceCustomFlatApplication.id, + }), + ); + + const standardUpdates = standardFieldMetadataEntities.map( + ({ fieldMetadataEntity, universalIdentifier }) => ({ + id: fieldMetadataEntity.id, + universalIdentifier, + applicationId: twentyStandardFlatApplication.id, + }), + ); + + await this.fieldMetadataRepository.save([ + ...customUpdates, + ...standardUpdates, + ]); + + const relatedMetadataNames = + getMetadataRelatedMetadataNames('fieldMetadata'); + const cacheKeysToInvalidate = relatedMetadataNames.map( + getMetadataFlatEntityMapsKey, + ); + + this.logger.log( + `Invalidating caches: ${cacheKeysToInvalidate.join(' ')}`, + ); + await this.workspaceCacheService.invalidateAndRecompute( + workspaceId, + cacheKeysToInvalidate, + ); + + this.logger.log( + `Applied ${totalUpdates} field metadata update(s) for workspace ${workspaceId}`, + ); + } else { + this.logger.log( + `Dry run: would apply ${totalUpdates} field metadata update(s) for workspace ${workspaceId}`, + ); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command.ts new file mode 100644 index 0000000000..04e066b1a5 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command.ts @@ -0,0 +1,71 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { DataSource, Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1767277454048-makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable.util'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; + +@Command({ + name: 'upgrade:1-16:make-field-metadata-universal-identifier-and-application-id-not-nullable-migration', + description: + 'Make universalIdentifier and applicationId columns NOT NULL on fieldMetadata table', +}) +export class MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + private hasRunOnce = false; + + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager, + protected readonly dataSourceService: DataSourceService, + @InjectDataSource() + private readonly coreDataSource: DataSource, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + options, + }: RunOnWorkspaceArgs): Promise { + if (this.hasRunOnce) { + this.logger.warn( + 'Skipping has already been run once MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', + ); + + return; + } + + if (options.dryRun) { + return; + } + + const queryRunner = this.coreDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + await makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries( + queryRunner, + ); + + await queryRunner.commitTransaction(); + this.logger.log( + 'Successfully run MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand', + ); + this.hasRunOnce = true; + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.log( + `Rollbacking MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`, + ); + } finally { + await queryRunner.release(); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts index d9d33db5e3..f173f08a2a 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module.ts @@ -3,6 +3,8 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { BackfillOpportunityOwnerFieldCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-opportunity-owner-field.command'; import { BackfillStandardPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-standard-page-layouts.command'; +import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command'; +import { MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command'; import { UpdateTaskOnDeleteActionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -29,11 +31,15 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace UpdateTaskOnDeleteActionCommand, BackfillOpportunityOwnerFieldCommand, BackfillStandardPageLayoutsCommand, + IdentifyFieldMetadataCommand, + MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, ], exports: [ UpdateTaskOnDeleteActionCommand, BackfillOpportunityOwnerFieldCommand, BackfillStandardPageLayoutsCommand, + IdentifyFieldMetadataCommand, + MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, ], }) export class V1_16_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 6c42a5643f..c27908bc2c 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -24,6 +24,8 @@ import { FixNanPositionValuesInNotesCommand } from 'src/database/commands/upgrad import { MigratePageLayoutWidgetConfigurationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-migrate-page-layout-widget-configuration.command'; import { BackfillOpportunityOwnerFieldCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-opportunity-owner-field.command'; import { BackfillStandardPageLayoutsCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-backfill-standard-page-layouts.command'; +import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command'; +import { MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command'; import { UpdateTaskOnDeleteActionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -67,6 +69,8 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly updateTaskOnDeleteActionCommand: UpdateTaskOnDeleteActionCommand, protected readonly backfillOpportunityOwnerFieldCommand: BackfillOpportunityOwnerFieldCommand, protected readonly backfillStandardPageLayoutsCommand: BackfillStandardPageLayoutsCommand, + protected readonly identifyFieldMetadataCommand: IdentifyFieldMetadataCommand, + protected readonly makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, ) { super( workspaceRepository, @@ -104,6 +108,9 @@ export class UpgradeCommand extends UpgradeCommandRunner { this.updateTaskOnDeleteActionCommand, this.backfillOpportunityOwnerFieldCommand, this.backfillStandardPageLayoutsCommand, + this.identifyFieldMetadataCommand, + this + .makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand, ]; this.allCommands = { diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1767277454048-makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1767277454048-makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable.ts new file mode 100644 index 0000000000..b287f62bbc --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1767277454048-makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable.ts @@ -0,0 +1,64 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +import { makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1767277454048-makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable.util'; + +export class MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable1767277454048 + implements MigrationInterface +{ + name = + 'MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable1767277454048'; + + public async up(queryRunner: QueryRunner): Promise { + const savepointName = + 'sp_make_field_metadata_universal_identifier_and_application_id_not_nullable'; + + try { + await queryRunner.query(`SAVEPOINT ${savepointName}`); + + await makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries( + queryRunner, + ); + + await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`); + } catch (e) { + try { + await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`); + await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`); + } catch (rollbackError) { + // eslint-disable-next-line no-console + console.error( + 'Failed to rollback to savepoint in MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable1767277454048', + rollbackError, + ); + throw rollbackError; + } + + // eslint-disable-next-line no-console + console.error( + 'Swallowing MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable1767277454048 error', + e, + ); + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."fieldMetadata" DROP CONSTRAINT "FK_05453a954e458e3d91f2ff5043f"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_f1c88fdfc3ad8910b17fc1fd73"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."fieldMetadata" ALTER COLUMN "applicationId" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."fieldMetadata" ALTER COLUMN "universalIdentifier" DROP NOT NULL`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_f1c88fdfc3ad8910b17fc1fd73" ON "core"."fieldMetadata" ("workspaceId", "universalIdentifier") `, + ); + await queryRunner.query( + `ALTER TABLE "core"."fieldMetadata" ADD CONSTRAINT "FK_05453a954e458e3d91f2ff5043f" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/utils/1767277454048-makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable.util.ts b/packages/twenty-server/src/database/typeorm/core/migrations/utils/1767277454048-makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable.util.ts new file mode 100644 index 0000000000..a73e71f69d --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/utils/1767277454048-makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable.util.ts @@ -0,0 +1,23 @@ +import { type QueryRunner } from 'typeorm'; + +export const makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries = + async (queryRunner: QueryRunner): Promise => { + await queryRunner.query( + `ALTER TABLE "core"."fieldMetadata" DROP CONSTRAINT "FK_05453a954e458e3d91f2ff5043f"`, + ); + await queryRunner.query( + `DROP INDEX "core"."IDX_f1c88fdfc3ad8910b17fc1fd73"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."fieldMetadata" ALTER COLUMN "universalIdentifier" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."fieldMetadata" ALTER COLUMN "applicationId" SET NOT NULL`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_f1c88fdfc3ad8910b17fc1fd73" ON "core"."fieldMetadata" ("workspaceId", "universalIdentifier") `, + ); + await queryRunner.query( + `ALTER TABLE "core"."fieldMetadata" ADD CONSTRAINT "FK_05453a954e458e3d91f2ff5043f" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + }; diff --git a/packages/twenty-server/src/engine/core-modules/application/application.service.ts b/packages/twenty-server/src/engine/core-modules/application/application.service.ts index 76f26531ab..f858505919 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.service.ts @@ -33,13 +33,17 @@ export class ApplicationService { workspaceId: string; workspace?: never; } - | { workspace: WorkspaceEntity; workspaceId?: never }) { + | { + workspace: WorkspaceEntity; + workspaceId?: never; + }) { const workspace = isDefined(workspaceInput) ? workspaceInput : await this.workspaceRepository.findOne({ where: { id: workspaceId, }, + withDeleted: true, }); if (!isDefined(workspace)) { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts index 7c7abf5670..1177e3701f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts @@ -161,6 +161,7 @@ export class FieldMetadataDTO { @Field() updatedAt: Date; + // TODO prastoin make non nullable once MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand has passed in production @Field(() => UUIDScalarType, { nullable: true }) @Field(() => UUIDScalarType, { nullable: true }) applicationId?: string; } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts index f39808e62c..c2353a5aa8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts @@ -30,7 +30,7 @@ import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permis import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; -import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; +import { SyncableEntityRequired } from 'src/engine/workspace-manager/types/syncable-entity-required.interface'; @Entity('fieldMetadata') @Check( @@ -56,7 +56,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti export class FieldMetadataEntity< TFieldMetadataType extends FieldMetadataType = FieldMetadataType, > - extends SyncableEntity + extends SyncableEntityRequired implements Required { @PrimaryGeneratedColumn('uuid') diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts index d5c7957316..148f386f73 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util.ts @@ -1,4 +1,7 @@ -import { FieldMetadataType } from 'twenty-shared/types'; +import { + FieldMetadataType, + type NonNullableRequired, +} from 'twenty-shared/types'; import { v4 } from 'uuid'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; @@ -11,7 +14,9 @@ import { type BuildDefaultFlatFieldMetadataForCustomObjectArgs = { workspaceId: string; - flatObjectMetadata: Pick; + flatObjectMetadata: NonNullableRequired< + Pick + >; }; export type DefaultFlatFieldForCustomObjectMaps = ReturnType< @@ -57,7 +62,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ relationTargetObjectMetadataId: null, settings: null, morphId: null, - applicationId: applicationId ?? null, + applicationId, }; const nameFieldId = v4(); @@ -94,7 +99,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ relationTargetObjectMetadataId: null, settings: null, morphId: null, - applicationId: applicationId ?? null, + applicationId, }; const createdAtFieldId = v4(); @@ -131,7 +136,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ relationTargetObjectMetadataId: null, settings: null, morphId: null, - applicationId: applicationId ?? null, + applicationId, }; const updatedAtFieldId = v4(); @@ -168,7 +173,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ relationTargetObjectMetadataId: null, settings: null, morphId: null, - applicationId: applicationId ?? null, + applicationId, }; const deletedAtFieldId = v4(); @@ -205,7 +210,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ relationTargetObjectMetadataId: null, settings: null, morphId: null, - applicationId: applicationId ?? null, + applicationId, }; const createdByFieldId = v4(); @@ -241,7 +246,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ relationTargetObjectMetadataId: null, settings: null, morphId: null, - applicationId: applicationId ?? null, + applicationId, }; const updatedByFieldId = v4(); @@ -277,7 +282,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ relationTargetObjectMetadataId: null, settings: null, morphId: null, - applicationId: applicationId ?? null, + applicationId, }; const positionFieldId = v4(); @@ -314,7 +319,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ relationTargetObjectMetadataId: null, settings: null, morphId: null, - applicationId: applicationId ?? null, + applicationId, }; const searchVectorFieldId = v4(); @@ -354,7 +359,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({ generatedType: 'STORED', }, morphId: null, - applicationId: applicationId ?? null, + applicationId, }; return { diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index d318c8059e..262fc9859e 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -161,7 +161,7 @@ describe('WorkspaceEntityManager', () => { relationTargetFieldMetadataId: null, relationTargetObjectMetadataId: null, morphId: null, - applicationId: null, + applicationId: 'application-id', }; const flatObjectMetadataMaps: FlatEntityMaps = { diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts index 43f8972f37..ad10ac832b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts @@ -89,7 +89,7 @@ describe('WorkspaceRepository', () => { morphId: null, standardId: null, standardOverrides: null, - applicationId: null, + applicationId: 'application-id', relationTargetFieldMetadataId: null, relationTargetObjectMetadataId: null, calendarViewIds: [], diff --git a/packages/twenty-server/src/engine/workspace-manager/types/syncable-entity-required.interface.ts b/packages/twenty-server/src/engine/workspace-manager/types/syncable-entity-required.interface.ts new file mode 100644 index 0000000000..9eebe0b9a9 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/types/syncable-entity-required.interface.ts @@ -0,0 +1,22 @@ +import { Column, Index, JoinColumn, ManyToOne, type Relation } from 'typeorm'; + +import type { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity'; + +@Index(['workspaceId', 'universalIdentifier'], { + unique: true, +}) +export abstract class SyncableEntityRequired extends WorkspaceRelatedEntity { + @Column({ nullable: false, type: 'uuid' }) + universalIdentifier: string; + + @Column({ nullable: false, type: 'uuid' }) + applicationId: string; + + @ManyToOne('ApplicationEntity', { + onDelete: 'CASCADE', + nullable: false, + }) + @JoinColumn({ name: 'applicationId' }) + application: Relation; +}