From f98f514640e30ded5023c701656d791f1cbf96bd Mon Sep 17 00:00:00 2001
From: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
Date: Wed, 24 Jun 2026 10:18:59 +0200
Subject: [PATCH] Introduce search field metadata in 2 16 (#22055)
# Introduction
The devpx wasn't prepare for an already existing entity becoming a
syncable entity
Though the search field metadata entity was dormant anw
So considering it has been introduced starting from 2.16 is the quickest
and easiest tradeoff we can get
This PR is also reverting this one
https://github.com/twentyhq/twenty/pull/22039 that was introducing a new
way to decorate an entity at class level. But it did not fixed the issue
---
...as-introduced-in-upgrade.decorator.spec.ts | 83 ------------------
.../was-introduced-in-upgrade.decorator.ts | 56 +-----------
...ntity-shape-at-upgrade-cursor.util.spec.ts | 52 -----------
...grade-aware-entity-decorators.util.spec.ts | 86 -------------------
...te-upgrade-aware-entity-decorators.util.ts | 26 ------
.../search-field-metadata.entity.ts | 1 -
.../upgrade-aware-entity-metadata.adapter.ts | 23 +----
7 files changed, 7 insertions(+), 320 deletions(-)
delete mode 100644 packages/twenty-server/src/engine/core-modules/upgrade/decorators/__tests__/was-introduced-in-upgrade.decorator.spec.ts
diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/decorators/__tests__/was-introduced-in-upgrade.decorator.spec.ts b/packages/twenty-server/src/engine/core-modules/upgrade/decorators/__tests__/was-introduced-in-upgrade.decorator.spec.ts
deleted file mode 100644
index ac2fcc859c..0000000000
--- a/packages/twenty-server/src/engine/core-modules/upgrade/decorators/__tests__/was-introduced-in-upgrade.decorator.spec.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import {
- WasIntroducedInUpgrade,
- getWasIntroducedInUpgradeClassMetadata,
- getWasIntroducedInUpgradePropertyMetadata,
-} from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
-
-describe('WasIntroducedInUpgrade', () => {
- it('records class-level entity introduction metadata', () => {
- @WasIntroducedInUpgrade({ upgradeCommandName: 'upgrade-step-class' })
- class Example {}
-
- expect(getWasIntroducedInUpgradeClassMetadata(Example)).toEqual({
- upgradeCommandName: 'upgrade-step-class',
- });
- expect(getWasIntroducedInUpgradePropertyMetadata(Example)).toEqual({});
- });
-
- it('records property-level metadata keyed by property name', () => {
- class Example {
- @WasIntroducedInUpgrade({ upgradeCommandName: 'upgrade-step-foo' })
- foo!: string;
-
- @WasIntroducedInUpgrade({ upgradeCommandName: 'upgrade-step-bar' })
- bar!: string;
-
- untouched!: string;
- }
-
- expect(getWasIntroducedInUpgradePropertyMetadata(Example)).toEqual({
- foo: { upgradeCommandName: 'upgrade-step-foo' },
- bar: { upgradeCommandName: 'upgrade-step-bar' },
- });
- expect(getWasIntroducedInUpgradeClassMetadata(Example)).toBeUndefined();
- });
-
- describe('class-level properties form (for inherited columns)', () => {
- it('registers the listed properties in the property-introduction map', () => {
- @WasIntroducedInUpgrade({
- upgradeCommandName: 'upgrade-step-columns',
- properties: ['inheritedA', 'inheritedB'],
- })
- class Example {}
-
- expect(getWasIntroducedInUpgradePropertyMetadata(Example)).toEqual({
- inheritedA: { upgradeCommandName: 'upgrade-step-columns' },
- inheritedB: { upgradeCommandName: 'upgrade-step-columns' },
- });
- });
-
- it('does not mark the entity itself as introduced', () => {
- @WasIntroducedInUpgrade({
- upgradeCommandName: 'upgrade-step-columns',
- properties: ['inheritedA'],
- })
- class Example {}
-
- expect(getWasIntroducedInUpgradeClassMetadata(Example)).toBeUndefined();
- });
-
- it('merges with property-level decorators on the same entity', () => {
- @WasIntroducedInUpgrade({
- upgradeCommandName: 'upgrade-step-inherited',
- properties: ['inheritedA'],
- })
- class Example {
- @WasIntroducedInUpgrade({ upgradeCommandName: 'upgrade-step-local' })
- localColumn!: string;
- }
-
- expect(getWasIntroducedInUpgradePropertyMetadata(Example)).toEqual({
- localColumn: { upgradeCommandName: 'upgrade-step-local' },
- inheritedA: { upgradeCommandName: 'upgrade-step-inherited' },
- });
- });
- });
-
- it('returns an empty map for classes with no decorated properties', () => {
- class Example {}
-
- expect(getWasIntroducedInUpgradePropertyMetadata(Example)).toEqual({});
- expect(getWasIntroducedInUpgradeClassMetadata(Example)).toBeUndefined();
- });
-});
diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator.ts b/packages/twenty-server/src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator.ts
index 58e74fde52..1631391cb9 100644
--- a/packages/twenty-server/src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator.ts
+++ b/packages/twenty-server/src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator.ts
@@ -1,19 +1,9 @@
import 'reflect-metadata';
-import { isDefined } from 'twenty-shared/utils';
-
import { defineUpgradeMetadataOnClassOrProperty } from 'src/engine/core-modules/upgrade/decorators/upgrade-decorator-metadata.util';
export type WasIntroducedInUpgradeOptions = {
upgradeCommandName: string;
- // Class-level escape hatch for columns inherited from a base entity (e.g.
- // SyncableEntity.universalIdentifier) that cannot carry a property decorator
- // in place
- properties?: readonly string[];
-};
-
-export type WasIntroducedInUpgradeClassMetadata = {
- upgradeCommandName: string;
};
export const WAS_INTRODUCED_IN_UPGRADE_CLASS_METADATA_KEY =
@@ -24,64 +14,24 @@ export const WAS_INTRODUCED_IN_UPGRADE_PROPERTIES_METADATA_KEY =
export type WasIntroducedInUpgradePropertyMap = Record<
string,
- WasIntroducedInUpgradeClassMetadata
+ WasIntroducedInUpgradeOptions
>;
export const WasIntroducedInUpgrade =
(options: WasIntroducedInUpgradeOptions) =>
(target: object, propertyKey?: string | symbol): void => {
- const value: WasIntroducedInUpgradeClassMetadata = {
- upgradeCommandName: options.upgradeCommandName,
- };
-
- if (!isDefined(propertyKey) && isDefined(options.properties)) {
- defineIntroducedPropertiesOnClass({
- entityClass: target as Function,
- propertyNames: options.properties,
- value,
- });
-
- return;
- }
-
defineUpgradeMetadataOnClassOrProperty({
classMetadataKey: WAS_INTRODUCED_IN_UPGRADE_CLASS_METADATA_KEY,
propertyMetadataKey: WAS_INTRODUCED_IN_UPGRADE_PROPERTIES_METADATA_KEY,
- value,
+ value: options,
target,
propertyKey,
});
};
-const defineIntroducedPropertiesOnClass = ({
- entityClass,
- propertyNames,
- value,
-}: {
- entityClass: Function;
- propertyNames: readonly string[];
- value: WasIntroducedInUpgradeClassMetadata;
-}): void => {
- const existing: WasIntroducedInUpgradePropertyMap =
- Reflect.getMetadata(
- WAS_INTRODUCED_IN_UPGRADE_PROPERTIES_METADATA_KEY,
- entityClass,
- ) ?? {};
-
- const additions: WasIntroducedInUpgradePropertyMap = Object.fromEntries(
- propertyNames.map((propertyName) => [propertyName, value]),
- );
-
- Reflect.defineMetadata(
- WAS_INTRODUCED_IN_UPGRADE_PROPERTIES_METADATA_KEY,
- { ...existing, ...additions },
- entityClass,
- );
-};
-
export const getWasIntroducedInUpgradeClassMetadata = (
target: Function,
-): WasIntroducedInUpgradeClassMetadata | undefined =>
+): WasIntroducedInUpgradeOptions | undefined =>
Reflect.getMetadata(WAS_INTRODUCED_IN_UPGRADE_CLASS_METADATA_KEY, target);
export const getWasIntroducedInUpgradePropertyMetadata = (
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 c992c65961..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
@@ -169,58 +169,6 @@ describe('resolveEntityShapeAtUpgradeCursor', () => {
});
});
- describe('class-level properties form (inherited columns)', () => {
- @WasIntroducedInUpgrade({
- upgradeCommandName: PROP_INTRODUCE_CMD,
- properties: ['inheritedColumn', 'localColumn'],
- })
- class EntityWithInheritedColumns {}
-
- const currentColumns = [
- { propertyName: 'inheritedColumn', databaseName: 'inheritedColumn' },
- { propertyName: 'localColumn', databaseName: 'localColumn' },
- { propertyName: 'baselineColumn', databaseName: 'baselineColumn' },
- ];
-
- it('hides the listed inherited columns before introduction applied', () => {
- const result = resolveEntityShapeAtUpgradeCursor({
- entityClass: EntityWithInheritedColumns,
- currentTableName: 'entityWithInheritedColumns',
- currentColumns,
- isStepApplied: buildPredicate([]),
- });
-
- expect(result.isAvailable).toBe(true);
- expect(result.hiddenPropertyNames).toEqual(
- new Set(['inheritedColumn', 'localColumn']),
- );
- });
-
- it('exposes the listed columns once introduction applied', () => {
- const result = resolveEntityShapeAtUpgradeCursor({
- entityClass: EntityWithInheritedColumns,
- currentTableName: 'entityWithInheritedColumns',
- currentColumns,
- isStepApplied: buildPredicate([PROP_INTRODUCE_CMD]),
- });
-
- expect(result.hiddenPropertyNames.size).toBe(0);
- });
-
- it('never hides undecorated baseline columns', () => {
- for (const applied of [[], [PROP_INTRODUCE_CMD]] as string[][]) {
- const result = resolveEntityShapeAtUpgradeCursor({
- entityClass: EntityWithInheritedColumns,
- currentTableName: 'entityWithInheritedColumns',
- currentColumns,
- isStepApplied: buildPredicate(applied),
- });
-
- expect(result.hiddenPropertyNames.has('baselineColumn')).toBe(false);
- }
- });
- });
-
describe('property-level @WasRemovedInUpgrade', () => {
class EntityWithRemovedColumn {
@WasRemovedInUpgrade({ upgradeCommandName: PROP_REMOVE_CMD })
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 896b7d1975..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
@@ -115,92 +115,6 @@ describe('validateUpgradeAwareEntityDecorators', () => {
expect(problems).toEqual([]);
});
- describe('column property name validation', () => {
- it('reports an introduced property name that does not exist on the entity', () => {
- @WasIntroducedInUpgrade({
- upgradeCommandName: KNOWN_CMD,
- properties: ['universalIdentifier', 'typoColumn'],
- })
- class EntityWithTypo {}
-
- const problems = validateUpgradeAwareEntityDecorators({
- entityClasses: [EntityWithTypo],
- stepNameToIndex: buildStepNameToIndex([KNOWN_CMD]),
- columnPropertyNamesByEntityClass: new Map([
- [EntityWithTypo, new Set(['universalIdentifier', 'position'])],
- ]),
- });
-
- expect(problems).toEqual([
- {
- kind: 'unknown-property-name',
- entityName: 'EntityWithTypo',
- decorator: '@WasIntroducedInUpgrade',
- propertyName: 'typoColumn',
- },
- ]);
- });
-
- it('reports a relation property name (resolution only acts on columns)', () => {
- @WasIntroducedInUpgrade({
- upgradeCommandName: KNOWN_CMD,
- properties: ['application'],
- })
- class EntityTargetingRelation {}
-
- const problems = validateUpgradeAwareEntityDecorators({
- entityClasses: [EntityTargetingRelation],
- stepNameToIndex: buildStepNameToIndex([KNOWN_CMD]),
- // 'application' is a relation, not a column, so it is not in the set.
- columnPropertyNamesByEntityClass: new Map([
- [EntityTargetingRelation, new Set(['applicationId'])],
- ]),
- });
-
- expect(problems).toEqual([
- {
- kind: 'unknown-property-name',
- entityName: 'EntityTargetingRelation',
- decorator: '@WasIntroducedInUpgrade',
- propertyName: 'application',
- },
- ]);
- });
-
- it('reports no problem when all introduced property names are columns', () => {
- @WasIntroducedInUpgrade({
- upgradeCommandName: KNOWN_CMD,
- properties: ['universalIdentifier', 'position'],
- })
- class ValidEntity {}
-
- const problems = validateUpgradeAwareEntityDecorators({
- entityClasses: [ValidEntity],
- stepNameToIndex: buildStepNameToIndex([KNOWN_CMD]),
- columnPropertyNamesByEntityClass: new Map([
- [ValidEntity, new Set(['universalIdentifier', 'position'])],
- ]),
- });
-
- expect(problems).toEqual([]);
- });
-
- it('skips property name validation when no column property names are provided', () => {
- @WasIntroducedInUpgrade({
- upgradeCommandName: KNOWN_CMD,
- properties: ['anythingGoes'],
- })
- class UnvalidatedEntity {}
-
- const problems = validateUpgradeAwareEntityDecorators({
- entityClasses: [UnvalidatedEntity],
- stepNameToIndex: buildStepNameToIndex([KNOWN_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/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 7fe29d3a0b..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
@@ -38,29 +38,19 @@ export type UpgradeAwareDecoratorReferenceProblem =
scope: 'class' | `property:${string}`;
introductionUpgradeCommandName: string;
removalUpgradeCommandName: string;
- }
- | {
- kind: 'unknown-property-name';
- entityName: string;
- decorator: '@WasIntroducedInUpgrade';
- propertyName: string;
};
export const validateUpgradeAwareEntityDecorators = ({
entityClasses,
stepNameToIndex,
- columnPropertyNamesByEntityClass,
}: {
entityClasses: Function[];
stepNameToIndex: ReadonlyMap;
- columnPropertyNamesByEntityClass?: ReadonlyMap>;
}): UpgradeAwareDecoratorReferenceProblem[] => {
const problems: UpgradeAwareDecoratorReferenceProblem[] = [];
for (const entityClass of entityClasses) {
const entityName = entityClass.name;
- const columnPropertyNames =
- columnPropertyNamesByEntityClass?.get(entityClass);
const classIntroduced = getWasIntroducedInUpgradeClassMetadata(entityClass);
@@ -125,18 +115,6 @@ export const validateUpgradeAwareEntityDecorators = ({
upgradeCommandName: options.upgradeCommandName,
});
}
-
- if (
- isDefined(columnPropertyNames) &&
- !columnPropertyNames.has(propertyName)
- ) {
- problems.push({
- kind: 'unknown-property-name',
- entityName,
- decorator: '@WasIntroducedInUpgrade',
- propertyName,
- });
- }
}
const propRemovedMap = getWasRemovedInUpgradePropertyMetadata(entityClass);
@@ -273,10 +251,6 @@ export const formatUpgradeAwareDecoratorReferenceProblems = (
return ` - ${problem.entityName} @WasRenamedInUpgrade (${problem.scope}): "${problem.offendingUpgradeCommandName}" must come after "${problem.precedingUpgradeCommandName}" in the upgrade sequence`;
}
- if (problem.kind === 'unknown-property-name') {
- return ` - ${problem.entityName} ${problem.decorator} (property:${problem.propertyName}): "${problem.propertyName}" is not a known property on the entity (check the spelling in the properties array)`;
- }
-
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/metadata-modules/search-field-metadata/search-field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity.ts
index ea6a12d814..3145e99fb3 100644
--- a/packages/twenty-server/src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity.ts
@@ -21,7 +21,6 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
@WasIntroducedInUpgrade({
upgradeCommandName:
ADD_UNIVERSAL_IDENTIFIER_AND_APPLICATION_ID_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME,
- properties: ['universalIdentifier', 'applicationId', 'position'],
})
@Unique('IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE', [
'objectMetadataId',
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 ff09bc5a10..88cee8685f 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
@@ -4,8 +4,8 @@ import { InjectDataSource } from '@nestjs/typeorm';
import { type ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
import { type EntityMetadata } from 'typeorm/metadata/EntityMetadata';
-import { isDefined } from 'twenty-shared/utils';
import { DataSource } from 'typeorm';
+import { isDefined } from 'twenty-shared/utils';
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';
@@ -346,28 +346,13 @@ export class UpgradeAwareEntityMetadataAdapter implements OnModuleInit {
}
private validateDecoratorsAgainstSequence(): void {
- const entityClasses: Function[] = [];
- const columnPropertyNamesByEntityClass = new Map<
- Function,
- ReadonlySet
- >();
-
- for (const metadata of this.coreDataSource.entityMetadatas) {
- if (typeof metadata.target !== 'function') {
- continue;
- }
-
- entityClasses.push(metadata.target);
- columnPropertyNamesByEntityClass.set(
- metadata.target,
- new Set(metadata.columns.map((column) => column.propertyName)),
- );
- }
+ const entityClasses = this.coreDataSource.entityMetadatas
+ .map((metadata) => metadata.target)
+ .filter((target): target is Function => typeof target === 'function');
const problems = validateUpgradeAwareEntityDecorators({
entityClasses,
stepNameToIndex: this.stepNameToIndex,
- columnPropertyNamesByEntityClass,
});
if (problems.length === 0) {