Nested serialized relation property (#17490)
## Introduction
Handling nested serializedRelation references mapping
Removed the brand signature omit for the moment
I want to determine if it's really problematic later in the devx
## Motivation
```ts
@ObjectType('RatioAggregateConfig')
export class RatioAggregateConfigDTO {
@Field(() => UUIDScalarType)
@IsUUID()
@IsNotEmpty()
fieldMetadataId: SerializedRelation;
@Field(() => String)
@IsString()
@IsNotEmpty()
optionValue: string;
}
@ObjectType('AggregateChartConfiguration')
export class AggregateChartConfigurationDTO
implements PageLayoutWidgetConfigurationBase
{
// ...
@Field(() => RatioAggregateConfigDTO, { nullable: true })
@ValidateNested()
@Type(() => RatioAggregateConfigDTO)
@IsOptional()
ratioAggregateConfig?: RatioAggregateConfigDTO;
}
```
Blocking https://github.com/twentyhq/twenty/pull/17452
This commit is contained in:
-173
@@ -1,173 +0,0 @@
|
||||
import { type Equal, type Expect } from 'twenty-shared/testing';
|
||||
import { type SerializedRelation } from 'twenty-shared/types';
|
||||
|
||||
import { type FormatJsonbSerializedRelation } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-jsonb-serialized-relation.type';
|
||||
import {
|
||||
type JSONB_PROPERTY_BRAND,
|
||||
type JsonbProperty,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
type BrandedObjectWithRelation = JsonbProperty<{
|
||||
name: string;
|
||||
targetFieldMetadataId: SerializedRelation;
|
||||
}>;
|
||||
|
||||
type BrandedObjectWithoutRelation = JsonbProperty<{
|
||||
name: string;
|
||||
count: number;
|
||||
}>;
|
||||
|
||||
type UnbrandedObject = {
|
||||
name: string;
|
||||
targetFieldMetadataId: SerializedRelation;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type BrandedObjectAssertions = [
|
||||
// Branded object with SerializedRelation: Id suffix renamed to UniversalIdentifier
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithRelation>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
// Branded object without SerializedRelation: no renaming, just removes brand
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithoutRelation>,
|
||||
{
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type UnbrandedObjectAssertions = [
|
||||
// Unbranded objects pass through unchanged
|
||||
Expect<
|
||||
Equal<FormatJsonbSerializedRelation<UnbrandedObject>, UnbrandedObject>
|
||||
>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type PrimitiveAssertions = [
|
||||
// Primitives pass through unchanged
|
||||
Expect<Equal<FormatJsonbSerializedRelation<string>, string>>,
|
||||
Expect<Equal<FormatJsonbSerializedRelation<number>, number>>,
|
||||
Expect<Equal<FormatJsonbSerializedRelation<null>, null>>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type ArrayAssertions = [
|
||||
// Array of branded objects: transforms each element
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithRelation[]>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
}[]
|
||||
>
|
||||
>,
|
||||
|
||||
// Array of unbranded objects: passes through unchanged
|
||||
Expect<
|
||||
Equal<FormatJsonbSerializedRelation<UnbrandedObject[]>, UnbrandedObject[]>
|
||||
>,
|
||||
|
||||
// Array of primitives: passes through unchanged
|
||||
Expect<Equal<FormatJsonbSerializedRelation<string[]>, string[]>>,
|
||||
|
||||
// Nested array of branded objects: transforms innermost elements
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithRelation[][]>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
}[][]
|
||||
>
|
||||
>,
|
||||
|
||||
// Array of unbranded and branded objects union: transforms branded element
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<
|
||||
(BrandedObjectWithRelation | UnbrandedObject)[]
|
||||
>,
|
||||
(
|
||||
| {
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
}
|
||||
| UnbrandedObject
|
||||
)[]
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type UnionAssertions = [
|
||||
// Union with null: transforms branded object, keeps null
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithRelation | null>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
} | null
|
||||
>
|
||||
>,
|
||||
|
||||
// Array of union: transforms elements appropriately
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<(BrandedObjectWithRelation | null)[]>,
|
||||
({
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
} | null)[]
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
type MultipleRelationsObject = JsonbProperty<{
|
||||
name: string;
|
||||
sourceFieldId: SerializedRelation;
|
||||
targetFieldId: SerializedRelation;
|
||||
regularId: string;
|
||||
}>;
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type MultipleRelationsAssertions = [
|
||||
// Multiple SerializedRelation properties: all get renamed
|
||||
Expect<
|
||||
Equal<
|
||||
FormatJsonbSerializedRelation<MultipleRelationsObject>,
|
||||
{
|
||||
name: string;
|
||||
sourceFieldUniversalIdentifier: SerializedRelation;
|
||||
targetFieldUniversalIdentifier: SerializedRelation;
|
||||
regularId: string;
|
||||
}
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
// Verify brand is removed
|
||||
type BrandRemovedCheck =
|
||||
FormatJsonbSerializedRelation<BrandedObjectWithRelation>;
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type BrandRemovedAssertion = Expect<
|
||||
Equal<
|
||||
typeof JSONB_PROPERTY_BRAND extends keyof BrandRemovedCheck ? true : false,
|
||||
false
|
||||
>
|
||||
>;
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
import { type Equal, type Expect } from 'twenty-shared/testing';
|
||||
import { type SerializedRelation } from 'twenty-shared/types';
|
||||
|
||||
import { type FormatRecordSerializedRelationProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type';
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
|
||||
type ObjectWithRelation = {
|
||||
name: string;
|
||||
targetFieldMetadataId: SerializedRelation;
|
||||
};
|
||||
|
||||
type ObjectWithoutRelation = {
|
||||
name: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
type BrandedObjectWithRelation = JsonbProperty<ObjectWithRelation>;
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type ObjectAssertions = [
|
||||
// Object with SerializedRelation: Id suffix renamed to UniversalIdentifier
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<ObjectWithRelation>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
// Branded object with SerializedRelation: renames property, preserves brand key
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<BrandedObjectWithRelation>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
__JsonbPropertyBrand__?: undefined;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
// Object without SerializedRelation: no changes
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<ObjectWithoutRelation>,
|
||||
ObjectWithoutRelation
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type PrimitiveAssertions = [
|
||||
// Primitives pass through unchanged
|
||||
Expect<Equal<FormatRecordSerializedRelationProperties<string>, string>>,
|
||||
Expect<Equal<FormatRecordSerializedRelationProperties<number>, number>>,
|
||||
Expect<Equal<FormatRecordSerializedRelationProperties<null>, null>>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type ArrayAssertions = [
|
||||
// Array of objects with relation: transforms each element
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<ObjectWithRelation[]>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
}[]
|
||||
>
|
||||
>,
|
||||
|
||||
// Array of objects without relation: passes through unchanged
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<ObjectWithoutRelation[]>,
|
||||
ObjectWithoutRelation[]
|
||||
>
|
||||
>,
|
||||
|
||||
// Array of primitives: passes through unchanged
|
||||
Expect<Equal<FormatRecordSerializedRelationProperties<string[]>, string[]>>,
|
||||
|
||||
// Nested array of objects: transforms innermost elements
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<ObjectWithRelation[][]>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
}[][]
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type UnionAssertions = [
|
||||
// Union with null: transforms object, keeps null
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<ObjectWithRelation | null>,
|
||||
{
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
} | null
|
||||
>
|
||||
>,
|
||||
|
||||
// Array of union: transforms elements appropriately
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<(ObjectWithRelation | null)[]>,
|
||||
({
|
||||
name: string;
|
||||
targetFieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
} | null)[]
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
type MultipleRelationsObject = {
|
||||
name: string;
|
||||
sourceFieldId: SerializedRelation;
|
||||
targetFieldId: SerializedRelation;
|
||||
regularId: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type MultipleRelationsAssertions = [
|
||||
// Multiple SerializedRelation properties: all get renamed
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<MultipleRelationsObject>,
|
||||
{
|
||||
name: string;
|
||||
sourceFieldUniversalIdentifier: SerializedRelation;
|
||||
targetFieldUniversalIdentifier: SerializedRelation;
|
||||
regularId: string;
|
||||
}
|
||||
>
|
||||
>,
|
||||
];
|
||||
|
||||
type NestedObjectWithRelation = {
|
||||
name: string;
|
||||
nested: {
|
||||
fieldMetadataId: SerializedRelation;
|
||||
};
|
||||
};
|
||||
|
||||
type DeeplyNestedObjectWithRelation = {
|
||||
name: string;
|
||||
level1: {
|
||||
level2: {
|
||||
targetId: SerializedRelation;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type MixedNestedObject = {
|
||||
name: string;
|
||||
directRelationId: SerializedRelation;
|
||||
nested: {
|
||||
nestedRelationId: SerializedRelation;
|
||||
plainField: string;
|
||||
};
|
||||
};
|
||||
|
||||
type NullableNestedObject = {
|
||||
name: string;
|
||||
nested: {
|
||||
relationId: SerializedRelation;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type NestedWithArrayOfObjects = {
|
||||
name: string;
|
||||
items: {
|
||||
itemRelationId: SerializedRelation;
|
||||
}[];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
type NestedObjectAssertions = [
|
||||
// Simple nested object: transforms relation inside nested object
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<NestedObjectWithRelation>,
|
||||
{
|
||||
name: string;
|
||||
nested: {
|
||||
fieldMetadataUniversalIdentifier: SerializedRelation;
|
||||
};
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
// Deeply nested object: transforms relation at any depth
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<DeeplyNestedObjectWithRelation>,
|
||||
{
|
||||
name: string;
|
||||
level1: {
|
||||
level2: {
|
||||
targetUniversalIdentifier: SerializedRelation;
|
||||
};
|
||||
};
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
// Mixed: transforms both direct and nested relations
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<MixedNestedObject>,
|
||||
{
|
||||
name: string;
|
||||
directRelationUniversalIdentifier: SerializedRelation;
|
||||
nested: {
|
||||
nestedRelationUniversalIdentifier: SerializedRelation;
|
||||
plainField: string;
|
||||
};
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
// Nullable nested object: transforms relation inside, preserves union with null
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<NullableNestedObject>,
|
||||
{
|
||||
name: string;
|
||||
nested: {
|
||||
relationUniversalIdentifier: SerializedRelation;
|
||||
} | null;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
// Nested with array of objects: transforms relations inside array elements
|
||||
Expect<
|
||||
Equal<
|
||||
FormatRecordSerializedRelationProperties<NestedWithArrayOfObjects>,
|
||||
{
|
||||
name: string;
|
||||
items: {
|
||||
itemRelationUniversalIdentifier: SerializedRelation;
|
||||
}[];
|
||||
}
|
||||
>
|
||||
>,
|
||||
];
|
||||
+6
@@ -99,6 +99,7 @@ type NarrowedExpectedResult = {
|
||||
onDelete?: RelationOnDeleteAction | undefined;
|
||||
joinColumnName?: string | null | undefined;
|
||||
junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined;
|
||||
__JsonbPropertyBrand__?: undefined;
|
||||
};
|
||||
|
||||
type SettingsTestCase = UniversalFlatFieldMetadata<
|
||||
@@ -111,14 +112,17 @@ type SettingsExpectedResult =
|
||||
onDelete?: RelationOnDeleteAction | undefined;
|
||||
joinColumnName?: string | null | undefined;
|
||||
junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined;
|
||||
__JsonbPropertyBrand__?: undefined;
|
||||
}
|
||||
| {
|
||||
dataType?: NumberDataType | undefined;
|
||||
decimals?: number | undefined;
|
||||
type?: FieldNumberVariant | undefined;
|
||||
__JsonbPropertyBrand__?: undefined;
|
||||
}
|
||||
| {
|
||||
displayedMaxRows?: number | undefined;
|
||||
__JsonbPropertyBrand__?: undefined;
|
||||
}
|
||||
| null;
|
||||
|
||||
@@ -137,11 +141,13 @@ type DefaultValueExpectedResult =
|
||||
| {
|
||||
amountMicros: string | null;
|
||||
currencyCode: string | null;
|
||||
__JsonbPropertyBrand__?: undefined;
|
||||
}
|
||||
| {
|
||||
primaryLinkLabel: string | null;
|
||||
primaryLinkUrl: string | null;
|
||||
secondaryLinks: LinkMetadata[] | null;
|
||||
__JsonbPropertyBrand__?: undefined;
|
||||
};
|
||||
|
||||
type OptionsTestCase =
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { type ExtractSerializedRelationProperties } from 'twenty-shared/types';
|
||||
|
||||
import { type HasJsonbPropertyBrand } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type';
|
||||
import { type JSONB_PROPERTY_BRAND } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type';
|
||||
|
||||
export type FormatJsonbSerializedRelation<T> = T extends unknown
|
||||
? T extends (infer U)[]
|
||||
? FormatJsonbSerializedRelation<U>[]
|
||||
: HasJsonbPropertyBrand<T> extends true
|
||||
? Omit<
|
||||
{
|
||||
[P in keyof T as P extends ExtractSerializedRelationProperties<T> &
|
||||
string
|
||||
? `${RemoveSuffix<P, 'Id'>}UniversalIdentifier`
|
||||
: P]: T[P];
|
||||
},
|
||||
typeof JSONB_PROPERTY_BRAND
|
||||
>
|
||||
: T
|
||||
: never;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type ExtractSerializedRelationProperties } from 'twenty-shared/types';
|
||||
|
||||
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type';
|
||||
|
||||
export type FormatRecordSerializedRelationProperties<T> = T extends unknown
|
||||
? T extends (infer U)[]
|
||||
? FormatRecordSerializedRelationProperties<U>[]
|
||||
: T extends string
|
||||
? T
|
||||
: T extends object
|
||||
? {
|
||||
[P in keyof T as P extends ExtractSerializedRelationProperties<T> &
|
||||
string
|
||||
? `${RemoveSuffix<P, 'Id'>}UniversalIdentifier`
|
||||
: P]: FormatRecordSerializedRelationProperties<T[P]>;
|
||||
}
|
||||
: T
|
||||
: never;
|
||||
+2
-2
@@ -7,7 +7,7 @@ import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modul
|
||||
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 ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type';
|
||||
import { type FormatJsonbSerializedRelation } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-jsonb-serialized-relation.type';
|
||||
import { type FormatRecordSerializedRelationProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type';
|
||||
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type';
|
||||
|
||||
// TODO Handle universal settings
|
||||
@@ -38,7 +38,7 @@ export type UniversalFlatEntityFrom<
|
||||
} & {
|
||||
applicationUniversalIdentifier: string;
|
||||
} & {
|
||||
[P in ExtractJsonbProperties<TEntity>]: FormatJsonbSerializedRelation<
|
||||
[P in ExtractJsonbProperties<TEntity>]: FormatRecordSerializedRelationProperties<
|
||||
TEntity[P]
|
||||
>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user