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
@@ -5,6 +5,7 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils';
import { FindManyOptions, FindOneOptions, Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util';
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
@@ -43,6 +44,7 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService<ObjectMetadataE
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly applicationService: ApplicationService,
) {
super(objectMetadataRepository);
}
@@ -323,10 +325,22 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService<ObjectMetadataE
async createOneObject({
createObjectInput,
workspaceId,
applicationId,
}: {
createObjectInput: CreateObjectInput;
workspaceId: string;
/**
* @deprecated do not use call validateBuildAndRunWorkspaceMigration contextually
* when interacting with another application than workspace custom one
* */
applicationId?: string;
}): Promise<FlatObjectMetadata> {
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const {
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
flatViewMaps: existingFlatViewMaps,
@@ -354,6 +368,8 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService<ObjectMetadataE
} = fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate({
createObjectInput,
workspaceId,
workspaceCustomApplicationId:
applicationId ?? workspaceCustomFlatApplication.id,
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
});
@@ -367,13 +383,15 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService<ObjectMetadataE
flatEntityToUpdate: [],
});
const flatDefaultViewToCreate = await this.createDefaultFlatView({
const flatDefaultViewToCreate = await this.computeFlatViewToCreate({
objectMetadata: flatObjectMetadataToCreate,
workspaceId,
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
});
const flatDefaultViewFieldsToCreate =
await this.createDefaultFlatViewFields({
await this.computeFlatViewFieldsToCreate({
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
objectFlatFieldMetadatas: findManyFlatEntityByIdInFlatEntityMapsOrThrow(
{
flatEntityMaps: flatFieldMetadataMapsFromTo.to,
@@ -456,10 +474,12 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService<ObjectMetadataE
return createdFlatObjectMetadata;
}
private async createDefaultFlatView({
private async computeFlatViewToCreate({
objectMetadata,
workspaceId,
workspaceCustomApplicationId,
}: {
workspaceCustomApplicationId: string;
objectMetadata: FlatObjectMetadata;
workspaceId: string;
}) {
@@ -475,16 +495,19 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService<ObjectMetadataE
const flatViewFromCreateInput = fromCreateViewInputToFlatViewToCreate({
createViewInput: defaultViewInput,
workspaceId,
workspaceCustomApplicationId,
});
return flatViewFromCreateInput;
}
private async createDefaultFlatViewFields({
private async computeFlatViewFieldsToCreate({
objectFlatFieldMetadatas,
viewId,
workspaceId,
workspaceCustomApplicationId,
}: {
workspaceCustomApplicationId: string;
objectFlatFieldMetadatas: FlatFieldMetadata[];
viewId: string;
workspaceId: string;
@@ -500,6 +523,7 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService<ObjectMetadataE
size: DEFAULT_VIEW_FIELD_SIZE,
viewId: viewId,
},
workspaceCustomApplicationId,
workspaceId: workspaceId,
}),
);
@@ -9,6 +9,7 @@ import {
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@@ -50,6 +51,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
FieldMetadataEntity,
]),
TypeOrmModule.forFeature([FeatureFlagEntity, ViewEntity]),
ApplicationModule,
DataSourceModule,
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
@@ -1,123 +0,0 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 } from 'uuid';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
BASE_OBJECT_STANDARD_FIELD_IDS,
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
export const buildDefaultFieldsForCustomObject = (
workspaceId: string,
): Array<Partial<FieldMetadataEntity> & { id: string }> => [
{
id: v4(),
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
type: FieldMetadataType.UUID,
name: 'id',
label: 'Id',
icon: 'Icon123',
description: 'Id',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: true,
isUIReadOnly: true,
workspaceId,
defaultValue: 'uuid',
},
{
id: v4(),
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
icon: 'IconAbc',
description: 'Name',
isNullable: false,
isActive: true,
isCustom: false,
isUIReadOnly: false,
workspaceId,
defaultValue: "''",
},
{
id: v4(),
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt,
type: FieldMetadataType.DATE_TIME,
name: 'createdAt',
label: 'Creation date',
icon: 'IconCalendar',
description: 'Creation date',
isNullable: false,
isActive: true,
isCustom: false,
isUIReadOnly: false,
workspaceId,
defaultValue: 'now',
},
{
id: v4(),
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
type: FieldMetadataType.DATE_TIME,
name: 'updatedAt',
label: 'Last update',
icon: 'IconCalendarClock',
description: 'Last time the record was changed',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: false,
workspaceId,
defaultValue: 'now',
},
{
id: v4(),
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
type: FieldMetadataType.DATE_TIME,
name: 'deletedAt',
label: 'Deleted at',
icon: 'IconCalendarClock',
description: 'Deletion date',
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: true,
workspaceId,
defaultValue: null,
},
{
id: v4(),
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
type: FieldMetadataType.ACTOR,
name: 'createdBy',
label: 'Created by',
icon: 'IconCreativeCommonsSa',
description: 'The creator of the record',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: false,
isUIReadOnly: true,
workspaceId,
defaultValue: { name: "''", source: "'MANUAL'" },
},
{
id: v4(),
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.position,
type: FieldMetadataType.POSITION,
name: 'position',
label: 'Position',
icon: 'IconHierarchy2',
description: 'Position',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: true,
isUIReadOnly: false,
workspaceId,
defaultValue: 0,
},
];
@@ -7,7 +7,6 @@ import {
BASE_OBJECT_STANDARD_FIELD_IDS,
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { createDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
type BuildDefaultFlatFieldMetadataForCustomObjectArgs = {
@@ -24,9 +23,10 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
flatObjectMetadata: { id: objectMetadataId, applicationId },
}: BuildDefaultFlatFieldMetadataForCustomObjectArgs) => {
const createdAt = new Date();
const idFieldId = v4();
const idField: FlatFieldMetadata<FieldMetadataType.UUID> = {
type: FieldMetadataType.UUID,
id: v4(),
id: idFieldId,
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
@@ -34,10 +34,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isLabelSyncedWithName: false,
isUnique: true,
objectMetadataId,
universalIdentifier: createDeterministicUuid([
objectMetadataId,
BASE_OBJECT_STANDARD_FIELD_IDS.id,
]),
universalIdentifier: idFieldId,
workspaceId,
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
name: 'id',
@@ -63,9 +60,10 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
applicationId: applicationId ?? null,
};
const nameFieldId = v4();
const nameField: FlatFieldMetadata<FieldMetadataType.TEXT> = {
type: FieldMetadataType.TEXT,
id: v4(),
id: nameFieldId,
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
@@ -73,10 +71,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
universalIdentifier: createDeterministicUuid([
objectMetadataId,
CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
]),
universalIdentifier: nameFieldId,
workspaceId,
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
name: 'name',
@@ -102,9 +97,10 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
applicationId: applicationId ?? null,
};
const createdAtFieldId = v4();
const createdAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
type: FieldMetadataType.DATE_TIME,
id: v4(),
id: createdAtFieldId,
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
@@ -112,10 +108,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
universalIdentifier: createDeterministicUuid([
objectMetadataId,
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt,
]),
universalIdentifier: createdAtFieldId,
workspaceId,
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt,
name: 'createdAt',
@@ -141,9 +134,10 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
applicationId: applicationId ?? null,
};
const updatedAtFieldId = v4();
const updatedAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
type: FieldMetadataType.DATE_TIME,
id: v4(),
id: updatedAtFieldId,
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
@@ -151,10 +145,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
universalIdentifier: createDeterministicUuid([
objectMetadataId,
BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
]),
universalIdentifier: updatedAtFieldId,
workspaceId,
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
name: 'updatedAt',
@@ -180,9 +171,10 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
applicationId: applicationId ?? null,
};
const deletedAtFieldId = v4();
const deletedAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
type: FieldMetadataType.DATE_TIME,
id: v4(),
id: deletedAtFieldId,
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
@@ -190,10 +182,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
universalIdentifier: createDeterministicUuid([
objectMetadataId,
BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
]),
universalIdentifier: deletedAtFieldId,
workspaceId,
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
name: 'deletedAt',
@@ -219,9 +208,10 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
applicationId: applicationId ?? null,
};
const createdByFieldId = v4();
const createdByField: FlatFieldMetadata<FieldMetadataType.ACTOR> = {
type: FieldMetadataType.ACTOR,
id: v4(),
id: createdByFieldId,
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
@@ -229,10 +219,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
universalIdentifier: createDeterministicUuid([
objectMetadataId,
CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
]),
universalIdentifier: createdByFieldId,
workspaceId,
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
name: 'createdBy',
@@ -257,9 +244,10 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
applicationId: applicationId ?? null,
};
const positionFieldId = v4();
const positionField: FlatFieldMetadata<FieldMetadataType.POSITION> = {
type: FieldMetadataType.POSITION,
id: v4(),
id: positionFieldId,
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
@@ -267,10 +255,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
universalIdentifier: createDeterministicUuid([
objectMetadataId,
CUSTOM_OBJECT_STANDARD_FIELD_IDS.position,
]),
universalIdentifier: positionFieldId,
workspaceId,
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.position,
name: 'position',
@@ -296,20 +281,18 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
applicationId: applicationId ?? null,
};
const searchVectorFieldId = v4();
const searchVectorField: FlatFieldMetadata<FieldMetadataType.TS_VECTOR> = {
type: FieldMetadataType.TS_VECTOR,
viewFieldIds: [],
viewGroupIds: [],
kanbanAggregateOperationViewIds: [],
calendarViewIds: [],
id: v4(),
id: searchVectorFieldId,
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
universalIdentifier: createDeterministicUuid([
objectMetadataId,
CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector,
]),
universalIdentifier: searchVectorFieldId,
workspaceId,
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector,
name: 'searchVector',
@@ -1,7 +1,7 @@
import { type STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
@@ -16,15 +16,9 @@ import {
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
import {
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
STANDARD_OBJECT_FIELD_IDS,
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
import {
createDeterministicUuid,
createRelationDeterministicUuid,
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
import { createRelationDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
const generateSourceFlatFieldMetadata = ({
workspaceId,
@@ -49,17 +43,6 @@ const generateSourceFlatFieldMetadata = ({
STANDARD_OBJECT_ICONS[
targetFlatObjectMetadata.nameSingular as keyof typeof STANDARD_OBJECT_ICONS
] || 'IconBuildingSkyscraper';
const standardId =
CUSTOM_OBJECT_STANDARD_FIELD_IDS[
targetFlatObjectMetadata.namePlural as keyof typeof CUSTOM_OBJECT_STANDARD_FIELD_IDS
];
if (!isDefined(standardId)) {
throw new ObjectMetadataException(
`Standard field ID not found for target object ${targetFlatObjectMetadata.namePlural}`,
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
}
return {
calendarViewIds: [],
@@ -89,16 +72,13 @@ const generateSourceFlatFieldMetadata = ({
settings: {
relationType: RelationType.ONE_TO_MANY,
},
standardId,
standardId: null,
standardOverrides: null,
type: FieldMetadataType.RELATION,
universalIdentifier: createDeterministicUuid([
sourceFlatObjectMetadata.id,
standardId,
]),
universalIdentifier: sourceFieldMetadataId,
workspaceId,
morphId: null,
applicationId: sourceFlatObjectMetadata.applicationId ?? null,
applicationId: sourceFlatObjectMetadata.applicationId,
};
};
@@ -165,11 +145,8 @@ const generateTargetFlatFieldMetadata = ({
relationTargetFieldMetadataId: sourceFlatFieldMetadata.id,
relationTargetObjectMetadataId: sourceFlatObjectMetadata.id,
standardOverrides: null,
universalIdentifier: createDeterministicUuid([
targetFlatObjectMetadata.id,
standardId,
]),
applicationId: sourceFlatObjectMetadata.applicationId ?? null,
universalIdentifier: sourceFlatFieldMetadata.relationTargetFieldMetadataId,
applicationId: sourceFlatObjectMetadata.applicationId,
};
};