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
This commit is contained in:
Paul Rastoin
2025-11-23 22:35:17 +01:00
committed by GitHub
parent 4848bc03f3
commit f9ab09c404
99 changed files with 3458 additions and 730 deletions
@@ -25,12 +25,14 @@ import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-m
export type FromCreateFieldInputToFlatObjectMetadataArgs = {
createFieldInput: Omit<CreateFieldInput, 'workspaceId'>;
workspaceId: string;
workspaceCustomApplicationId: string;
} & Pick<AllFlatEntityMaps, 'flatObjectMetadataMaps'>;
export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
createFieldInput: rawCreateFieldInput,
workspaceId,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
workspaceCustomApplicationId,
}: FromCreateFieldInputToFlatObjectMetadataArgs): Promise<
FieldInputTranspilationResult<{
flatFieldMetadatas: FlatFieldMetadata[];
@@ -71,6 +73,7 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
createFieldInput,
workspaceId,
fieldMetadataId,
workspaceCustomApplicationId,
});
switch (createFieldInput.type) {
@@ -83,6 +86,7 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
existingFlatObjectMetadataMaps,
sourceFlatObjectMetadata: parentFlatObjectMetadata,
workspaceId,
workspaceCustomApplicationId,
});
}
case FieldMetadataType.RELATION: {
@@ -94,6 +98,7 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
type: createFieldInput.type,
},
workspaceId,
workspaceCustomApplicationId,
});
}
case FieldMetadataType.RATING: {
@@ -21,12 +21,14 @@ type FromMorphRelationCreateFieldInputToFlatFieldMetadatasArgs = {
existingFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
sourceFlatObjectMetadata: FlatObjectMetadata;
workspaceId: string;
workspaceCustomApplicationId: string;
};
export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
createFieldInput,
existingFlatObjectMetadataMaps,
sourceFlatObjectMetadata,
workspaceId,
workspaceCustomApplicationId,
}: FromMorphRelationCreateFieldInputToFlatFieldMetadatasArgs): Promise<
FieldInputTranspilationResult<{
flatFieldMetadatas: FlatFieldMetadata[];
@@ -90,6 +92,7 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
targetFlatObjectMetadata,
workspaceId,
morphId,
workspaceCustomApplicationId,
});
return {
@@ -21,12 +21,14 @@ type FromRelationCreateFieldInputToFlatFieldMetadataArgs = {
existingFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
sourceFlatObjectMetadata: FlatObjectMetadata;
workspaceId: string;
workspaceCustomApplicationId: string;
};
export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({
existingFlatObjectMetadataMaps,
sourceFlatObjectMetadata,
createFieldInput,
workspaceId,
workspaceCustomApplicationId,
}: FromRelationCreateFieldInputToFlatFieldMetadataArgs): Promise<
FieldInputTranspilationResult<SourceTargetMorphOrRelationFlatFieldAndFlatIndex>
> => {
@@ -67,6 +69,7 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({
sourceFlatObjectMetadata,
targetFlatObjectMetadata,
workspaceId,
workspaceCustomApplicationId,
});
return {
@@ -44,7 +44,7 @@ export const generateIndexForFlatFieldMetadata = ({
universalIdentifier: indexId,
updatedAt: createdAt,
workspaceId,
applicationId: null,
applicationId: flatFieldMetadata.applicationId,
},
flatObjectMetadata,
},
@@ -1,7 +1,7 @@
import {
FieldMetadataType,
RelationType,
RelationOnDeleteAction,
RelationType,
} from 'twenty-shared/types';
import { v4 } from 'uuid';
@@ -46,6 +46,7 @@ type GenerateMorphOrRelationFlatFieldMetadataPairArgs = {
> & { type: MorphOrRelationFieldMetadataType };
workspaceId: string;
morphId?: string | null;
workspaceCustomApplicationId: string;
};
export type SourceTargetMorphOrRelationFlatFieldAndFlatIndex = {
@@ -58,6 +59,7 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
sourceFlatObjectMetadata,
targetFlatObjectMetadata,
workspaceId,
workspaceCustomApplicationId,
sourceFlatObjectMetadataJoinColumnName,
morphId = null,
}: GenerateMorphOrRelationFlatFieldMetadataPairArgs): SourceTargetMorphOrRelationFlatFieldAndFlatIndex => {
@@ -78,6 +80,7 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
createFieldInput,
workspaceId,
fieldMetadataId: sourceRelationTargetFieldMetadataId,
workspaceCustomApplicationId,
}),
morphId,
objectMetadataId: sourceFlatObjectMetadata.id,
@@ -116,6 +119,7 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
createFieldInput: targetCreateFieldInput,
workspaceId,
fieldMetadataId: targetRelationTargetFieldMetadataId,
workspaceCustomApplicationId,
}),
type: FieldMetadataType.RELATION,
defaultValue: null,
@@ -9,11 +9,13 @@ type GetDefaultFlatFieldMetadataArgs = {
fieldMetadataId: string;
createFieldInput: Omit<CreateFieldInput, 'workspaceId'>;
workspaceId: string;
workspaceCustomApplicationId: string;
};
export const getDefaultFlatFieldMetadata = ({
createFieldInput,
fieldMetadataId,
workspaceId,
workspaceCustomApplicationId,
}: GetDefaultFlatFieldMetadataArgs) => {
const { defaultValue, settings } = extractAndSanitizeObjectStringFields(
createFieldInput,
@@ -56,7 +58,7 @@ export const getDefaultFlatFieldMetadata = ({
updatedAt: createdAt,
isUIReadOnly: createFieldInput.isUIReadOnly ?? false,
morphId: null,
applicationId: createFieldInput.applicationId ?? null,
applicationId: workspaceCustomApplicationId,
viewFilterIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],