[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:
-57
@@ -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];
|
||||
+42
@@ -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];
|
||||
+146
@@ -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
|
||||
>
|
||||
>,
|
||||
];
|
||||
+3
-35
@@ -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>>,
|
||||
];
|
||||
|
||||
+24
@@ -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
-9
@@ -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<
|
||||
{
|
||||
|
||||
+5
@@ -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;
|
||||
+4
@@ -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;
|
||||
+11
-4
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user