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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22055?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Paul Rastoin
2026-06-24 10:18:59 +02:00
committed by GitHub
parent d00d26c4a4
commit f98f514640
7 changed files with 7 additions and 320 deletions
@@ -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();
});
});
@@ -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 = (
@@ -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 })
@@ -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 },
@@ -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<string, number>;
columnPropertyNamesByEntityClass?: ReadonlyMap<Function, ReadonlySet<string>>;
}): 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');
@@ -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',
@@ -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<string>
>();
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) {