diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/decorators/__tests__/was-removed-in-upgrade.decorator.spec.ts b/packages/twenty-server/src/engine/core-modules/upgrade/decorators/__tests__/was-removed-in-upgrade.decorator.spec.ts new file mode 100644 index 0000000000..205ad59412 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/upgrade/decorators/__tests__/was-removed-in-upgrade.decorator.spec.ts @@ -0,0 +1,42 @@ +import { + WasRemovedInUpgrade, + getWasRemovedInUpgradeClassMetadata, + getWasRemovedInUpgradePropertyMetadata, +} from 'src/engine/core-modules/upgrade/decorators/was-removed-in-upgrade.decorator'; + +describe('WasRemovedInUpgrade', () => { + it('records class-level metadata', () => { + @WasRemovedInUpgrade({ upgradeCommandName: 'upgrade-step-class' }) + class Example {} + + expect(getWasRemovedInUpgradeClassMetadata(Example)).toEqual({ + upgradeCommandName: 'upgrade-step-class', + }); + expect(getWasRemovedInUpgradePropertyMetadata(Example)).toEqual({}); + }); + + it('records property-level metadata keyed by property name', () => { + class Example { + @WasRemovedInUpgrade({ upgradeCommandName: 'upgrade-step-foo' }) + foo!: string; + + @WasRemovedInUpgrade({ upgradeCommandName: 'upgrade-step-bar' }) + bar!: string; + + untouched!: string; + } + + expect(getWasRemovedInUpgradePropertyMetadata(Example)).toEqual({ + foo: { upgradeCommandName: 'upgrade-step-foo' }, + bar: { upgradeCommandName: 'upgrade-step-bar' }, + }); + expect(getWasRemovedInUpgradeClassMetadata(Example)).toBeUndefined(); + }); + + it('returns an empty map for classes with no decorated properties', () => { + class Example {} + + expect(getWasRemovedInUpgradePropertyMetadata(Example)).toEqual({}); + expect(getWasRemovedInUpgradeClassMetadata(Example)).toBeUndefined(); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/decorators/was-removed-in-upgrade.decorator.ts b/packages/twenty-server/src/engine/core-modules/upgrade/decorators/was-removed-in-upgrade.decorator.ts new file mode 100644 index 0000000000..4cf8efa63e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/upgrade/decorators/was-removed-in-upgrade.decorator.ts @@ -0,0 +1,41 @@ +import 'reflect-metadata'; + +import { defineUpgradeMetadataOnClassOrProperty } from 'src/engine/core-modules/upgrade/decorators/upgrade-decorator-metadata.util'; + +export type WasRemovedInUpgradeOptions = { + upgradeCommandName: string; +}; + +export const WAS_REMOVED_IN_UPGRADE_CLASS_METADATA_KEY = + 'WAS_REMOVED_IN_UPGRADE_CLASS'; + +export const WAS_REMOVED_IN_UPGRADE_PROPERTIES_METADATA_KEY = + 'WAS_REMOVED_IN_UPGRADE_PROPERTIES'; + +export type WasRemovedInUpgradePropertyMap = Record< + string, + WasRemovedInUpgradeOptions +>; + +export const WasRemovedInUpgrade = + (options: WasRemovedInUpgradeOptions) => + (target: object, propertyKey?: string | symbol): void => { + defineUpgradeMetadataOnClassOrProperty({ + classMetadataKey: WAS_REMOVED_IN_UPGRADE_CLASS_METADATA_KEY, + propertyMetadataKey: WAS_REMOVED_IN_UPGRADE_PROPERTIES_METADATA_KEY, + value: options, + target, + propertyKey, + }); + }; + +export const getWasRemovedInUpgradeClassMetadata = ( + target: Function, +): WasRemovedInUpgradeOptions | undefined => + Reflect.getMetadata(WAS_REMOVED_IN_UPGRADE_CLASS_METADATA_KEY, target); + +export const getWasRemovedInUpgradePropertyMetadata = ( + target: Function, +): WasRemovedInUpgradePropertyMap => + Reflect.getMetadata(WAS_REMOVED_IN_UPGRADE_PROPERTIES_METADATA_KEY, target) ?? + {}; diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/resolve-entity-shape-at-upgrade-cursor.util.spec.ts b/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/resolve-entity-shape-at-upgrade-cursor.util.spec.ts index 6e969b11b5..9fb1a2bd97 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/resolve-entity-shape-at-upgrade-cursor.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/resolve-entity-shape-at-upgrade-cursor.util.spec.ts @@ -1,4 +1,5 @@ import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator'; +import { WasRemovedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-removed-in-upgrade.decorator'; import { WasRenamedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-renamed-in-upgrade.decorator'; import { resolveEntityShapeAtUpgradeCursor } from 'src/engine/core-modules/upgrade/utils/resolve-entity-shape-at-upgrade-cursor.util'; @@ -6,6 +7,7 @@ const INTRODUCE_CMD = '2.7.0_IntroduceCommand_1800000000000'; const RENAME_CMD = '2.6.0_RenameCommand_1700000000000'; const PROP_INTRODUCE_CMD = '2.7.0_AddColumnCommand_1800000000001'; const PROP_RENAME_CMD = '2.6.0_RenameColumnCommand_1700000000001'; +const PROP_REMOVE_CMD = '2.7.0_DropColumnCommand_1800000000002'; const buildPredicate = (applied: string[]) => { const set = new Set(applied); @@ -167,6 +169,100 @@ describe('resolveEntityShapeAtUpgradeCursor', () => { }); }); + describe('property-level @WasRemovedInUpgrade', () => { + class EntityWithRemovedColumn { + @WasRemovedInUpgrade({ upgradeCommandName: PROP_REMOVE_CMD }) + removedColumn!: string; + + untouchedColumn!: string; + } + + const currentColumns = [ + { propertyName: 'removedColumn', databaseName: 'removedColumn' }, + { propertyName: 'untouchedColumn', databaseName: 'untouchedColumn' }, + ]; + + it('should not hide the column before its removal step applied', () => { + const result = resolveEntityShapeAtUpgradeCursor({ + entityClass: EntityWithRemovedColumn, + currentTableName: 'entityWithRemovedColumn', + currentColumns, + isStepApplied: buildPredicate([]), + }); + + expect(result.hiddenPropertyNames.size).toBe(0); + }); + + it('should hide the column once its removal step applied', () => { + const result = resolveEntityShapeAtUpgradeCursor({ + entityClass: EntityWithRemovedColumn, + currentTableName: 'entityWithRemovedColumn', + currentColumns, + isStepApplied: buildPredicate([PROP_REMOVE_CMD]), + }); + + expect(result.hiddenPropertyNames).toEqual(new Set(['removedColumn'])); + }); + + it('should leave undecorated siblings untouched at every cursor', () => { + for (const applied of [[], [PROP_REMOVE_CMD]] as string[][]) { + const result = resolveEntityShapeAtUpgradeCursor({ + entityClass: EntityWithRemovedColumn, + currentTableName: 'entityWithRemovedColumn', + currentColumns, + isStepApplied: buildPredicate(applied), + }); + + expect(result.hiddenPropertyNames.has('untouchedColumn')).toBe(false); + } + }); + }); + + describe('property-level intro + remove combined', () => { + class EntityWithIntroAndRemove { + @WasIntroducedInUpgrade({ upgradeCommandName: PROP_INTRODUCE_CMD }) + @WasRemovedInUpgrade({ upgradeCommandName: PROP_REMOVE_CMD }) + transientColumn!: string; + } + + const currentColumns = [ + { propertyName: 'transientColumn', databaseName: 'transientColumn' }, + ]; + + it('hides the column before intro applied', () => { + const result = resolveEntityShapeAtUpgradeCursor({ + entityClass: EntityWithIntroAndRemove, + currentTableName: 'entityWithIntroAndRemove', + currentColumns, + isStepApplied: buildPredicate([]), + }); + + expect(result.hiddenPropertyNames).toEqual(new Set(['transientColumn'])); + }); + + it('exposes the column between intro and removal', () => { + const result = resolveEntityShapeAtUpgradeCursor({ + entityClass: EntityWithIntroAndRemove, + currentTableName: 'entityWithIntroAndRemove', + currentColumns, + isStepApplied: buildPredicate([PROP_INTRODUCE_CMD]), + }); + + expect(result.hiddenPropertyNames.size).toBe(0); + }); + + it('hides the column once removal applied', () => { + const result = resolveEntityShapeAtUpgradeCursor({ + entityClass: EntityWithIntroAndRemove, + currentTableName: 'entityWithIntroAndRemove', + currentColumns, + isStepApplied: buildPredicate([PROP_INTRODUCE_CMD, PROP_REMOVE_CMD]), + }); + + expect(result.hiddenPropertyNames).toEqual(new Set(['transientColumn'])); + }); + }); + it('should treat an entity with no decorators as available and unchanged', () => { class Plain {} diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/validate-upgrade-aware-entity-decorators.util.spec.ts b/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/validate-upgrade-aware-entity-decorators.util.spec.ts index d1cd4a1889..1c20e9f730 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/validate-upgrade-aware-entity-decorators.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/validate-upgrade-aware-entity-decorators.util.spec.ts @@ -1,4 +1,5 @@ import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator'; +import { WasRemovedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-removed-in-upgrade.decorator'; import { WasRenamedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-renamed-in-upgrade.decorator'; import { validateUpgradeAwareEntityDecorators } from 'src/engine/core-modules/upgrade/utils/validate-upgrade-aware-entity-decorators.util'; @@ -48,6 +49,72 @@ describe('validateUpgradeAwareEntityDecorators', () => { ]); }); + it('should report an unknown step name on @WasRemovedInUpgrade', () => { + class BrokenRemoved { + @WasRemovedInUpgrade({ upgradeCommandName: UNKNOWN_CMD }) + doomedColumn!: string; + } + + const problems = validateUpgradeAwareEntityDecorators({ + entityClasses: [BrokenRemoved], + stepNameToIndex: buildStepNameToIndex([KNOWN_CMD]), + }); + + expect(problems).toEqual([ + { + kind: 'unknown-step-name', + entityName: 'BrokenRemoved', + decorator: '@WasRemovedInUpgrade', + scope: 'property:doomedColumn', + upgradeCommandName: UNKNOWN_CMD, + }, + ]); + }); + + it('should report when a property is removed before it is introduced', () => { + class BackwardsLifecycle { + @WasIntroducedInUpgrade({ upgradeCommandName: KNOWN_LATER_RENAME_CMD }) + @WasRemovedInUpgrade({ upgradeCommandName: KNOWN_CMD }) + transientColumn!: string; + } + + const problems = validateUpgradeAwareEntityDecorators({ + entityClasses: [BackwardsLifecycle], + stepNameToIndex: buildStepNameToIndex([ + KNOWN_CMD, + KNOWN_LATER_RENAME_CMD, + ]), + }); + + expect(problems).toEqual([ + { + kind: 'removal-before-introduction', + entityName: 'BackwardsLifecycle', + scope: 'property:transientColumn', + introductionUpgradeCommandName: KNOWN_LATER_RENAME_CMD, + removalUpgradeCommandName: KNOWN_CMD, + }, + ]); + }); + + it('accepts a property that is introduced before being removed', () => { + class ProperLifecycle { + @WasIntroducedInUpgrade({ upgradeCommandName: KNOWN_CMD }) + @WasRemovedInUpgrade({ upgradeCommandName: KNOWN_LATER_RENAME_CMD }) + transientColumn!: string; + } + + const problems = validateUpgradeAwareEntityDecorators({ + entityClasses: [ProperLifecycle], + stepNameToIndex: buildStepNameToIndex([ + KNOWN_CMD, + KNOWN_LATER_RENAME_CMD, + ]), + }); + + expect(problems).toEqual([]); + }); + it('should report a rename history that is out of order versus the sequence', () => { @WasRenamedInUpgrade([ { previousName: 'first', upgradeCommandName: KNOWN_LATER_RENAME_CMD }, diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/utils/resolve-entity-shape-at-upgrade-cursor.util.ts b/packages/twenty-server/src/engine/core-modules/upgrade/utils/resolve-entity-shape-at-upgrade-cursor.util.ts index c991c47d0b..ee45fa734b 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/utils/resolve-entity-shape-at-upgrade-cursor.util.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/utils/resolve-entity-shape-at-upgrade-cursor.util.ts @@ -4,6 +4,7 @@ import { getWasIntroducedInUpgradeClassMetadata, getWasIntroducedInUpgradePropertyMetadata, } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator'; +import { getWasRemovedInUpgradePropertyMetadata } from 'src/engine/core-modules/upgrade/decorators/was-removed-in-upgrade.decorator'; import { getWasRenamedInUpgradeClassMetadata, getWasRenamedInUpgradePropertyMetadata, @@ -43,6 +44,8 @@ export const resolveEntityShapeAtUpgradeCursor = ({ const propertyIntroductionMap = getWasIntroducedInUpgradePropertyMetadata(entityClass); + const propertyRemovalMap = + getWasRemovedInUpgradePropertyMetadata(entityClass); const propertyRenameMap = getWasRenamedInUpgradePropertyMetadata(entityClass); const hiddenPropertyNames = new Set(); @@ -59,6 +62,13 @@ export const resolveEntityShapeAtUpgradeCursor = ({ continue; } + const removed = propertyRemovalMap[column.propertyName]; + + if (isDefined(removed) && isStepApplied(removed.upgradeCommandName)) { + hiddenPropertyNames.add(column.propertyName); + continue; + } + const renameHistory = propertyRenameMap[column.propertyName] ?? []; if (renameHistory.length === 0) { diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/utils/validate-upgrade-aware-entity-decorators.util.ts b/packages/twenty-server/src/engine/core-modules/upgrade/utils/validate-upgrade-aware-entity-decorators.util.ts index 25705602c7..4ee9508167 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/utils/validate-upgrade-aware-entity-decorators.util.ts +++ b/packages/twenty-server/src/engine/core-modules/upgrade/utils/validate-upgrade-aware-entity-decorators.util.ts @@ -4,6 +4,10 @@ import { getWasIntroducedInUpgradeClassMetadata, getWasIntroducedInUpgradePropertyMetadata, } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator'; +import { + getWasRemovedInUpgradeClassMetadata, + getWasRemovedInUpgradePropertyMetadata, +} from 'src/engine/core-modules/upgrade/decorators/was-removed-in-upgrade.decorator'; import { getWasRenamedInUpgradeClassMetadata, getWasRenamedInUpgradePropertyMetadata, @@ -14,7 +18,10 @@ export type UpgradeAwareDecoratorReferenceProblem = | { kind: 'unknown-step-name'; entityName: string; - decorator: '@WasIntroducedInUpgrade' | '@WasRenamedInUpgrade'; + decorator: + | '@WasIntroducedInUpgrade' + | '@WasRemovedInUpgrade' + | '@WasRenamedInUpgrade'; scope: 'class' | `property:${string}`; upgradeCommandName: string; } @@ -24,6 +31,13 @@ export type UpgradeAwareDecoratorReferenceProblem = scope: 'class' | `property:${string}`; offendingUpgradeCommandName: string; precedingUpgradeCommandName: string; + } + | { + kind: 'removal-before-introduction'; + entityName: string; + scope: 'class' | `property:${string}`; + introductionUpgradeCommandName: string; + removalUpgradeCommandName: string; }; export const validateUpgradeAwareEntityDecorators = ({ @@ -53,6 +67,30 @@ export const validateUpgradeAwareEntityDecorators = ({ }); } + const classRemoved = getWasRemovedInUpgradeClassMetadata(entityClass); + + if ( + isDefined(classRemoved) && + !stepNameToIndex.has(classRemoved.upgradeCommandName) + ) { + problems.push({ + kind: 'unknown-step-name', + entityName, + decorator: '@WasRemovedInUpgrade', + scope: 'class', + upgradeCommandName: classRemoved.upgradeCommandName, + }); + } + + checkRemovalAfterIntroduction({ + entityName, + scope: 'class', + introduced: classIntroduced, + removed: classRemoved, + stepNameToIndex, + problems, + }); + const classRenameHistory = getWasRenamedInUpgradeClassMetadata(entityClass) ?? []; @@ -79,6 +117,29 @@ export const validateUpgradeAwareEntityDecorators = ({ } } + const propRemovedMap = getWasRemovedInUpgradePropertyMetadata(entityClass); + + for (const [propertyName, options] of Object.entries(propRemovedMap)) { + if (!stepNameToIndex.has(options.upgradeCommandName)) { + problems.push({ + kind: 'unknown-step-name', + entityName, + decorator: '@WasRemovedInUpgrade', + scope: `property:${propertyName}`, + upgradeCommandName: options.upgradeCommandName, + }); + } + + checkRemovalAfterIntroduction({ + entityName, + scope: `property:${propertyName}`, + introduced: propIntroducedMap[propertyName], + removed: options, + stepNameToIndex, + problems, + }); + } + const propRenameMap = getWasRenamedInUpgradePropertyMetadata(entityClass); for (const [propertyName, history] of Object.entries(propRenameMap)) { @@ -140,6 +201,43 @@ const checkHistoryForReferenceAndOrder = ({ } }; +const checkRemovalAfterIntroduction = ({ + entityName, + scope, + introduced, + removed, + stepNameToIndex, + problems, +}: { + entityName: string; + scope: 'class' | `property:${string}`; + introduced: { upgradeCommandName: string } | undefined; + removed: { upgradeCommandName: string } | undefined; + stepNameToIndex: ReadonlyMap; + problems: UpgradeAwareDecoratorReferenceProblem[]; +}): void => { + if (!isDefined(introduced) || !isDefined(removed)) { + return; + } + + const introducedIndex = stepNameToIndex.get(introduced.upgradeCommandName); + const removedIndex = stepNameToIndex.get(removed.upgradeCommandName); + + if (!isDefined(introducedIndex) || !isDefined(removedIndex)) { + return; + } + + if (removedIndex <= introducedIndex) { + problems.push({ + kind: 'removal-before-introduction', + entityName, + scope, + introductionUpgradeCommandName: introduced.upgradeCommandName, + removalUpgradeCommandName: removed.upgradeCommandName, + }); + } +}; + export const formatUpgradeAwareDecoratorReferenceProblems = ( problems: UpgradeAwareDecoratorReferenceProblem[], ): string => @@ -149,6 +247,10 @@ export const formatUpgradeAwareDecoratorReferenceProblems = ( return ` - ${problem.entityName} ${problem.decorator} (${problem.scope}): unknown upgradeCommandName "${problem.upgradeCommandName}"`; } - return ` - ${problem.entityName} @WasRenamedInUpgrade (${problem.scope}): "${problem.offendingUpgradeCommandName}" must come after "${problem.precedingUpgradeCommandName}" in the upgrade sequence`; + if (problem.kind === 'rename-history-out-of-order') { + return ` - ${problem.entityName} @WasRenamedInUpgrade (${problem.scope}): "${problem.offendingUpgradeCommandName}" must come after "${problem.precedingUpgradeCommandName}" in the upgrade sequence`; + } + + return ` - ${problem.entityName} @WasRemovedInUpgrade (${problem.scope}): removal step "${problem.removalUpgradeCommandName}" must come after introduction step "${problem.introductionUpgradeCommandName}" in the upgrade sequence`; }) .join('\n'); diff --git a/packages/twenty-server/src/engine/twenty-orm/upgrade-aware/__tests__/upgrade-aware-entity-metadata.adapter.spec.ts b/packages/twenty-server/src/engine/twenty-orm/upgrade-aware/__tests__/upgrade-aware-entity-metadata.adapter.spec.ts index 2200076f50..c99fc4e321 100644 --- a/packages/twenty-server/src/engine/twenty-orm/upgrade-aware/__tests__/upgrade-aware-entity-metadata.adapter.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/upgrade-aware/__tests__/upgrade-aware-entity-metadata.adapter.spec.ts @@ -4,20 +4,44 @@ import { Test } from '@nestjs/testing'; import { getDataSourceToken } from '@nestjs/typeorm'; import { type DataSource } from 'typeorm'; +import { type ColumnMetadata } from 'typeorm/metadata/ColumnMetadata'; import { type EntityMetadata } from 'typeorm/metadata/EntityMetadata'; +import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator'; +import { WasRemovedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-removed-in-upgrade.decorator'; import { WasRenamedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-renamed-in-upgrade.decorator'; import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service'; import { UpgradeSequenceReaderService } from 'src/engine/core-modules/upgrade/services/upgrade-sequence-reader.service'; import { UpgradeAwareEntityMetadataAdapter } from 'src/engine/twenty-orm/upgrade-aware/upgrade-aware-entity-metadata.adapter'; const RENAME_STEP = '2.6.0_Rename_1700000000000'; +const INTRODUCE_STEP = '2.7.0_AddColumn_1800000000000'; +const REMOVE_STEP = '2.7.0_DropColumn_1800000000001'; @WasRenamedInUpgrade([ { previousName: 'oldEntity', upgradeCommandName: RENAME_STEP }, ]) class RenamedEntity {} +class EntityWithHideableColumns { + @WasIntroducedInUpgrade({ upgradeCommandName: INTRODUCE_STEP }) + introducedColumn!: string; + + @WasRemovedInUpgrade({ upgradeCommandName: REMOVE_STEP }) + removedColumn!: string; + + visibleColumn!: string; +} + +const buildColumn = (propertyName: string): ColumnMetadata => + ({ + propertyName, + databaseName: propertyName, + isSelect: true, + isInsert: true, + isUpdate: true, + }) as unknown as ColumnMetadata; + describe('UpgradeAwareEntityMetadataAdapter', () => { it('rewrites tableName / tablePath / givenTableName when the rename step is not yet applied', async () => { const metadata = { @@ -64,4 +88,68 @@ describe('UpgradeAwareEntityMetadataAdapter', () => { expect(metadata.tablePath).toBe('core.oldEntity'); expect(metadata.givenTableName).toBe('oldEntity'); }); + + it('disables isSelect, isInsert and isUpdate for hidden columns (intro pending + removal applied) while leaving the visible sibling untouched', async () => { + const introducedColumn = buildColumn('introducedColumn'); + const removedColumn = buildColumn('removedColumn'); + const visibleColumn = buildColumn('visibleColumn'); + + const metadata = { + target: EntityWithHideableColumns, + tableName: 'entityWithHideableColumns', + tablePath: 'core.entityWithHideableColumns', + givenTableName: 'entityWithHideableColumns', + schema: 'core', + columns: [introducedColumn, removedColumn, visibleColumn], + } as unknown as EntityMetadata; + + const dataSource = { + entityMetadatas: [metadata], + } as unknown as DataSource; + + const moduleRef = await Test.createTestingModule({ + providers: [ + UpgradeAwareEntityMetadataAdapter, + { + provide: UpgradeMigrationService, + useValue: { + getLastAttemptedInstanceCommand: jest.fn().mockResolvedValue({ + name: REMOVE_STEP, + status: 'completed', + }), + }, + }, + { + provide: UpgradeSequenceReaderService, + useValue: { + getUpgradeSequence: jest + .fn() + .mockReturnValue([ + { name: REMOVE_STEP }, + { name: INTRODUCE_STEP }, + ]), + }, + }, + { provide: getDataSourceToken(), useValue: dataSource }, + ], + }).compile(); + + const adapter = moduleRef.get(UpgradeAwareEntityMetadataAdapter); + + await adapter.onModuleInit(); + + await adapter.refresh(); + + expect(introducedColumn.isSelect).toBe(false); + expect(introducedColumn.isInsert).toBe(false); + expect(introducedColumn.isUpdate).toBe(false); + + expect(removedColumn.isSelect).toBe(false); + expect(removedColumn.isInsert).toBe(false); + expect(removedColumn.isUpdate).toBe(false); + + expect(visibleColumn.isSelect).toBe(true); + expect(visibleColumn.isInsert).toBe(true); + expect(visibleColumn.isUpdate).toBe(true); + }); }); diff --git a/packages/twenty-server/src/engine/twenty-orm/upgrade-aware/upgrade-aware-entity-metadata.adapter.ts b/packages/twenty-server/src/engine/twenty-orm/upgrade-aware/upgrade-aware-entity-metadata.adapter.ts index 3ecd73baed..fc2b6ef13f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/upgrade-aware/upgrade-aware-entity-metadata.adapter.ts +++ b/packages/twenty-server/src/engine/twenty-orm/upgrade-aware/upgrade-aware-entity-metadata.adapter.ts @@ -25,6 +25,8 @@ type EntityMetadataSnapshot = { givenTableName: string | undefined; columnDatabaseNamesByPropertyName: ReadonlyMap; columnSelectByPropertyName: ReadonlyMap; + columnInsertByPropertyName: ReadonlyMap; + columnUpdateByPropertyName: ReadonlyMap; }; @Injectable() @@ -116,6 +118,8 @@ export class UpgradeAwareEntityMetadataAdapter implements OnModuleInit { for (const metadata of this.coreDataSource.entityMetadatas) { const columnDatabaseNamesByPropertyName = new Map(); const columnSelectByPropertyName = new Map(); + const columnInsertByPropertyName = new Map(); + const columnUpdateByPropertyName = new Map(); for (const column of metadata.columns) { columnDatabaseNamesByPropertyName.set( @@ -123,6 +127,8 @@ export class UpgradeAwareEntityMetadataAdapter implements OnModuleInit { column.databaseName, ); columnSelectByPropertyName.set(column.propertyName, column.isSelect); + columnInsertByPropertyName.set(column.propertyName, column.isInsert); + columnUpdateByPropertyName.set(column.propertyName, column.isUpdate); } this.snapshotByMetadata.set(metadata, { @@ -131,6 +137,8 @@ export class UpgradeAwareEntityMetadataAdapter implements OnModuleInit { givenTableName: metadata.givenTableName, columnDatabaseNamesByPropertyName, columnSelectByPropertyName, + columnInsertByPropertyName, + columnUpdateByPropertyName, }); } } @@ -297,12 +305,18 @@ export class UpgradeAwareEntityMetadataAdapter implements OnModuleInit { column.databaseName = remappedName ?? canonicalName; + const isHidden = resolved.hiddenPropertyNames.has(column.propertyName); + const canonicalIsSelect = snapshot.columnSelectByPropertyName.get(column.propertyName) ?? true; + const canonicalIsInsert = + snapshot.columnInsertByPropertyName.get(column.propertyName) ?? true; + const canonicalIsUpdate = + snapshot.columnUpdateByPropertyName.get(column.propertyName) ?? true; - column.isSelect = resolved.hiddenPropertyNames.has(column.propertyName) - ? false - : canonicalIsSelect; + column.isSelect = isHidden ? false : canonicalIsSelect; + column.isInsert = isHidden ? false : canonicalIsInsert; + column.isUpdate = isHidden ? false : canonicalIsUpdate; } private computeTablePath({