Field metadata v2 relation (#13562)

# Introduction
- deferrable constraint on foreignKey definition
- fix relation creation input transpilation to flat field metadata
- fix validation to allow contextual validation on not already created
field metadata ( usefull for relation, and will be necessary for the
import )
This commit is contained in:
Paul Rastoin
2025-08-04 11:30:13 +02:00
committed by GitHub
parent 810fe06aa3
commit d8ec2e3959
9 changed files with 239 additions and 148 deletions
@@ -24,19 +24,11 @@ type FromCreateFieldInputToFlatObjectMetadata = {
rawCreateFieldInput: CreateFieldInput;
existingFlatObjectMetadatas: FlatObjectMetadata[];
};
export type FlatFieldAndItsFlatObjectMetadata<
T extends FieldMetadataType = FieldMetadataType,
> = {
flatFieldMetadata: FlatFieldMetadata<T>;
parentFlatObjectMetadata: FlatObjectMetadata;
};
export const fromCreateFieldInputToFlatFieldAndItsFlatObjectMetadata = async ({
existingFlatObjectMetadatas,
rawCreateFieldInput,
}: FromCreateFieldInputToFlatObjectMetadata): Promise<
FlatFieldAndItsFlatObjectMetadata[]
> => {
}: FromCreateFieldInputToFlatObjectMetadata): Promise<FlatFieldMetadata[]> => {
if (rawCreateFieldInput.isRemoteCreation) {
throw new FieldMetadataException(
"Remote fields aren't supported",
@@ -86,15 +78,12 @@ export const fromCreateFieldInputToFlatFieldAndItsFlatObjectMetadata = async ({
case FieldMetadataType.RATING: {
return [
{
flatFieldMetadata: {
...commonFlatFieldMetadata,
type: createFieldInput.type,
settings: null,
defaultValue: commonFlatFieldMetadata.defaultValue as string, // Could this be improved ?
options: generateRatingOptions(),
} satisfies FlatFieldMetadata<typeof createFieldInput.type>,
parentFlatObjectMetadata,
},
...commonFlatFieldMetadata,
type: createFieldInput.type,
settings: null,
defaultValue: commonFlatFieldMetadata.defaultValue as string, // Could this be improved ?
options: generateRatingOptions(),
} satisfies FlatFieldMetadata<typeof createFieldInput.type>,
];
}
case FieldMetadataType.SELECT:
@@ -110,15 +99,12 @@ export const fromCreateFieldInputToFlatFieldAndItsFlatObjectMetadata = async ({
return [
{
flatFieldMetadata: {
...commonFlatFieldMetadata,
type: createFieldInput.type,
options,
defaultValue: commonFlatFieldMetadata.defaultValue as string, // Could this be improved ?
settings: null,
} satisfies FlatFieldMetadata<typeof createFieldInput.type>,
parentFlatObjectMetadata,
},
...commonFlatFieldMetadata,
type: createFieldInput.type,
options,
defaultValue: commonFlatFieldMetadata.defaultValue as string, // Could this be improved ?
settings: null,
} satisfies FlatFieldMetadata<typeof createFieldInput.type>,
];
}
case FieldMetadataType.UUID:
@@ -143,11 +129,8 @@ export const fromCreateFieldInputToFlatFieldAndItsFlatObjectMetadata = async ({
case FieldMetadataType.TS_VECTOR: {
return [
{
flatFieldMetadata: {
...commonFlatFieldMetadata,
type: createFieldInput.type,
},
parentFlatObjectMetadata,
...commonFlatFieldMetadata,
type: createFieldInput.type,
},
];
}
@@ -2,6 +2,8 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import {
FieldMetadataException,
@@ -9,11 +11,32 @@ import {
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { validateRelationCreationPayloadOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { FlatFieldAndItsFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-and-its-flat-object-metadata.util';
import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
type ComputeFieldMetadataRelationSettingsForRelationTypeArgs = {
relationType: RelationType;
fieldMetadataName: string;
};
const computeFieldMetadataRelationSettingsForRelationType = ({
fieldMetadataName,
relationType,
}: ComputeFieldMetadataRelationSettingsForRelationTypeArgs) => {
if (relationType === RelationType.MANY_TO_ONE) {
return {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: fieldMetadataName,
};
}
return {
relationType: RelationType.ONE_TO_MANY,
};
};
type FromRelationCreateFieldInputToFlatFieldMetadataArgs = {
createFieldInput: CreateFieldInput;
existingFlatObjectMetadatas: FlatObjectMetadata[];
@@ -24,7 +47,7 @@ export const fromRelationCreateFieldInputToFlatFieldMetadata = async ({
sourceParentFlatObjectMetadata,
createFieldInput,
}: FromRelationCreateFieldInputToFlatFieldMetadataArgs): Promise<
FlatFieldAndItsFlatObjectMetadata[]
FlatFieldMetadata[]
> => {
const { relationCreationPayload } = createFieldInput;
@@ -48,6 +71,11 @@ export const fromRelationCreateFieldInputToFlatFieldMetadata = async ({
);
}
const sourceFlatFieldMetadataSettings =
computeFieldMetadataRelationSettingsForRelationType({
fieldMetadataName: createFieldInput.name,
relationType: relationCreationPayload.type,
});
const targetRelationTargetFieldMetadataId = v4();
const sourceRelationTargetFieldMetadataId = v4();
const sourceFlatFieldMetadata: Omit<
@@ -58,33 +86,43 @@ export const fromRelationCreateFieldInputToFlatFieldMetadata = async ({
createFieldInput,
fieldMetadataId: sourceRelationTargetFieldMetadataId,
}),
icon: createFieldInput.icon ?? 'IconRelationOneToMany',
type: FieldMetadataType.RELATION,
defaultValue: null,
settings: null,
settings: sourceFlatFieldMetadataSettings,
options: null,
relationTargetFieldMetadataId: targetRelationTargetFieldMetadataId, // Note: this won't work until we enable deferred transaction
relationTargetObjectMetadataId: targetParentFlatObjectMetadata.id,
flatRelationTargetObjectMetadata: targetParentFlatObjectMetadata,
};
const targetCreateFieldInput: CreateFieldInput = {
icon: relationCreationPayload.targetFieldIcon ?? 'Icon123',
label: relationCreationPayload.targetFieldLabel,
name: `${computeMetadataNameFromLabel(
relationCreationPayload.targetFieldLabel,
)}Id`,
objectMetadataId: targetParentFlatObjectMetadata.id,
type: FieldMetadataType.RELATION,
workspaceId: createFieldInput.workspaceId,
};
const targetFlatFieldMetadataSettings =
computeFieldMetadataRelationSettingsForRelationType({
fieldMetadataName: targetCreateFieldInput.name,
relationType:
relationCreationPayload.type === RelationType.ONE_TO_MANY
? RelationType.MANY_TO_ONE
: RelationType.ONE_TO_MANY,
});
const targetFlatFieldMetadata: FlatFieldMetadata<FieldMetadataType.RELATION> =
{
...getDefaultFlatFieldMetadata({
createFieldInput: {
icon: relationCreationPayload.targetFieldIcon,
label: relationCreationPayload.targetFieldLabel,
name: `${computeMetadataNameFromLabel(
relationCreationPayload.targetFieldLabel,
)}Id`,
objectMetadataId: targetParentFlatObjectMetadata.id,
type: FieldMetadataType.RELATION,
workspaceId: createFieldInput.workspaceId,
},
createFieldInput: targetCreateFieldInput,
fieldMetadataId: targetRelationTargetFieldMetadataId,
}),
type: FieldMetadataType.RELATION,
defaultValue: null,
settings: null,
settings: targetFlatFieldMetadataSettings,
options: null,
relationTargetFieldMetadataId: sourceRelationTargetFieldMetadataId,
relationTargetObjectMetadataId: sourceParentFlatObjectMetadata.id,
@@ -94,15 +132,9 @@ export const fromRelationCreateFieldInputToFlatFieldMetadata = async ({
return [
{
flatFieldMetadata: {
...sourceFlatFieldMetadata,
flatRelationTargetFieldMetadata: targetFlatFieldMetadata,
},
parentFlatObjectMetadata: sourceParentFlatObjectMetadata,
...sourceFlatFieldMetadata,
flatRelationTargetFieldMetadata: targetFlatFieldMetadata,
},
{
flatFieldMetadata: targetFlatFieldMetadata,
parentFlatObjectMetadata: targetParentFlatObjectMetadata,
},
] satisfies FlatFieldAndItsFlatObjectMetadata<FieldMetadataType.RELATION>[];
targetFlatFieldMetadata,
] satisfies FlatFieldMetadata<FieldMetadataType.RELATION>[];
};