[REQUIRES_CACHE_FLUSH_FOR_FIELD_AND_OBJECT]FlatFieldMetadata and FlatObjectMetadata required universal (#17557)

# Introduction
In this PR we're migrating both the `field` and `object` metadata to be
using the new `FlatEntityFromV2` that requires all the
`UniversalFlatEntityExtraProperties` to be spread at the flat entity
root.

This means that we have to update all of their flat declaration

This type swap allows to isole a specific entity migration into his own
type scope and avoid to have everything handled at once

```ts
/**
 * Currently under migration but aims to replace FlatEntity afterwards
 */
export type FlatEntityFromV2<
  TEntity,
  TMetadataName extends AllMetadataName | undefined = undefined,
  TInnerFlatEntity extends { __universal?: unknown } = FlatEntityFrom<
    TEntity,
    TMetadataName
  >,
> = Omit<TInnerFlatEntity, '__universal'> & TInnerFlatEntity['__universal'];

```

## Impact
Both object and field:
- Create input transpilation utils
- from entity to flat tools
- mocks

## Note
Removed from the universal extra properties the jsonb properties that do
not contain a serialized

## Next
Next step is to incrementally make the builder and runner expect
`UniversalFlatEntity` for both of these metadata
This way we will be able to fully migrate an entity e2e typesafely
This commit is contained in:
Paul Rastoin
2026-01-29 19:11:19 +01:00
committed by GitHub
parent e91457a47e
commit fbbd8fe967
69 changed files with 1244 additions and 498 deletions
@@ -1,57 +0,0 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type';
export const ALL_JSONB_PROPERTIES_BY_METADATA_NAME = {
fieldMetadata: {
defaultValue: 'defaultValue',
standardOverrides: 'standardOverrides',
options: 'options',
settings: 'settings',
},
objectMetadata: {
standardOverrides: 'standardOverrides',
duplicateCriteria: 'duplicateCriteria',
},
view: {},
viewField: {},
viewGroup: {},
viewFilter: { value: 'value' },
viewFilterGroup: {},
index: {},
role: {},
roleTarget: {},
rowLevelPermissionPredicate: { value: 'value' },
rowLevelPermissionPredicateGroup: {},
logicFunction: {
cronTriggerSettings: 'cronTriggerSettings',
databaseEventTriggerSettings: 'databaseEventTriggerSettings',
httpRouteTriggerSettings: 'httpRouteTriggerSettings',
publishedVersions: 'publishedVersions',
toolInputSchema: 'toolInputSchema',
},
webhook: {},
agent: {
responseFormat: 'responseFormat',
modelConfiguration: 'modelConfiguration',
},
skill: {},
pageLayout: {},
pageLayoutTab: {},
pageLayoutWidget: {
gridPosition: 'gridPosition',
configuration: 'configuration',
conditionalDisplay: 'conditionalDisplay',
},
commandMenuItem: {},
navigationMenuItem: {},
frontComponent: {},
} as const satisfies {
[P in AllMetadataName]: {
[K in ExtractJsonbProperties<MetadataEntity<P>>]: K;
};
};
export type AllJsonbPropertiesForMetadataName<T extends AllMetadataName> =
keyof (typeof ALL_JSONB_PROPERTIES_BY_METADATA_NAME)[T];
@@ -0,0 +1,42 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type';
export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
fieldMetadata: {
settings: 'settings',
},
objectMetadata: {},
view: {},
viewField: {},
viewGroup: {},
viewFilter: {},
viewFilterGroup: {},
index: {},
role: {},
roleTarget: {},
rowLevelPermissionPredicate: {},
rowLevelPermissionPredicateGroup: {},
logicFunction: {},
webhook: {},
agent: {},
skill: {},
pageLayout: {},
pageLayoutTab: {},
pageLayoutWidget: {
configuration: 'configuration',
},
commandMenuItem: {},
navigationMenuItem: {},
frontComponent: {},
} as const satisfies {
[P in AllMetadataName]: Partial<{
[K in ExtractJsonbProperties<MetadataEntity<P>>]: K;
}>;
};
export type AllJsonbPropertiesWithSerializedPropertiesForMetadataName<
T extends AllMetadataName,
> =
keyof (typeof ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME)[T];
@@ -0,0 +1,146 @@
import { type Equal, type Expect } from 'twenty-shared/testing';
import { type SerializedRelation } from 'twenty-shared/types';
import { type ContainsSerializedRelation } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/contains-serialized-relation.type';
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
type EmptyObject = {};
// ContainsSerializedRelation checks for SerializedRelation in object properties
// It recurses into nested objects and arrays, but stops at primitives
// Direct property tests
// eslint-disable-next-line unused-imports/no-unused-vars
type DirectPropertyAssertions = [
// Direct SerializedRelation property
Expect<
Equal<ContainsSerializedRelation<{ targetId: SerializedRelation }>, true>
>,
// Nullable SerializedRelation property
Expect<
Equal<
ContainsSerializedRelation<{ targetId: SerializedRelation | null }>,
true
>
>,
// Optional SerializedRelation property
Expect<
Equal<ContainsSerializedRelation<{ targetId?: SerializedRelation }>, true>
>,
// Array of SerializedRelation
Expect<
Equal<ContainsSerializedRelation<{ ids: SerializedRelation[] }>, true>
>,
// Plain properties only - no SerializedRelation
Expect<
Equal<ContainsSerializedRelation<{ name: string; count: number }>, false>
>,
// Empty object
Expect<Equal<ContainsSerializedRelation<EmptyObject>, false>>,
];
// Nested object tests - should recurse
// eslint-disable-next-line unused-imports/no-unused-vars
type NestedObjectAssertions = [
// Nested object with SerializedRelation
Expect<
Equal<
ContainsSerializedRelation<{
nested: { targetId: SerializedRelation };
}>,
true
>
>,
// Deeply nested SerializedRelation
Expect<
Equal<
ContainsSerializedRelation<{
level1: { level2: { level3: { id: SerializedRelation } } };
}>,
true
>
>,
// Nested object without SerializedRelation
Expect<
Equal<
ContainsSerializedRelation<{
nested: { name: string; count: number };
}>,
false
>
>,
];
// Nested array tests - should recurse
// eslint-disable-next-line unused-imports/no-unused-vars
type NestedArrayAssertions = [
// Array of objects with SerializedRelation
Expect<
Equal<
ContainsSerializedRelation<{
items: { targetId: SerializedRelation }[];
}>,
true
>
>,
// 2D array of SerializedRelation
Expect<
Equal<ContainsSerializedRelation<{ matrix: SerializedRelation[][] }>, true>
>,
// Array of plain objects
Expect<
Equal<
ContainsSerializedRelation<{
items: { name: string }[];
}>,
false
>
>,
// 2D array of strings
Expect<Equal<ContainsSerializedRelation<{ matrix: string[][] }>, false>>,
];
// Primitive types - should return false (not objects)
// eslint-disable-next-line unused-imports/no-unused-vars
type PrimitiveAssertions = [
Expect<Equal<ContainsSerializedRelation<string>, false>>,
Expect<Equal<ContainsSerializedRelation<number>, false>>,
Expect<Equal<ContainsSerializedRelation<boolean>, false>>,
Expect<Equal<ContainsSerializedRelation<null>, false>>,
Expect<Equal<ContainsSerializedRelation<undefined>, false>>,
];
// Real-world tests
// eslint-disable-next-line unused-imports/no-unused-vars
type RealWorldAssertions = [
// Settings with SerializedRelation
Expect<
Equal<
ContainsSerializedRelation<{
relationType?: string;
junctionTargetFieldId?: SerializedRelation;
}>,
true
>
>,
// Workflow config with nested SerializedRelation in array
Expect<
Equal<
ContainsSerializedRelation<{
steps: { assigneeId: SerializedRelation; action: string }[];
}>,
true
>
>,
];
@@ -4,10 +4,8 @@ import {
type HasAllProperties,
} from 'twenty-shared/testing';
import {
type FieldMetadataDefaultOption,
type FieldMetadataType,
type FieldNumberVariant,
type LinkMetadata,
type NullablePartial,
type NumberDataType,
type RelationOnDeleteAction,
@@ -91,8 +89,9 @@ type UniversalFlatTransformationAssertions = [
>,
];
// JSONB properties are now prefixed with 'universal' in UniversalFlatFieldMetadata
type NarrowedTestCase =
UniversalFlatFieldMetadata<FieldMetadataType.RELATION>['settings'];
UniversalFlatFieldMetadata<FieldMetadataType.RELATION>['universalSettings'];
type NarrowedExpectedResult = {
relationType: RelationType;
@@ -107,7 +106,7 @@ type NarrowedExpectedResult = {
type SettingsTestCase = UniversalFlatFieldMetadata<
FieldMetadataType.RELATION | FieldMetadataType.NUMBER | FieldMetadataType.TEXT
>['settings'];
>['universalSettings'];
type SettingsExpectedResult =
| {
@@ -132,39 +131,8 @@ type SettingsExpectedResult =
}
| null;
type DefaultValueTestCase = UniversalFlatFieldMetadata<
| FieldMetadataType.RELATION
| FieldMetadataType.NUMBER
| FieldMetadataType.TEXT
| FieldMetadataType.LINKS
| FieldMetadataType.CURRENCY
>['defaultValue'];
type DefaultValueExpectedResult =
| string
| number
| null
| {
amountMicros: string | null;
currencyCode: string | null;
__JsonbPropertyBrand__?: undefined;
}
| {
primaryLinkLabel: string | null;
primaryLinkUrl: string | null;
secondaryLinks: LinkMetadata[] | null;
__JsonbPropertyBrand__?: undefined;
};
type OptionsTestCase =
UniversalFlatFieldMetadata<FieldMetadataType.RATING>['options'];
type OptionsExpectedResult = FieldMetadataDefaultOption[];
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
Expect<Equal<SettingsTestCase, SettingsExpectedResult>>,
Expect<Equal<NarrowedTestCase, NarrowedExpectedResult>>,
Expect<Equal<DefaultValueTestCase, DefaultValueExpectedResult>>,
Expect<Equal<OptionsTestCase, OptionsExpectedResult>>,
];
@@ -0,0 +1,24 @@
import {
type IsEmptyObject,
type IsNever,
type IsSerializedRelation,
} from 'twenty-shared/types';
type ContainsSerializedRelationInner<T> = T extends unknown
? IsNever<T> extends true
? false
: unknown extends T
? false
: IsSerializedRelation<T> extends true
? true
: T extends readonly (infer U)[]
? ContainsSerializedRelationInner<U>
: T extends object
? IsEmptyObject<T> extends true
? false
: ContainsSerializedRelationInner<T[keyof T]>
: false
: never;
export type ContainsSerializedRelation<T> =
true extends ContainsSerializedRelationInner<T> ? true : false;
@@ -1,12 +1,4 @@
import { type JSONB_PROPERTY_BRAND } from './jsonb-property.type';
export type HasJsonbPropertyBrand<T> =
typeof JSONB_PROPERTY_BRAND extends keyof T ? true : false;
// Distributive check: returns `true` if any member of a union has the brand
type HasJsonbBrandInUnion<T> = T extends unknown
? HasJsonbPropertyBrand<T>
: never;
import { type HasJsonbBrandInUnion } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/has-jsonb-brand-in-union.type';
export type ExtractJsonbProperties<T> = NonNullable<
{
@@ -0,0 +1,5 @@
import { type HasJsonbPropertyBrand } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/has-jsonb-brand.type';
export type HasJsonbBrandInUnion<T> = T extends unknown
? HasJsonbPropertyBrand<T>
: never;
@@ -0,0 +1,4 @@
import { type JSONB_PROPERTY_BRAND } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
export type HasJsonbPropertyBrand<T> =
typeof JSONB_PROPERTY_BRAND extends keyof T ? true : false;
@@ -7,7 +7,8 @@ import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-m
import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type';
import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { type AllJsonbPropertiesForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-by-metadata-name.constant';
import { type AllJsonbPropertiesWithSerializedPropertiesForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant';
import { type ContainsSerializedRelation } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/contains-serialized-relation.type';
import { type FormatRecordSerializedRelationProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type';
export type UniversalSyncableFlatEntity = Omit<
@@ -19,6 +20,7 @@ export type UniversalSyncableFlatEntity = Omit<
export type UniversalFlatEntityExtraProperties<
TEntity extends SyncableEntity,
// Required to be passed for narrowed type
TMetadataName extends
AllMetadataName = FromMetadataEntityToMetadataName<TEntity>,
> = AddSuffixToEntityOneToManyProperties<TEntity, 'universalIdentifiers'> &
@@ -29,8 +31,13 @@ export type UniversalFlatEntityExtraProperties<
> & {
applicationUniversalIdentifier: string;
} & {
[P in AllJsonbPropertiesForMetadataName<TMetadataName> &
keyof TEntity]: FormatRecordSerializedRelationProperties<TEntity[P]>;
[P in AllJsonbPropertiesWithSerializedPropertiesForMetadataName<TMetadataName> &
keyof TEntity &
string as `universal${Capitalize<P>}`]: true extends ContainsSerializedRelation<
NonNullable<TEntity[P]>
>
? FormatRecordSerializedRelationProperties<TEntity[P]>
: null;
};
export type UniversalFlatEntityFrom<
@@ -46,7 +53,7 @@ export type UniversalFlatEntityFrom<
| ExtractEntityRelatedEntityProperties<TEntity>
| Extract<MetadataManyToOneJoinColumn<TMetadataName>, keyof TEntity>
| keyof CastRecordTypeOrmDatePropertiesToString<TEntity>
| AllJsonbPropertiesForMetadataName<TMetadataName>
| AllJsonbPropertiesWithSerializedPropertiesForMetadataName<TMetadataName>
> &
CastRecordTypeOrmDatePropertiesToString<TEntity> &
UniversalFlatEntityExtraProperties<TEntity, TMetadataName>;