Files
twenty/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-morph-relation-create-field-input-to-flat-field-metadatas.util.ts
T
Paul Rastoin f9ab09c404 Metadata api create entity in workspace custom app (#15911)
# Introduction
Cleaner and fewer scope version of
https://github.com/twentyhq/twenty/pull/15745 ( removed sync-metadata
hack through, too ambitious migration and upgrade )

Please note that this PR won't have any interaction with the existing
sync-metadata
Which mean that the sync metadata does not update the standard entities
applicationId and universalIdentifier, and it won't we will deprecate it
on favor of a workspace migration aka twenty-standard app installation

## API Metadata
Any operation going through the api metadata nows automatically scope
the related entity to the workspace custom application instance. (
optionally passing an applicationId to allow current hacky implem of app
sync service )

We need to either ignore the tests or remove the cli status check from
the blocking status badges for a PR to be merged

## New workspace
Already handled in previous
https://github.com/twentyhq/twenty/pull/15625, when a workspace is
created it gets created a twenty standard and custom workspace instance

All his views and permissions will be prefilled to the its twenty
standard app instance with a specific universalIdentifier

## New universalIdentifier
At the contrary as before with standardIds, universalIdentifier are
unique for a given workspace
This means that createdAt field of both object company and opportunity
will have a unique universalIdentifier whereas they share the same
standardId

## FlatApplication
Introduced the flatApplication and cache. Will migrate existing
`MetadataName` to be `SyncableMetadataName` in a following PR

## What's next
Next we will describe a twenty standard app configuration as json that
will be used to generate a workspace migration that will be run instead
of the sync metadata, in a nutshell we aim to deprecated the sync
metadata
So we can standardize any entity to have a non nullable applicationId
and universalIdentifier

## Upgrade command
Introduced an upgrade command that will create a custom workspace
instance for any workspace that do not have one in order to align with
the new behavior when creating a new workspace
2025-11-23 22:35:17 +01:00

114 lines
4.7 KiB
TypeScript

import { msg } from '@lingui/core/macro';
import { type FieldMetadataType } from 'twenty-shared/types';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util';
import { validateMorphRelationCreationPayload } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-creation-payload.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
type FromMorphRelationCreateFieldInputToFlatFieldMetadatasArgs = {
createFieldInput: Omit<CreateFieldInput, 'workspaceId'> & {
type: FieldMetadataType.MORPH_RELATION;
};
existingFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
sourceFlatObjectMetadata: FlatObjectMetadata;
workspaceId: string;
workspaceCustomApplicationId: string;
};
export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
createFieldInput,
existingFlatObjectMetadataMaps,
sourceFlatObjectMetadata,
workspaceId,
workspaceCustomApplicationId,
}: FromMorphRelationCreateFieldInputToFlatFieldMetadatasArgs): Promise<
FieldInputTranspilationResult<{
flatFieldMetadatas: FlatFieldMetadata[];
indexMetadatas: FlatIndexMetadata[];
}>
> => {
const rawMorphCreationPayload =
createFieldInput.morphRelationsCreationPayload;
if (
!isDefined(rawMorphCreationPayload) ||
!Array.isArray(rawMorphCreationPayload)
) {
return {
status: 'fail',
error: {
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
message: `Relation creation payload is required`,
userFriendlyMessage: msg`Relation creation payload is required`,
value: rawMorphCreationPayload,
},
};
}
const morphRelationCreationPayloadValidation =
await validateMorphRelationCreationPayload({
existingFlatObjectMetadataMaps,
morphRelationCreationPayload: rawMorphCreationPayload,
objectMetadataId: sourceFlatObjectMetadata.id,
});
if (morphRelationCreationPayloadValidation.status === 'fail') {
return morphRelationCreationPayloadValidation;
}
const morphRelationCreationPayload =
morphRelationCreationPayloadValidation.result;
const morphId = v4();
const flatFieldsAndIndexes = morphRelationCreationPayload.reduce(
(acc, { relationCreationPayload, targetFlatObjectMetadata }) => {
const currentMorphRelationFieldName = computeMorphRelationFieldName({
fieldName: createFieldInput.name,
relationType: relationCreationPayload.type,
targetObjectMetadataNameSingular: targetFlatObjectMetadata.nameSingular,
targetObjectMetadataNamePlural: targetFlatObjectMetadata.namePlural,
});
const sourceFlatObjectMetadataJoinColumnName =
computeMorphOrRelationFieldJoinColumnName({
name: currentMorphRelationFieldName,
});
const { flatFieldMetadatas, indexMetadatas } =
generateMorphOrRelationFlatFieldMetadataPair({
createFieldInput: {
...createFieldInput,
relationCreationPayload,
name: currentMorphRelationFieldName,
},
sourceFlatObjectMetadataJoinColumnName,
sourceFlatObjectMetadata,
targetFlatObjectMetadata,
workspaceId,
morphId,
workspaceCustomApplicationId,
});
return {
indexMetadatas: [...acc.indexMetadatas, ...indexMetadatas],
flatFieldMetadatas: [...acc.flatFieldMetadatas, ...flatFieldMetadatas],
};
},
{
indexMetadatas: [],
flatFieldMetadatas: [],
},
);
return {
status: 'success',
result: flatFieldsAndIndexes,
};
};