diff --git a/packages/twenty-server/src/command/command.module.ts b/packages/twenty-server/src/command/command.module.ts index 2b76ad76e79..acb3c2084d7 100644 --- a/packages/twenty-server/src/command/command.module.ts +++ b/packages/twenty-server/src/command/command.module.ts @@ -5,7 +5,6 @@ import { DatabaseCommandModule } from 'src/database/commands/database-command.mo import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { WorkspaceCleanerModule } from 'src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module'; -import { WorkspaceHealthCommandModule } from 'src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module'; import { WorkspaceMigrationRunnerCommandsModule } from 'src/engine/workspace-manager/workspace-migration-runner/commands/workspace-migration-runner-commands.module'; import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cleaner/messaging-message-cleaner.module'; @@ -15,7 +14,6 @@ import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cle AppModule, WorkspaceSyncMetadataCommandsModule, DatabaseCommandModule, - WorkspaceHealthCommandModule, MessagingMessageCleanerModule, WorkspaceMigrationRunnerCommandsModule, ObjectMetadataModule, diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts index c6e7b08ecbf..888259737a1 100644 --- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts +++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts @@ -9,8 +9,6 @@ import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfa import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; export const FIELD_LINKS_MOCK_NAME = 'fieldLinks'; @@ -463,32 +461,3 @@ export const objectMetadataItemMock: ObjectMetadataEntity = { createdAt: new Date(), updatedAt: new Date(), } as ObjectMetadataEntity; - -export const objectMetadataMapItemMock: ObjectMetadataItemWithFieldMaps = { - ...objectMetadataItemMock, - fieldsById: FIELDS_MOCK.reduce( - (acc, field) => ({ - ...acc, - [field.id]: field, - }), - {}, - ), - fieldIdByName: FIELDS_MOCK.reduce( - (acc, field) => ({ - ...acc, - [field.name]: field.id, - }), - {}, - ), - fieldIdByJoinColumnName: {}, - indexMetadatas: [], -}; -export const objectMetadataMapsMock: ObjectMetadataMaps = { - byId: { - [objectMetadataMapItemMock.id || 'mock-id']: objectMetadataMapItemMock, - }, - idByNameSingular: { - [objectMetadataMapItemMock.nameSingular]: - objectMetadataMapItemMock.id || 'mock-id', - }, -}; diff --git a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts index 9c255e23f2a..2577034a8db 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler.ts @@ -11,39 +11,43 @@ import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-s import { getAllSelectableFields } from 'src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util'; import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant'; import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class CommonSelectedFieldsHandler { computeFromDepth = ({ objectsPermissions, - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, depth, }: { objectsPermissions: ObjectsPermissions; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatObjectMetadata: FlatObjectMetadata; depth: Depth | undefined; }): CommonSelectedFieldsResult => { const restrictedFields = - objectsPermissions[objectMetadataMapItem.id].restrictedFields; + objectsPermissions[flatObjectMetadata.id].restrictedFields; const { relations, relationsSelectFields } = this.getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, objectsPermissions, depth, }); const selectableFields = getAllSelectableFields({ restrictedFields, - objectMetadata: { - objectMetadataMapItem, - }, + flatObjectMetadata, + flatFieldMetadataMaps, }); return { @@ -57,13 +61,15 @@ export class CommonSelectedFieldsHandler { }; private getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, objectsPermissions, depth, }: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatObjectMetadata: FlatObjectMetadata; objectsPermissions: ObjectsPermissions; depth: Depth | undefined; }) { @@ -82,12 +88,19 @@ export class CommonSelectedFieldsHandler { | { [key: string]: boolean | { [key: string]: boolean } }; } = {}; - for (const field of Object.values(objectMetadataMapItem.fieldsById)) { - if (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const field = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); + + if (!isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION)) continue; + if (!field.relationTargetObjectMetadataId) continue; + const relationTargetObjectMetadata = - objectMetadataMaps.byId[field.relationTargetObjectMetadataId]; + flatObjectMetadataMaps.byId[field.relationTargetObjectMetadataId]; if (!isDefined(relationTargetObjectMetadata)) { throw new CommonQueryRunnerException( @@ -98,9 +111,8 @@ export class CommonSelectedFieldsHandler { const relationFieldSelectFields = getAllSelectableFields({ restrictedFields: objectsPermissions[relationTargetObjectMetadata.id].restrictedFields, - objectMetadata: { - objectMetadataMapItem: relationTargetObjectMetadata, - }, + flatObjectMetadata: relationTargetObjectMetadata, + flatFieldMetadataMaps, }); if (Object.keys(relationFieldSelectFields).length === 0) continue; @@ -113,8 +125,9 @@ export class CommonSelectedFieldsHandler { relations: depth2Relations, relationsSelectFields: depth2RelationsSelectFields, } = this.getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem: relationTargetObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata: relationTargetObjectMetadata, objectsPermissions, depth: 1, }) as { diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts index d2661301b54..b2892621004 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts @@ -52,8 +52,10 @@ import { transformLinksValue } from 'src/engine/core-modules/record-transformer/ import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util'; import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class DataArgProcessor { @@ -65,12 +67,14 @@ export class DataArgProcessor { async process({ partialRecordInputs, authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, shouldBackfillPositionIfUndefined = true, }: { partialRecordInputs: Partial[] | undefined; authContext: AuthContext; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; shouldBackfillPositionIfUndefined?: boolean; }): Promise[]> { if (!isDefined(partialRecordInputs)) { @@ -87,6 +91,12 @@ export class DataArgProcessor { workspace.id, ); + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const processedRecords: Partial[] = []; for (const record of partialRecordInputs) { @@ -94,18 +104,23 @@ export class DataArgProcessor { for (const [key, value] of Object.entries(record)) { const fieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName[key] || - objectMetadataItemWithFieldMaps.fieldIdByJoinColumnName[key]; + fieldIdByName[key] || fieldIdByJoinColumnName[key]; if (!isDefined(fieldMetadataId)) { throw new CommonQueryRunnerException( - `Object ${objectMetadataItemWithFieldMaps.nameSingular} doesn't have any "${key}" field.`, + `Object ${flatObjectMetadata.nameSingular} doesn't have any "${key}" field.`, CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, ); } - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + const fieldMetadata = flatFieldMetadataMaps.byId[fieldMetadataId]; + + if (!fieldMetadata) { + throw new CommonQueryRunnerException( + `Field metadata not found for field ${key}`, + CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA, + ); + } if ( !isDefined(fieldMetadata.defaultValue) && @@ -137,9 +152,9 @@ export class DataArgProcessor { partialRecordInputs: processedRecords, workspaceId: workspace.id, objectMetadata: { - isCustom: objectMetadataItemWithFieldMaps.isCustom, - nameSingular: objectMetadataItemWithFieldMaps.nameSingular, - fieldIdByName: objectMetadataItemWithFieldMaps.fieldIdByName, + isCustom: flatObjectMetadata.isCustom, + nameSingular: flatObjectMetadata.nameSingular, + fieldIdByName, }, shouldBackfillPositionIfUndefined, }); @@ -148,7 +163,7 @@ export class DataArgProcessor { } private async processField( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, key: string, value: unknown, isNullEquivalenceEnabled: boolean, diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts index 2af321989f4..084a0139391 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/query-runner-args.factory.ts @@ -5,15 +5,15 @@ import { isDefined } from 'twenty-shared/utils'; import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class QueryRunnerArgsFactory { constructor( - private readonly recordPositionService: RecordPositionService, private readonly recordInputTransformerService: RecordInputTransformerService, ) {} @@ -21,12 +21,18 @@ export class QueryRunnerArgsFactory { T extends ObjectRecordFilter | undefined, >( filter: T, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): T { if (!isDefined(filter)) { return filter; } + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const overrideFilter = (filterObject: ObjectRecordFilter) => { return Object.entries(filterObject).reduce((acc, [key, value]) => { if (key === 'and' || key === 'or') { @@ -42,7 +48,8 @@ export class QueryRunnerArgsFactory { acc[key] = this.transformFilterValueByType( key, value, - objectMetadataItemWithFieldMaps, + fieldIdByName, + flatFieldMetadataMaps, ); } @@ -57,11 +64,13 @@ export class QueryRunnerArgsFactory { key: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + fieldIdByName: Record, + flatFieldMetadataMaps: FlatEntityMaps, ) { - const fieldMetadataId = objectMetadataItemWithFieldMaps.fieldIdByName[key]; - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + const fieldMetadataId = fieldIdByName[key]; + const fieldMetadata = fieldMetadataId + ? flatFieldMetadataMaps.byId[fieldMetadataId] + : undefined; if (!fieldMetadata) { return value; @@ -90,18 +99,22 @@ export class QueryRunnerArgsFactory { key: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, - fieldMetadataMapByName: FieldMetadataMap, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + fieldIdByName: Record, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { - const fieldMetadata = fieldMetadataMapByName[key]; + const fieldMetadata = flatFieldMetadataMaps.byId[fieldIdByName[key]]; if (!fieldMetadata) { return value; } - return this.recordInputTransformerService.process({ + const processed = await this.recordInputTransformerService.process({ recordInput: { [key]: value }, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); + + return processed[key] ?? value; } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index dc28fdffe25..d84d3c60632 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -36,6 +36,9 @@ import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service' import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsException, @@ -43,8 +46,6 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; @@ -93,8 +94,12 @@ export abstract class CommonBaseQueryRunnerService< args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - queryRunnerContext; + const { + authContext, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = queryRunnerContext; if (!isWorkspaceAuthContext(authContext)) { throw new CommonQueryRunnerException( @@ -107,7 +112,7 @@ export abstract class CommonBaseQueryRunnerService< await this.validate(args, queryRunnerContext); - if (objectMetadataItemWithFieldMaps.isSystem === true) { + if (flatObjectMetadata.isSystem === true) { await this.validateSettingsPermissionsOnObjectOrThrow( authContext, queryRunnerContext, @@ -115,8 +120,9 @@ export abstract class CommonBaseQueryRunnerService< } const commonQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); const processedArgs = await this.processArgs( @@ -172,8 +178,9 @@ export abstract class CommonBaseQueryRunnerService< protected abstract processQueryResult( queryResult: Output, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise; @@ -187,14 +194,14 @@ export abstract class CommonBaseQueryRunnerService< args.selectedFields, ); - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata } = queryRunnerContext; const computedArgs = await this.computeArgs(args, queryRunnerContext); const hookedArgs = (await this.workspaceQueryHookService.executePreQueryHooks( authContext, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, operationName, computedArgs as WorkspacePreQueryHookPayload, )) as CommonInput; @@ -231,9 +238,9 @@ export abstract class CommonBaseQueryRunnerService< results, operationName: this.operationName, authContext, - objectMetadataItemWithFieldMaps: - queryRunnerContext.objectMetadataItemWithFieldMaps, - objectMetadataMaps: queryRunnerContext.objectMetadataMaps, + flatObjectMetadata: queryRunnerContext.flatObjectMetadata, + flatObjectMetadataMaps: queryRunnerContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: queryRunnerContext.flatFieldMetadataMaps, }); } @@ -241,25 +248,28 @@ export abstract class CommonBaseQueryRunnerService< results, operationName, authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { results: Output; operationName: CommonQueryNames; authContext: WorkspaceAuthContext; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise { const resultWithGetters = await this.processQueryResult( results, - objectMetadataItemWithFieldMaps.id, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, ); await this.workspaceQueryHookService.executePostQueryHooks( authContext, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, operationName, resultWithGetters as QueryResultFieldValue, ); @@ -271,18 +281,18 @@ export abstract class CommonBaseQueryRunnerService< authContext: WorkspaceAuthContext, queryRunnerContext: CommonBaseQueryRunnerContext, ) { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; const workspace = authContext.workspace; if ( Object.keys(OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS).includes( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ) ) { const permissionRequired: PermissionFlagType = OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS[ - objectMetadataItemWithFieldMaps.nameSingular as keyof typeof OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS + flatObjectMetadata.nameSingular as keyof typeof OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS ]; const userHasPermission = @@ -368,7 +378,7 @@ export abstract class CommonBaseQueryRunnerService< const rolePermissionConfig = { unionOf: [roleId] }; const repository = workspaceDataSource.getRepository( - queryRunnerContext.objectMetadataItemWithFieldMaps.nameSingular, + queryRunnerContext.flatObjectMetadata.nameSingular, rolePermissionConfig, authContext, ); @@ -397,7 +407,7 @@ export abstract class CommonBaseQueryRunnerService< const repository = await this.globalWorkspaceOrmManager.getRepository( workspaceId, - queryRunnerContext.objectMetadataItemWithFieldMaps.nameSingular, + queryRunnerContext.flatObjectMetadata.nameSingular, rolePermissionConfig, ); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts index cd6f733044b..e0e5a41e1ac 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts @@ -31,9 +31,11 @@ import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runne import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @@ -62,22 +64,25 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer repository, authContext, rolePermissionConfig, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceDataSource, } = queryRunnerContext; const objectRecords = await this.insertOrUpsertRecords({ repository, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, args, }); const upsertedRecords = await this.fetchUpsertedRecords({ objectRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, repository, selectedFieldsResult: args.selectedFieldsResult, }); @@ -85,8 +90,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer await this.processNestedRelationsIfNeeded({ args, records: upsertedRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, workspaceDataSource, rolePermissionConfig, @@ -98,16 +104,18 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async processNestedRelationsIfNeeded({ args, records, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, workspaceDataSource, rolePermissionConfig, }: { args: CommonExtendedInput; records: ObjectRecord[]; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; authContext: AuthContext; workspaceDataSource: WorkspaceDataSource; rolePermissionConfig?: RolePermissionConfig; @@ -117,8 +125,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer } await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: records, relations: args.selectedFieldsResult.relations as Record< string, @@ -136,14 +145,16 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata, flatFieldMetadataMaps } = + queryRunnerContext; return { ...args, data: await this.dataArgProcessor.process({ partialRecordInputs: args.data, authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }), }; } @@ -152,9 +163,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); args.data.forEach((record) => { if (record?.id) { @@ -165,13 +176,15 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async insertOrUpsertRecords({ repository, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, args, }: { repository: WorkspaceRepository; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; args: CommonExtendedInput; }): Promise { const { selectedFieldsResult } = args; @@ -180,8 +193,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer const selectedColumns = buildColumnsToReturn({ select: selectedFieldsResult.select, relations: selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); return await repository.insert(args.data, undefined, selectedColumns); @@ -189,8 +203,9 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer return this.performUpsertOperation({ repository, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, args, selectedFieldsResult, }); @@ -198,23 +213,27 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async performUpsertOperation({ repository, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, args, selectedFieldsResult, }: { repository: WorkspaceRepository; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; args: CreateManyQueryArgs; selectedFieldsResult: CommonSelectedFieldsResult; }): Promise { const conflictingFields = getConflictingFields( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const existingRecords = await this.findExistingRecords({ repository, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, args, conflictingFields, }); @@ -234,15 +253,17 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer const columnsToReturn = buildColumnsToReturn({ select: selectedFieldsResult.select, relations: selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); if (recordsToUpdate.length > 0) { await this.processRecordsToUpdate({ partialRecordsToUpdate: recordsToUpdate, repository, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, result, columnsToReturn, }); @@ -260,12 +281,14 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async findExistingRecords({ repository, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, args, conflictingFields, }: { repository: WorkspaceRepository; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; args: CreateManyQueryArgs; conflictingFields: { baseField: string; @@ -274,7 +297,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer }[]; }): Promise { const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); const whereConditions = buildWhereConditions(args.data, conflictingFields); @@ -284,13 +307,14 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer }); const restrictedFields = - repository.objectRecordsPermissions?.[objectMetadataItemWithFieldMaps.id] + repository.objectRecordsPermissions?.[flatObjectMetadata.id] ?.restrictedFields; const selectOptions = getAllSelectableColumnNames({ restrictedFields: restrictedFields ?? {}, objectMetadata: { - objectMetadataMapItem: objectMetadataItemWithFieldMaps, + objectMetadataMapItem: flatObjectMetadata, + flatFieldMetadataMaps, }, }); @@ -305,19 +329,25 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async processRecordsToUpdate({ partialRecordsToUpdate, repository, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, result, columnsToReturn, }: { partialRecordsToUpdate: PartialObjectRecordWithId[]; repository: WorkspaceRepository; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; result: InsertResult; columnsToReturn: string[]; }): Promise { const partialRecordsToUpdateWithoutCreatedByUpdate = partialRecordsToUpdate.map((record) => - this.getRecordWithoutCreatedBy(record, objectMetadataItemWithFieldMaps), + this.getRecordWithoutCreatedBy( + record, + flatObjectMetadata, + flatFieldMetadataMaps, + ), ); const savedRecords = await repository.updateMany( @@ -363,26 +393,29 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer private async fetchUpsertedRecords({ objectRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, repository, selectedFieldsResult, }: { objectRecords: InsertResult; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; repository: WorkspaceRepository; selectedFieldsResult: CommonSelectedFieldsResult; }): Promise { const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); const columnsToSelect = buildColumnsToSelect({ select: selectedFieldsResult.select, relations: selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const upsertedRecords = await queryBuilder @@ -401,32 +434,38 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer async processQueryResult( queryResult: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return await this.commonResultGettersService.processRecordArray( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } private getRecordWithoutCreatedBy( record: PartialObjectRecordWithId, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): Omit { let recordWithoutCreatedByUpdate = record; - const createdByFieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName['createdBy']; + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const createdByFieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[createdByFieldMetadataId]; + flatFieldMetadataMaps.byId[fieldIdByName['createdBy']]; if (!isDefined(createdByFieldMetadata)) { throw new CommonQueryRunnerException( - `Missing createdBy field metadata for object ${objectMetadataItemWithFieldMaps.nameSingular}`, + `Missing createdBy field metadata for object ${flatObjectMetadata.nameSingular}`, CommonQueryRunnerExceptionCode.MISSING_SYSTEM_FIELD, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/__tests__/get-conflicting-fields.util.spec.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/__tests__/get-conflicting-fields.util.spec.ts index ce8758e9a84..4140dbe79ce 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/__tests__/get-conflicting-fields.util.spec.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/__tests__/get-conflicting-fields.util.spec.ts @@ -1,87 +1,128 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { objectMetadataMapItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { getConflictingFields } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; describe('getConflictingFields', () => { const workspaceId = 'workspaceId'; const objectMetadataId = 'objectMetadataId'; - const idField = getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId, + const createMockField = ( + overrides: Partial & { + id: string; + name: string; + type: FieldMetadataType; + }, + ): FlatFieldMetadata => + ({ + workspaceId, + objectMetadataId, + isNullable: false, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: overrides.id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + label: overrides.name, + ...overrides, + }) as FlatFieldMetadata; + + const idField = createMockField({ id: 'id-field-id', name: 'id', type: FieldMetadataType.UUID, isUnique: true, }); - const uniqueTextField = getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId, + const uniqueTextField = createMockField({ id: 'unique-text-id', name: 'uniqueText', type: FieldMetadataType.TEXT, isUnique: true, }); - const emailsUniqueField = getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId, + const emailsUniqueField = createMockField({ id: 'emails-unique-id', name: 'emailsField', type: FieldMetadataType.EMAILS, isUnique: true, }); - const phonesNotUniqueField = getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId, + const phonesNotUniqueField = createMockField({ id: 'phones-not-unique-id', name: 'phonesField', type: FieldMetadataType.PHONES, isUnique: false, }); - const addressUniqueFieldNoIncludedProp = getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId, + const addressUniqueFieldNoIncludedProp = createMockField({ id: 'address-unique-id', name: 'addressField', type: FieldMetadataType.ADDRESS, isUnique: true, }); - const buildObjectMetadataWithFields = ( - fields: (typeof idField)[], - ): ObjectMetadataItemWithFieldMaps => { - const fieldsById = fields.reduce>( + const buildFlatObjectMetadata = ( + fields: FlatFieldMetadata[], + ): FlatObjectMetadata => + ({ + id: objectMetadataId, + workspaceId, + nameSingular: 'testObject', + namePlural: 'testObjects', + labelSingular: 'Test Object', + labelPlural: 'Test Objects', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: objectMetadataId, + fieldMetadataIds: fields.map((f) => f.id), + indexMetadataIds: [], + viewIds: [], + applicationId: null, + }) as unknown as FlatObjectMetadata; + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( (acc, field) => { acc[field.id] = field; return acc; }, - {}, - ); + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; - return { - ...objectMetadataMapItemMock, - fieldsById, - fieldIdByName: Object.fromEntries( - Object.values(fieldsById).map((f) => [f.name, f.id]), - ), - } as ObjectMetadataItemWithFieldMaps; - }; + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); it('returns id and unique non-composite fields as conflicts', () => { - const objectMetadata = buildObjectMetadataWithFields([ - idField, - uniqueTextField, - ]); + const fields = [idField, uniqueTextField]; + const flatObjectMetadata = buildFlatObjectMetadata(fields); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); - const result = getConflictingFields(objectMetadata); + const result = getConflictingFields( + flatObjectMetadata, + flatFieldMetadataMaps, + ); expect(result).toEqual( expect.arrayContaining([ @@ -96,12 +137,14 @@ describe('getConflictingFields', () => { }); it('returns composite field with included unique property using full path and computed column', () => { - const objectMetadata = buildObjectMetadataWithFields([ - idField, - emailsUniqueField, - ]); + const fields = [idField, emailsUniqueField]; + const flatObjectMetadata = buildFlatObjectMetadata(fields); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); - const result = getConflictingFields(objectMetadata); + const result = getConflictingFields( + flatObjectMetadata, + flatFieldMetadataMaps, + ); expect(result).toEqual( expect.arrayContaining([ @@ -116,23 +159,27 @@ describe('getConflictingFields', () => { }); it('does not include composite fields without included unique property', () => { - const objectMetadata = buildObjectMetadataWithFields([ - idField, - addressUniqueFieldNoIncludedProp, - ]); + const fields = [idField, addressUniqueFieldNoIncludedProp]; + const flatObjectMetadata = buildFlatObjectMetadata(fields); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); - const result = getConflictingFields(objectMetadata); + const result = getConflictingFields( + flatObjectMetadata, + flatFieldMetadataMaps, + ); expect(result).toEqual([{ baseField: 'id', fullPath: 'id', column: 'id' }]); }); it('ignores non-unique fields', () => { - const objectMetadata = buildObjectMetadataWithFields([ - idField, - phonesNotUniqueField, - ]); + const fields = [idField, phonesNotUniqueField]; + const flatObjectMetadata = buildFlatObjectMetadata(fields); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); - const result = getConflictingFields(objectMetadata); + const result = getConflictingFields( + flatObjectMetadata, + flatFieldMetadataMaps, + ); expect(result).toEqual([{ baseField: 'id', fullPath: 'id', column: 'id' }]); }); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util.ts index 2657363e547..4207ef12da4 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util.ts @@ -1,16 +1,23 @@ -import { capitalize } from 'twenty-shared/utils'; import { compositeTypeDefinitions } from 'twenty-shared/types'; +import { capitalize } from 'twenty-shared/utils'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const getConflictingFields = ( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): { baseField: string; fullPath: string; column: string; }[] => { - return Object.values(objectMetadataItemWithFieldMaps.fieldsById) + return getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, + ) .filter((field) => field.isUnique || field.name === 'id') .flatMap((field) => { const compositeType = compositeTypeDefinitions.get(field.type); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts index 635fad0b52c..ad30ae45720 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-create-one-query-runner.service.ts @@ -16,8 +16,10 @@ import { CreateOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -51,12 +53,14 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata, flatFieldMetadataMaps } = + queryRunnerContext; const coercedData = await this.dataArgProcessor.process({ partialRecordInputs: [args.data], authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); return { @@ -67,14 +71,16 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } @@ -83,9 +89,9 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); if (args.data?.id) { assertIsValidUuid(args.data.id); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts index b546793c94b..f3a872e1fed 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-many-query-runner.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { isDefined } from 'class-validator'; import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; import { ObjectRecord } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { FindOptionsRelations, ObjectLiteral } from 'typeorm'; import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; @@ -22,8 +22,10 @@ import { } from 'src/engine/api/common/types/common-query-args.type'; import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -41,26 +43,28 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer authContext, rolePermissionConfig, workspaceDataSource, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, args.filter, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const deletedObjectRecords = await queryBuilder @@ -72,10 +76,10 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: deletedRecords, - //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -95,39 +99,43 @@ export class CommonDeleteManyQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } async processQueryResult( queryResult: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecordArray( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } async validate( - args: DeleteManyQueryArgs, + args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, - ) { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + ): Promise { + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); - if (!args.filter) { + assertMutationNotOnRemoteObject(flatObjectMetadata); + + if (!isDefined(args.filter)) { throw new CommonQueryRunnerException( 'Filter is required', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts index b9fb2c20fcd..b28eaa8e67b 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-delete-one-query-runner.service.ts @@ -20,8 +20,10 @@ import { DeleteOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonDeleteOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -67,14 +69,16 @@ export class CommonDeleteOneQueryRunnerService extends CommonBaseQueryRunnerServ async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } @@ -83,9 +87,9 @@ export class CommonDeleteOneQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); assertIsValidUuid(args.id); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts index 9cebffc580c..b90bb47e96e 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service.ts @@ -21,7 +21,11 @@ import { DestroyManyQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; @Injectable() export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -39,41 +43,44 @@ export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerSe authContext, rolePermissionConfig, workspaceDataSource, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, args.filter, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); - const deletedObjectRecords = await queryBuilder + const destroyedObjectRecords = await queryBuilder .delete() .returning(columnsToReturn) .execute(); - const deletedRecords = deletedObjectRecords.generatedMaps as ObjectRecord[]; + const destroyedRecords = + destroyedObjectRecords.generatedMaps as ObjectRecord[]; if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, - parentObjectRecords: deletedRecords, - //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, + parentObjectRecords: destroyedRecords, relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -86,47 +93,56 @@ export class CommonDestroyManyQueryRunnerService extends CommonBaseQueryRunnerSe }); } - return deletedRecords; + return destroyedRecords; } async computeArgs( args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } async processQueryResult( queryResult: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecordArray( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } async validate( args: CommonInput, - _queryRunnerContext: CommonBaseQueryRunnerContext, + queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { + const { flatObjectMetadata } = queryRunnerContext; + + assertMutationNotOnRemoteObject(flatObjectMetadata); + if (!isDefined(args.filter)) { throw new CommonQueryRunnerException( 'Filter is required', CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, ); } + + args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id)); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts index 9b8529c48ac..2d9c035f026 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-destroy-one-query-runner.service.ts @@ -19,7 +19,9 @@ import { CommonQueryNames, DestroyOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class CommonDestroyOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -65,14 +67,16 @@ export class CommonDestroyOneQueryRunnerService extends CommonBaseQueryRunnerSer async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts index b16c488dc89..3973f8a594a 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-duplicates-query-runner.service.ts @@ -25,7 +25,10 @@ import { import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunnerService< @@ -40,8 +43,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne ): Promise { const { repository, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, commonQueryParser, authContext, workspaceDataSource, @@ -49,7 +53,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne } = queryRunnerContext; const existingRecordsQueryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); let objectRecords: Partial[] = []; @@ -57,8 +61,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne const columnsToSelect = buildColumnsToSelect({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); if (isDefined(args.ids) && args.ids.length > 0) { @@ -76,7 +81,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne await Promise.all( objectRecords.map(async (record) => { const duplicateConditions = buildDuplicateConditions( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, [record], record.id, ); @@ -93,12 +100,12 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne } const duplicateRecordsQueryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( duplicateRecordsQueryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, duplicateConditions, ); @@ -132,8 +139,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: findDuplicatesOutput.flatMap( (item) => item.records, ), @@ -157,7 +165,13 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata, flatFieldMetadataMaps } = + queryRunnerContext; + + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); return { ...args, @@ -166,15 +180,17 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne this.queryRunnerArgsFactory.overrideValueByFieldMetadata( 'id', id, - objectMetadataItemWithFieldMaps.fieldsById, - objectMetadataItemWithFieldMaps, + fieldIdByName, + flatObjectMetadata, + flatFieldMetadataMaps, ), ) ?? [], ), data: await this.dataArgProcessor.process({ partialRecordInputs: args.data, authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, shouldBackfillPositionIfUndefined: false, }), }; @@ -182,8 +198,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne async processQueryResult( queryResult: CommonFindDuplicatesOutputItem[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { const processedResults = await Promise.all( @@ -192,8 +209,9 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne ...result, records: await this.commonResultGettersService.processRecordArray( result.records, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ), }; diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts index 38bba8c57d8..5105d9dd332 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-many-query-runner.service.ts @@ -30,7 +30,9 @@ import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-run import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; import { getCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -47,14 +49,15 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi repository, authContext, rolePermissionConfig, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceDataSource, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); const aggregateQueryBuilder = queryBuilder.clone(); @@ -63,7 +66,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi commonQueryParser.applyFilterToBuilder( aggregateQueryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, appliedFilters, ); @@ -85,7 +88,8 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi const cursorArgFilter = computeCursorArgFilter( cursor, orderByWithIdCondition, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, isForwardPagination, ); @@ -98,14 +102,14 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, appliedFilters, ); commonQueryParser.applyOrderToBuilder( queryBuilder, orderByWithIdCondition, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, isForwardPagination, ); @@ -114,7 +118,7 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi ProcessAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({ selectedAggregatedFields: args.selectedFieldsResult.aggregate, queryBuilder: aggregateQueryBuilder, - objectMetadataNameSingular: objectMetadataItemWithFieldMaps.nameSingular, + objectMetadataNameSingular: flatObjectMetadata.nameSingular, }); const limit = args.first ?? args.last ?? QUERY_MAX_RECORDS; @@ -122,8 +126,9 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi const columnsToSelect = buildColumnsToSelect({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); if (isDefined(args.offset)) { @@ -153,11 +158,11 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: objectRecords, parentObjectRecordsAggregatedValues, - //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -184,28 +189,31 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } async processQueryResult( queryResult: CommonFindManyOutput, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { const processedRecords = await this.commonResultGettersService.processRecordArray( queryResult.records, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts index 3cbd1100f7d..c9aadc716fc 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-find-one-query-runner.service.ts @@ -22,7 +22,9 @@ import { FindOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -40,18 +42,19 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic authContext, rolePermissionConfig, workspaceDataSource, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, args.filter ?? ({} as ObjectRecordFilter), ); @@ -63,8 +66,9 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic const columnsToSelect = buildColumnsToSelect({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const objectRecord = await queryBuilder @@ -84,10 +88,10 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: objectRecords, - //TODO : Refacto-common - To fix when switching processNestedRelationsHelper to Common relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -107,27 +111,30 @@ export class CommonFindOneQueryRunnerService extends CommonBaseQueryRunnerServic args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts index 6f709a3b1d1..ffb3ac62f30 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-group-by-query-runner.service.ts @@ -44,9 +44,11 @@ import { parseGroupByArgs } from 'src/engine/api/graphql/graphql-query-runner/gr import { GroupByWithRecordsService } from 'src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service'; import { getGroupLimit } from 'src/engine/api/graphql/graphql-query-runner/group-by/utils/get-group-limit.util'; import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { ViewFilterGroupService } from 'src/engine/metadata-modules/view-filter-group/services/view-filter-group.service'; import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; @@ -77,12 +79,13 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic const { repository, commonQueryParser, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, } = queryRunnerContext; - const objectMetadataNameSingular = - objectMetadataItemWithFieldMaps.nameSingular; + const objectMetadataNameSingular = flatObjectMetadata.nameSingular; let queryBuilder = repository.createQueryBuilder( objectMetadataNameSingular, @@ -90,11 +93,12 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic const groupByFields = parseGroupByArgs( args, - objectMetadataItemWithFieldMaps, - queryRunnerContext.objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); - const objectAlias = getObjectAlias(objectMetadataItemWithFieldMaps); + const objectAlias = getObjectAlias(flatObjectMetadata); this.addJoinForGroupByOnRelationFields({ queryBuilder, @@ -108,7 +112,8 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic args, appliedFilters, queryBuilder, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, workspaceId: authContext.workspace.id, commonQueryParser, }); @@ -166,8 +171,9 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic async processQueryResult( queryResult: CommonGroupByOutputItem[], - _objectMetadataItemId: string, - _objectMetadataMaps: ObjectMetadataMaps, + _flatObjectMetadata: FlatObjectMetadata, + _flatObjectMetadataMaps: FlatEntityMaps, + _flatFieldMetadataMaps: FlatEntityMaps, _authContext: WorkspaceAuthContext, ): Promise { return queryResult; @@ -175,12 +181,14 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic private async addFiltersFromView({ args, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, appliedFilters, workspaceId, }: { args: GroupByQueryArgs; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; appliedFilters: ObjectRecordFilter; workspaceId: string; }): Promise { @@ -197,14 +205,21 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic ); const recordFilters = viewFilters.map((viewFilter) => { - const fieldMetadataItem = - objectMetadataItemWithFieldMaps.fieldsById[viewFilter.fieldMetadataId]; + const fieldMetadata = + flatFieldMetadataMaps.byId[viewFilter.fieldMetadataId]; + + if (!fieldMetadata) { + throw new CommonQueryRunnerException( + `Field metadata not found for field ${viewFilter.fieldMetadataId}`, + CommonQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR, + ); + } return { id: viewFilter.id, fieldMetadataId: viewFilter.fieldMetadataId, value: convertViewFilterValueToString(viewFilter.value), - type: getFilterTypeFromFieldType(fieldMetadataItem.type), + type: getFilterTypeFromFieldType(fieldMetadata.type), operand: viewFilter.operand, recordFilterGroupId: viewFilter.viewFilterGroupId, positionInRecordFilterGroup: viewFilter.positionInViewFilterGroup, @@ -221,8 +236,9 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic }; }); - const fields = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, + const fields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ).map((field) => ({ id: field.id, name: field.name, @@ -263,24 +279,26 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic args, appliedFilters, queryBuilder, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, workspaceId, commonQueryParser, }: { args: GroupByQueryArgs; appliedFilters: ObjectRecordFilter; queryBuilder: WorkspaceSelectQueryBuilder; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; workspaceId: string; commonQueryParser: GraphqlQueryParser; }): Promise { - const objectMetadataNameSingular = - objectMetadataItemWithFieldMaps.nameSingular; + const objectMetadataNameSingular = flatObjectMetadata.nameSingular; if (args.viewId) { appliedFilters = await this.addFiltersFromView({ args, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, appliedFilters, workspaceId, }); @@ -336,7 +354,7 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic if ( !groupByField.fieldMetadata.settings || - !isFieldMetadataRelationOrMorphRelation(groupByField.fieldMetadata) + !isMorphOrRelationFlatFieldMetadata(groupByField.fieldMetadata) ) { throw new CommonQueryRunnerException( `Field metadata settings are missing or invalid for field ${groupByField.fieldMetadata.name}`, @@ -370,13 +388,14 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts index 72b839353ad..d8e7f2415fd 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts @@ -33,10 +33,12 @@ import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runne import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util'; import { mergeFieldValues } from 'src/engine/api/graphql/graphql-query-runner/utils/merge-field-values.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; @Injectable() export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -50,8 +52,11 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonExtendedInput, queryRunnerContext: CommonExtendedQueryRunnerContext, ): Promise { - const { objectMetadataMaps, objectMetadataItemWithFieldMaps } = - queryRunnerContext; + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, + } = queryRunnerContext; const recordsToMerge = await this.fetchRecordsToMerge( queryRunnerContext, @@ -67,7 +72,8 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ const mergedData = this.performDeepMerge( recordsToMerge, priorityRecord.id, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, args.dryRun ?? false, ); @@ -84,14 +90,15 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ ); const queryBuilder = queryRunnerContext.repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); await queryBuilder @@ -123,8 +130,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ const columnsToSelect = buildColumnsToSelect({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps: context.objectMetadataItemWithFieldMaps, - objectMetadataMaps: context.objectMetadataMaps, + flatObjectMetadata: context.flatObjectMetadata, + flatObjectMetadataMaps: context.flatObjectMetadataMaps, + flatFieldMetadataMaps: context.flatFieldMetadataMaps, }); const recordsToMerge = await context.repository.find({ @@ -141,8 +149,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ if (args.dryRun && args.selectedFieldsResult.relations) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps: context.objectMetadataMaps, - parentObjectMetadataItem: context.objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps: context.flatObjectMetadataMaps, + flatFieldMetadataMaps: context.flatFieldMetadataMaps, + parentObjectMetadataItem: context.flatObjectMetadata, parentObjectRecords: recordsToMerge as ObjectRecord[], relations: args.selectedFieldsResult.relations as Record< string, @@ -182,7 +191,8 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ private performDeepMerge( recordsToMerge: ObjectRecord[], priorityRecordId: string, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, isDryRun = false, ): Partial { const mergedResult: Partial = {}; @@ -194,7 +204,8 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ if ( !this.shouldExcludeFieldFromMerge( fieldName, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ) ) { allFieldNames.add(fieldName); @@ -218,9 +229,12 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ } else if (recordsWithValues.length === 1) { mergedResult[fieldName] = recordsWithValues[0].value; } else { - const fieldMetadata = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, - ).find((field) => field?.name === fieldName); + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const fieldMetadata = + flatFieldMetadataMaps.byId[fieldIdByName[fieldName]]; if (!fieldMetadata) { return; @@ -247,11 +261,14 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ private shouldExcludeFieldFromMerge( fieldName: string, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): boolean { - const fieldMetadata = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, - ).find((field) => field?.name === fieldName); + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const fieldMetadata = flatFieldMetadataMaps.byId[fieldIdByName[fieldName]]; return fieldMetadata?.isSystem ?? false; } @@ -276,18 +293,23 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ priorityRecordId: string, mergedData: Partial, ): Promise { - const { objectMetadataItemWithFieldMaps, objectMetadataMaps, repository } = - queryRunnerContext; + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + repository, + } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const updatedObjectRecords = await queryBuilder @@ -314,46 +336,54 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ fromIds: string[], toId: string, ): Promise { - const { objectMetadataMaps, objectMetadataItemWithFieldMaps } = context; + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, + } = context; - const relationFieldsPointingToCurrentObject = Object.values( - objectMetadataMaps.byId, - ) - .filter(isDefined) - .flatMap((metadata) => { - const relationFields = Object.values(metadata.fieldsById) - .filter(isDefined) - .filter((field) => - isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION), - ) - .filter( - (field) => - field.relationTargetObjectMetadataId === - objectMetadataItemWithFieldMaps.id && field.isActive, - ); + const relationFieldsPointingToCurrentObject: Array<{ + objectMetadata: FlatObjectMetadata; + fieldName: string; + fieldId: string; + joinColumnName: string | undefined; + }> = []; - return relationFields - .filter((field) => { - const relationSettings = - field.settings as FieldMetadataRelationSettings; + for (const field of Object.values(flatFieldMetadataMaps.byId).filter( + isDefined, + )) { + if ( + !isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION) || + field.relationTargetObjectMetadataId !== flatObjectMetadata.id || + !field.isActive + ) { + continue; + } - return ( - relationSettings?.relationType === RelationType.MANY_TO_ONE && - relationSettings?.joinColumnName - ); - }) - .map((field) => { - const relationSettings = - field.settings as FieldMetadataRelationSettings; + const relationSettings = field.settings as + | FieldMetadataRelationSettings + | undefined; - return { - objectMetadata: metadata, - fieldName: field.name, - fieldId: field.id, - joinColumnName: relationSettings.joinColumnName, - }; - }); + if ( + relationSettings?.relationType !== RelationType.MANY_TO_ONE || + !relationSettings?.joinColumnName + ) { + continue; + } + + const objMetadata = flatObjectMetadataMaps.byId[field.objectMetadataId]; + + if (!objMetadata) { + continue; + } + + relationFieldsPointingToCurrentObject.push({ + objectMetadata: objMetadata, + fieldName: field.name, + fieldId: field.id, + joinColumnName: relationSettings.joinColumnName, }); + } for (const relationField of relationFieldsPointingToCurrentObject) { if (!relationField.joinColumnName) { @@ -396,8 +426,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ updatedRecords: ObjectRecord[]; }): Promise { const { - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, authContext, workspaceDataSource, rolePermissionConfig, @@ -405,8 +436,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ if (args.selectedFieldsResult.relations) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: updatedRecords, relations: args.selectedFieldsResult.relations as Record< string, @@ -430,8 +462,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ async processQueryResult( queryResult: ObjectRecord, - _objectMetadataItemId: string, - _objectMetadataMaps: ObjectMetadataMaps, + _flatObjectMetadata: FlatObjectMetadata, + _flatObjectMetadataMaps: FlatEntityMaps, + _flatFieldMetadataMaps: FlatEntityMaps, _authContext: WorkspaceAuthContext, ): Promise { return queryResult; @@ -441,13 +474,13 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonExtendedQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); - if (!isDefined(objectMetadataItemWithFieldMaps.duplicateCriteria)) { + if (!isDefined(flatObjectMetadata.duplicateCriteria)) { throw new CommonQueryRunnerException( - `Merge is only available for objects with duplicate criteria. Object '${objectMetadataItemWithFieldMaps.nameSingular}' does not have duplicate criteria defined.`, + `Merge is only available for objects with duplicate criteria. Object '${flatObjectMetadata.nameSingular}' does not have duplicate criteria defined.`, CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts index 52045178e3b..444c76811e8 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-many-query-runner.service.ts @@ -22,8 +22,10 @@ import { } from 'src/engine/api/common/types/common-query-args.type'; import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -41,26 +43,28 @@ export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerSe authContext, rolePermissionConfig, workspaceDataSource, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, args.filter, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const restoredObjectRecords = await queryBuilder @@ -73,10 +77,10 @@ export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerSe if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: restoredRecords, - //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -96,27 +100,30 @@ export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerSe args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata, flatFieldMetadataMaps } = queryRunnerContext; return { ...args, filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( args.filter, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ), }; } async processQueryResult( queryResult: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecordArray( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } @@ -125,9 +132,9 @@ export class CommonRestoreManyQueryRunnerService extends CommonBaseQueryRunnerSe args: RestoreManyQueryArgs, queryRunnerContext: CommonBaseQueryRunnerContext, ) { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); if (!isDefined(args.filter)) { throw new CommonQueryRunnerException( 'Filter is required', diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts index 61332332d0b..93eab1448d9 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-restore-one-query-runner.service.ts @@ -20,8 +20,10 @@ import { RestoreOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonRestoreOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -67,14 +69,16 @@ export class CommonRestoreOneQueryRunnerService extends CommonBaseQueryRunnerSer async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } @@ -83,9 +87,9 @@ export class CommonRestoreOneQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); assertIsValidUuid(args.id); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts index 60df7d4f670..736c255abb2 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-many-query-runner.service.ts @@ -22,8 +22,10 @@ import { } from 'src/engine/api/common/types/common-query-args.type'; import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerService< @@ -41,26 +43,28 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer authContext, rolePermissionConfig, workspaceDataSource, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, commonQueryParser, } = queryRunnerContext; const queryBuilder = repository.createQueryBuilder( - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); commonQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, args.filter, ); const columnsToReturn = buildColumnsToReturn({ select: args.selectedFieldsResult.select, relations: args.selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const updatedObjectRecords = await queryBuilder @@ -73,10 +77,10 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer if (isDefined(args.selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: updatedRecords, - //TODO : Refacto-common - Typing to fix when switching processNestedRelationsHelper to Common relations: args.selectedFieldsResult.relations as Record< string, FindOptionsRelations @@ -96,20 +100,22 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata, flatFieldMetadataMaps } = + queryRunnerContext; return { ...args, - filter: - this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( - args.filter, - objectMetadataItemWithFieldMaps, - ) || {}, + filter: this.queryRunnerArgsFactory.overrideFilterByFieldMetadata( + args.filter, + flatObjectMetadata, + flatFieldMetadataMaps, + ), data: ( await this.dataArgProcessor.process({ partialRecordInputs: [args.data], authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, shouldBackfillPositionIfUndefined: false, }) )[0], @@ -120,9 +126,9 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); if (!args.filter) { throw new CommonQueryRunnerException( 'Filter is required', @@ -135,14 +141,16 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer async processQueryResult( queryResult: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return await this.commonResultGettersService.processRecordArray( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts index c899bd641be..b564d0ac8c8 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-update-one-query-runner.service.ts @@ -19,8 +19,10 @@ import { UpdateOneQueryArgs, } from 'src/engine/api/common/types/common-query-args.type'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Injectable() export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerService< @@ -60,7 +62,8 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise> { - const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { authContext, flatObjectMetadata, flatFieldMetadataMaps } = + queryRunnerContext; return { ...args, @@ -68,7 +71,8 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ await this.dataArgProcessor.process({ partialRecordInputs: [args.data], authContext, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, shouldBackfillPositionIfUndefined: false, }) )[0], @@ -77,14 +81,16 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ async processQueryResult( queryResult: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, authContext: WorkspaceAuthContext, ): Promise { return this.commonResultGettersService.processRecord( queryResult, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ); } @@ -93,9 +99,9 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ args: CommonInput, queryRunnerContext: CommonBaseQueryRunnerContext, ): Promise { - const { objectMetadataItemWithFieldMaps } = queryRunnerContext; + const { flatObjectMetadata } = queryRunnerContext; - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); + assertMutationNotOnRemoteObject(flatObjectMetadata); assertIsValidUuid(args.id); } } diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/get-object-alias-for-group-by.util.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/get-object-alias-for-group-by.util.ts index 36fa83fdd3c..f9372dfe359 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/utils/get-object-alias-for-group-by.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/utils/get-object-alias-for-group-by.util.ts @@ -1,7 +1,7 @@ -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const getObjectAlias = ( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, ): string => { - return objectMetadataItemWithFieldMaps.nameSingular; + return flatObjectMetadata.nameSingular; }; diff --git a/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts index 1cb419b042e..1d598102025 100644 --- a/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-result-getters/common-result-getters.service.ts @@ -15,8 +15,12 @@ import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/works import { PersonQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler'; import { WorkspaceMemberQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/workspace-member-query-result-getter.handler'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; // TODO: find a way to prevent conflict between handlers executing logic on object relations // And this factory that is also executing logic on object relations @@ -45,8 +49,9 @@ export class CommonResultGettersService { public async processRecordArray( recordArray: ObjectRecord[], - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, workspaceId: string, ) { return await Promise.all( @@ -54,8 +59,9 @@ export class CommonResultGettersService { async (record: ObjectRecord) => await this.processRecord( record, - objectMetadataItemId, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceId, ), ), @@ -64,28 +70,26 @@ export class CommonResultGettersService { public async processRecord( record: ObjectRecord, - objectMetadataItemId: string, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, workspaceId: string, ): Promise { - const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId]; + const handler = this.getHandler(flatObjectMetadata.nameSingular); - if (!isDefined(objectMetadataMapItem)) { - throw new Error('Object metadata map item is not defined'); - } - - const handler = this.getHandler(objectMetadataMapItem.nameSingular); + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); const relationFields = Object.keys(record) .map( (recordFieldName) => - objectMetadataMapItem.fieldsById[ - objectMetadataMapItem.fieldIdByName[recordFieldName] - ], + flatFieldMetadataMaps.byId[fieldIdByName[recordFieldName]], ) .filter(isDefined) .filter((fieldMetadata) => - isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION), + isFlatFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION), ); const relationFieldsProcessedMap = {} as Record< @@ -104,18 +108,26 @@ export class CommonResultGettersService { continue; } + const targetFlatObjectMetadata = + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: relationField.relationTargetObjectMetadataId, + flatEntityMaps: flatObjectMetadataMaps, + }); + relationFieldsProcessedMap[relationField.name] = relationField.settings?.relationType === RelationType.ONE_TO_MANY ? await this.processRecordArray( record[relationField.name], - relationField.relationTargetObjectMetadataId, - objectMetadataMaps, + targetFlatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceId, ) : await this.processRecord( record[relationField.name], - relationField.relationTargetObjectMetadataId, - objectMetadataMaps, + targetFlatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceId, ); } diff --git a/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts b/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts index d6c5e1e5e5a..b265bf45355 100644 --- a/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts +++ b/packages/twenty-server/src/engine/api/common/types/common-base-query-runner-context.type.ts @@ -1,9 +1,12 @@ import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export type CommonBaseQueryRunnerContext = { authContext: AuthContext; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts index b2297d08c4e..0ab4cdf36ad 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts @@ -1,24 +1,151 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419'; +const objectMetadataId = 'person-object-id'; -export const mockPersonObjectMetadataWithFieldMaps = ( +const mockFieldMetadatas: FlatFieldMetadata[] = [ + { + id: 'name-id', + type: FieldMetadataType.FULL_NAME, + name: 'name', + label: 'Name', + defaultValue: { + lastName: "''", + firstName: "''", + }, + objectMetadataId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'name-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'emails-id', + type: FieldMetadataType.EMAILS, + name: 'emails', + label: 'Emails', + defaultValue: { + primaryEmail: "''", + additionalEmails: null, + }, + objectMetadataId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'emails-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'linkedinLink-id', + type: FieldMetadataType.LINKS, + name: 'linkedinLink', + label: 'Linkedin', + defaultValue: { + primaryLinkUrl: "''", + secondaryLinks: [], + primaryLinkLabel: "''", + }, + objectMetadataId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'linkedinLink-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'jobTitle-id', + type: FieldMetadataType.TEXT, + name: 'jobTitle', + label: 'Job Title', + defaultValue: "''", + objectMetadataId, + isNullable: false, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'jobTitle-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, +]; + +export const mockPersonFlatFieldMetadataMaps = + (): FlatEntityMaps => ({ + byId: mockFieldMetadatas.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: mockFieldMetadatas.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); + +export const mockPersonFlatObjectMetadataMaps = ( duplicateCriteria: WorkspaceEntityDuplicateCriteria[], -) => - getMockObjectMetadataItemWithFieldsMaps({ - id: '', +): FlatEntityMaps => { + const flatObjectMetadata = mockPersonFlatObjectMetadata(duplicateCriteria); + + return { + byId: { + [flatObjectMetadata.id]: flatObjectMetadata, + }, + idByUniversalIdentifier: { + [flatObjectMetadata.universalIdentifier as string]: flatObjectMetadata.id, + }, + universalIdentifiersByApplicationId: {}, + }; +}; + +export const mockPersonFlatObjectMetadata = ( + duplicateCriteria: WorkspaceEntityDuplicateCriteria[], +): FlatObjectMetadata => + ({ + id: objectMetadataId, icon: 'Icon123', standardId: '', nameSingular: 'person', namePlural: 'people', labelSingular: 'Person', labelPlural: 'People', - description: 'A person', - targetTableName: 'DEPRECATED', + targetTableName: 'person', isCustom: false, isRemote: false, isActive: true, @@ -29,87 +156,16 @@ export const mockPersonObjectMetadataWithFieldMaps = ( labelIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '', workspaceId, - indexMetadatas: [], - fieldIdByName: { - name: 'name-id', - emails: 'emails-id', - linkedinLink: 'linkedinLink-id', - jobTitle: 'jobTitle-id', - }, - fieldIdByJoinColumnName: {}, - fieldsById: { - 'name-id': getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '', - id: 'name-id', - type: FieldMetadataType.FULL_NAME, - name: 'name', - label: 'Name', - defaultValue: { - lastName: "''", - firstName: "''", - }, - description: "Contact's name", - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - 'emails-id': getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '', - id: 'emails-id', - type: FieldMetadataType.EMAILS, - name: 'emails', - label: 'Emails', - defaultValue: { - primaryEmail: "''", - additionalEmails: null, - }, - description: "Contact's Emails", - isCustom: false, - isNullable: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - 'linkedinLink-id': getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '', - id: 'linkedinLink-id', - type: FieldMetadataType.LINKS, - name: 'linkedinLink', - label: 'Linkedin', - defaultValue: { - primaryLinkUrl: "''", - secondaryLinks: [], - primaryLinkLabel: "''", - }, - description: "Contact's Linkedin account", - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - 'jobTitle-id': getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '', - id: 'jobTitle-id', - type: FieldMetadataType.TEXT, - name: 'jobTitle', - label: 'Job Title', - defaultValue: "''", - description: "Contact's job title", - isCustom: false, - isNullable: false, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - }, - }); + universalIdentifier: objectMetadataId, + indexMetadataIds: [], + fieldMetadataIds: mockFieldMetadatas.map((field) => field.id), + viewIds: [], + applicationId: null, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + shortcut: null, + description: null, + standardOverrides: null, + isUIReadOnly: false, + }) as FlatObjectMetadata; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts index d421fdf2a2c..928acec19b7 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts @@ -2,19 +2,25 @@ import { Brackets, NotBrackets, type WhereExpressionBuilder } from 'typeorm'; import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { GraphqlQueryFilterFieldParser } from './graphql-query-filter-field.parser'; export class GraphqlQueryFilterConditionParser { - private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + private flatObjectMetadata: FlatObjectMetadata; private queryFilterFieldParser: GraphqlQueryFilterFieldParser; - constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) { - this.objectMetadataMapItem = objectMetadataMapItem; + constructor( + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, + ) { + this.flatObjectMetadata = flatObjectMetadata; this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser( - this.objectMetadataMapItem, + this.flatObjectMetadata, + flatFieldMetadataMaps, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts index afe54f1b371..a595b0e5daf 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts @@ -1,5 +1,5 @@ import { compositeTypeDefinitions } from 'twenty-shared/types'; -import { capitalize } from 'twenty-shared/utils'; +import { capitalize, isDefined } from 'twenty-shared/utils'; import { type WhereExpressionBuilder } from 'typeorm'; import { @@ -7,18 +7,35 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { computeWhereConditionParts } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-where-condition-parts'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; const ARRAY_OPERATORS = ['in', 'contains', 'notContains']; export class GraphqlQueryFilterFieldParser { - private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + private flatObjectMetadata: FlatObjectMetadata; + private flatFieldMetadataMaps: FlatEntityMaps; + private fieldIdByName: Record; + private fieldIdByJoinColumnName: Record; - constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) { - this.objectMetadataMapItem = objectMetadataMapItem; + constructor( + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, + ) { + this.flatObjectMetadata = flatObjectMetadata; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; + + const fieldMaps = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + this.fieldIdByName = fieldMaps.fieldIdByName; + this.fieldIdByJoinColumnName = fieldMaps.fieldIdByJoinColumnName; } public parse( @@ -30,13 +47,11 @@ export class GraphqlQueryFilterFieldParser { isFirst = false, ): void { const fieldMetadataId = - this.objectMetadataMapItem.fieldIdByName[`${key}`] || - this.objectMetadataMapItem.fieldIdByJoinColumnName[`${key}`]; + this.fieldIdByName[`${key}`] || this.fieldIdByJoinColumnName[`${key}`]; - const fieldMetadata = - this.objectMetadataMapItem.fieldsById[fieldMetadataId]; + const fieldMetadata = this.flatFieldMetadataMaps.byId[fieldMetadataId]; - if (!fieldMetadata) { + if (!isDefined(fieldMetadata)) { throw new Error(`Field metadata not found for field: ${key}`); } @@ -77,7 +92,7 @@ export class GraphqlQueryFilterFieldParser { private parseCompositeFieldForFilter( queryBuilder: WhereExpressionBuilder, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, objectNameSingular: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any fieldValue: any, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts index 683bddff33a..1404a2bf80e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts @@ -32,12 +32,13 @@ import { type AggregationField, getAvailableAggregationsFromObjectFields, } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { formatColumnNameForRelationField } from 'src/engine/twenty-orm/utils/format-column-name-for-relation-field.util'; import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util'; @@ -47,15 +48,28 @@ export type OrderByCondition = { }; export class GraphqlQueryOrderFieldParser { - private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - private objectMetadataMaps?: ObjectMetadataMaps; + private flatObjectMetadata: FlatObjectMetadata; + private flatObjectMetadataMaps: FlatEntityMaps; + private flatFieldMetadataMaps: FlatEntityMaps; + private fieldIdByName: Record; + private fieldIdByJoinColumnName: Record; constructor( - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps?: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ) { - this.objectMetadataMapItem = objectMetadataMapItem; - this.objectMetadataMaps = objectMetadataMaps; + this.flatObjectMetadata = flatObjectMetadata; + this.flatObjectMetadataMaps = flatObjectMetadataMaps; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; + + const fieldMaps = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + this.fieldIdByName = fieldMaps.fieldIdByName; + this.fieldIdByJoinColumnName = fieldMaps.fieldIdByJoinColumnName; } parse( @@ -67,10 +81,10 @@ export class GraphqlQueryOrderFieldParser { (acc, item) => { Object.entries(item).forEach(([fieldName, orderByDirection]) => { const fieldMetadataId = - this.objectMetadataMapItem.fieldIdByName[fieldName] || - this.objectMetadataMapItem.fieldIdByJoinColumnName[fieldName]; + this.fieldIdByName[fieldName] || + this.fieldIdByJoinColumnName[fieldName]; const fieldMetadata = - this.objectMetadataMapItem.fieldsById[fieldMetadataId]; + this.flatFieldMetadataMaps.byId[fieldMetadataId]; if (!fieldMetadata || orderByDirection === undefined) { throw new GraphqlQueryRunnerException( @@ -92,9 +106,7 @@ export class GraphqlQueryOrderFieldParser { const orderByCasting = this.getOptionalOrderByCasting(fieldMetadata); - const columnName = isFieldMetadataRelationOrMorphRelation( - fieldMetadata, - ) + const columnName = isMorphOrRelationFlatFieldMetadata(fieldMetadata) ? formatColumnNameForRelationField( fieldMetadata.name, fieldMetadata.settings, @@ -124,17 +136,19 @@ export class GraphqlQueryOrderFieldParser { }): Record[] { let parsedOrderBy: Record[] = []; + const fields = this.flatObjectMetadata.fieldMetadataIds + .map((id) => this.flatFieldMetadataMaps.byId[id]) + .filter(isDefined); + const availableAggregations: Record = - getAvailableAggregationsFromObjectFields( - Object.values(this.objectMetadataMapItem.fieldsById), - ); + getAvailableAggregationsFromObjectFields(fields); for (const orderByArg of orderBy) { if (this.isAggregateOrderByArg(orderByArg)) { const parsedAggregateOrderBy = this.parseAggregateOrderByArg( availableAggregations, orderByArg, - this.objectMetadataMapItem, + this.flatObjectMetadata, ); parsedOrderBy.push(parsedAggregateOrderBy); @@ -148,10 +162,8 @@ export class GraphqlQueryOrderFieldParser { } const fieldName = Object.keys(orderByArg)[0]; - const fieldMetadataId = - this.objectMetadataMapItem.fieldIdByName[fieldName]; - const fieldMetadata = - this.objectMetadataMapItem.fieldsById[fieldMetadataId]; + const fieldMetadataId = this.fieldIdByName[fieldName]; + const fieldMetadata = this.flatFieldMetadataMaps.byId[fieldMetadataId]; if (!isDefined(fieldMetadata)) { throw new UserInputError(`Cannot orderBy unknown field: ${fieldName}.`); @@ -162,7 +174,7 @@ export class GraphqlQueryOrderFieldParser { this.parseObjectRecordOrderByForScalarField({ groupByFields, orderByArg, - objectMetadataItemWithFieldMaps: this.objectMetadataMapItem, + flatObjectMetadata: this.flatObjectMetadata, fieldMetadata, }); @@ -218,7 +230,7 @@ export class GraphqlQueryOrderFieldParser { this.parseObjectRecordOrderByForCompositeField({ groupByFields, orderByArg, - objectMetadataItemWithFieldMaps: this.objectMetadataMapItem, + flatObjectMetadata: this.flatObjectMetadata, fieldMetadata, }); @@ -237,7 +249,7 @@ export class GraphqlQueryOrderFieldParser { } getOptionalOrderByCasting( - fieldMetadata: Pick, + fieldMetadata: Pick, ): string { if ( fieldMetadata.type === FieldMetadataType.SELECT || @@ -379,7 +391,7 @@ export class GraphqlQueryOrderFieldParser { parseAggregateOrderByArg = ( availableAggregations: Record, orderByArg: AggregateOrderByWithGroupByField, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, ): Record => { const aggregate = orderByArg.aggregate; @@ -399,7 +411,7 @@ export class GraphqlQueryOrderFieldParser { const aggregateExpression = ProcessAggregateHelper.getAggregateExpression( aggregateField, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); if (!isDefined(aggregateExpression)) { @@ -419,13 +431,13 @@ export class GraphqlQueryOrderFieldParser { parseObjectRecordOrderByForScalarField = ({ groupByFields, orderByArg, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, fieldMetadata, }: { groupByFields: GroupByField[]; orderByArg: ObjectRecordOrderByForScalarField; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - fieldMetadata: FieldMetadataEntity; + flatObjectMetadata: FlatObjectMetadata; + fieldMetadata: FlatFieldMetadata; }): Record | null => { const fieldIsInGroupBy = groupByFields.some( (groupByField) => groupByField.fieldMetadata.id === fieldMetadata.id, @@ -445,7 +457,7 @@ export class GraphqlQueryOrderFieldParser { } return { - [`"${objectMetadataItemWithFieldMaps.nameSingular}"."${fieldMetadata.name}"${orderByCasting}`]: + [`"${flatObjectMetadata.nameSingular}"."${fieldMetadata.name}"${orderByCasting}`]: convertOrderByToFindOptionsOrder(orderByDirection), }; }; @@ -453,13 +465,13 @@ export class GraphqlQueryOrderFieldParser { parseObjectRecordOrderByForCompositeField = ({ groupByFields, orderByArg, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, fieldMetadata, }: { groupByFields: GroupByField[]; orderByArg: ObjectRecordOrderByForCompositeField; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - fieldMetadata: FieldMetadataEntity; + flatObjectMetadata: FlatObjectMetadata; + fieldMetadata: FlatFieldMetadata; }): Record | null => { const fieldName = Object.keys(orderByArg)[0]; const orderBySubField = orderByArg[fieldName]; @@ -491,7 +503,7 @@ export class GraphqlQueryOrderFieldParser { return parseCompositeFieldForOrder( fieldMetadata, orderBySubField, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); }; @@ -556,7 +568,7 @@ export class GraphqlQueryOrderFieldParser { }: { groupByFields: GroupByField[]; orderByArg: ObjectRecordOrderByForRelationField; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; }): Record | null => { const { associatedGroupByField, @@ -565,7 +577,8 @@ export class GraphqlQueryOrderFieldParser { } = prepareForOrderByRelationFieldParsing({ orderByArg, fieldMetadata, - objectMetadataMaps: this.objectMetadataMaps, + flatObjectMetadataMaps: this.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.flatFieldMetadataMaps, groupByFields, }); @@ -709,9 +722,9 @@ export class GraphqlQueryOrderFieldParser { | ObjectRecordOrderByForCompositeField | ObjectRecordOrderByWithGroupByDateField | ObjectRecordOrderByForRelationField, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ): orderByArg is ObjectRecordOrderByForRelationField => { - if (!isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (!isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { return false; } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util.ts index c57468e4221..be7def8deff 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util.ts @@ -6,11 +6,11 @@ import { capitalize } from 'twenty-shared/utils'; import { type OrderByCondition } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; import { convertOrderByToFindOptionsOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/convert-order-by-to-find-options-order'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; export const parseCompositeFieldForOrder = ( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, objectNameSingular: string, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util.ts index 75eb0d3c133..70e9538e0db 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util.ts @@ -8,26 +8,24 @@ import { type GroupByRelationField, } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types'; import { isGroupByRelationField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-relation-field.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const prepareForOrderByRelationFieldParsing = ({ orderByArg, fieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, groupByFields, }: { orderByArg: ObjectRecordOrderByForRelationField; - fieldMetadata: FieldMetadataEntity; - objectMetadataMaps?: ObjectMetadataMaps; + fieldMetadata: FlatFieldMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; groupByFields: GroupByField[]; }) => { - if (!objectMetadataMaps) { - throw new UserInputError( - `Object metadata maps not found for field ${fieldMetadata.name}`, - ); - } - const relationFieldName = Object.keys(orderByArg)[0]; const nestedFieldOrderByObject = orderByArg[relationFieldName]; @@ -57,17 +55,22 @@ export const prepareForOrderByRelationFieldParsing = ({ ); } - const targetObjectMetadataItem = - objectMetadataMaps?.byId[fieldMetadata.relationTargetObjectMetadataId]; + const targetObjectMetadata = + flatObjectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; - if (!isDefined(targetObjectMetadataItem)) { + if (!isDefined(targetObjectMetadata)) { throw new UserInputError( `Target object metadata item not found for field ${fieldMetadata.name}`, ); } - const nestedFieldMetadataId = - targetObjectMetadataItem?.fieldIdByName[nestedFieldName]; + const { fieldIdByName: targetFieldIdByName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + targetObjectMetadata, + ); + + const nestedFieldMetadataId = targetFieldIdByName[nestedFieldName]; if (!isDefined(nestedFieldMetadataId)) { throw new UserInputError( @@ -75,12 +78,11 @@ export const prepareForOrderByRelationFieldParsing = ({ ); } - const nestedFieldMetadata = - targetObjectMetadataItem?.fieldsById[nestedFieldMetadataId]; + const nestedFieldMetadata = flatFieldMetadataMaps.byId[nestedFieldMetadataId]; if (!isDefined(nestedFieldMetadata) || !isDefined(nestedFieldMetadataId)) { throw new UserInputError( - `Nested field "${nestedFieldName}" not found in target object "${targetObjectMetadataItem.nameSingular}"`, + `Nested field "${nestedFieldName}" not found in target object "${targetObjectMetadata.nameSingular}"`, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts index 6a57bc46582..f3fb4c620f1 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser.ts @@ -1,21 +1,28 @@ +import { isDefined } from 'twenty-shared/utils'; + import { type GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { type AggregationField, getAvailableAggregationsFromObjectFields, } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export class GraphqlQuerySelectedFieldsAggregateParser { parse( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, accumulator: GraphqlQuerySelectedFieldsResult, ): void { + const fields = flatObjectMetadata.fieldMetadataIds + .map((id) => flatFieldMetadataMaps.byId[id]) + .filter(isDefined); + const availableAggregations: Record = - getAvailableAggregationsFromObjectFields( - Object.values(objectMetadataMapItem.fieldsById), - ); + getAvailableAggregationsFromObjectFields(fields); for (const selectedField of Object.keys(graphqlSelectedFields)) { const selectedAggregation = availableAggregations[selectedField]; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts index fa0e1cd1120..2b2b14b91e4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts @@ -1,20 +1,28 @@ +import { isDefined } from 'twenty-shared/utils'; + import { GraphqlQuerySelectedFieldsParser, type GraphqlQuerySelectedFieldsResult, } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; -import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export class GraphqlQuerySelectedFieldsRelationParser { - private objectMetadataMaps: ObjectMetadataMaps; + private flatObjectMetadataMaps: FlatEntityMaps; + private flatFieldMetadataMaps: FlatEntityMaps; - constructor(objectMetadataMaps: ObjectMetadataMaps) { - this.objectMetadataMaps = objectMetadataMaps; + constructor( + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + ) { + this.flatObjectMetadataMaps = flatObjectMetadataMaps; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; } parseRelationField( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, fieldKey: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any fieldValue: any, @@ -26,18 +34,24 @@ export class GraphqlQuerySelectedFieldsRelationParser { accumulator.relations[fieldKey] = true; - const targetObjectMetadata = getTargetObjectMetadataOrThrow( - fieldMetadata, - this.objectMetadataMaps, - ); + if (!isDefined(fieldMetadata.relationTargetObjectMetadataId)) { + throw new Error( + `Relation target object metadata id not found for field ${fieldMetadata.name}`, + ); + } + + const targetObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: fieldMetadata.relationTargetObjectMetadataId, + flatEntityMaps: this.flatObjectMetadataMaps, + }); const fieldParser = new GraphqlQuerySelectedFieldsParser( - this.objectMetadataMaps, + this.flatObjectMetadataMaps, + this.flatFieldMetadataMaps, ); const relationAccumulator = fieldParser.parse( fieldValue, targetObjectMetadata, - this.objectMetadataMaps, ); accumulator.select[fieldKey] = { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts index f832666b165..4f566194371 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts @@ -6,12 +6,13 @@ import { capitalize, isDefined } from 'twenty-shared/utils'; import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser'; import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export type GraphqlQuerySelectedFieldsResult = { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -25,18 +26,27 @@ export type GraphqlQuerySelectedFieldsResult = { export class GraphqlQuerySelectedFieldsParser { private graphqlQuerySelectedFieldsRelationParser: GraphqlQuerySelectedFieldsRelationParser; private aggregateParser: GraphqlQuerySelectedFieldsAggregateParser; + private flatObjectMetadataMaps: FlatEntityMaps; + private flatFieldMetadataMaps: FlatEntityMaps; - constructor(objectMetadataMaps: ObjectMetadataMaps) { + constructor( + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + ) { + this.flatObjectMetadataMaps = flatObjectMetadataMaps; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; this.graphqlQuerySelectedFieldsRelationParser = - new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMaps); + new GraphqlQuerySelectedFieldsRelationParser( + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); this.aggregateParser = new GraphqlQuerySelectedFieldsAggregateParser(); } parse( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, ): GraphqlQuerySelectedFieldsResult { const accumulator: GraphqlQuerySelectedFieldsResult = { select: {}, @@ -47,9 +57,8 @@ export class GraphqlQuerySelectedFieldsParser { if (this.isRootConnection(graphqlSelectedFields)) { this.parseConnectionField( graphqlSelectedFields, - objectMetadataMapItem, + flatObjectMetadata, accumulator, - objectMetadataMaps, ); return accumulator; @@ -57,15 +66,15 @@ export class GraphqlQuerySelectedFieldsParser { this.aggregateParser.parse( graphqlSelectedFields, - objectMetadataMapItem, + flatObjectMetadata, + this.flatFieldMetadataMaps, accumulator, ); this.parseRecordFields( graphqlSelectedFields, - objectMetadataMapItem, + flatObjectMetadata, accumulator, - objectMetadataMaps, ); return accumulator; @@ -74,15 +83,17 @@ export class GraphqlQuerySelectedFieldsParser { private parseRecordFields( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, accumulator: GraphqlQuerySelectedFieldsResult, - objectMetadataMaps: ObjectMetadataMaps, ): void { - for (const fieldMetadata of Object.values( - objectMetadataMapItem.fieldsById, - )) { + for (const fieldMetadataId of flatObjectMetadata.fieldMetadataIds) { + const fieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: fieldMetadataId, + flatEntityMaps: this.flatFieldMetadataMaps, + }); + if ( - isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION) + isFlatFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION) ) { const joinColumnName = fieldMetadata.settings?.joinColumnName; @@ -111,13 +122,15 @@ export class GraphqlQuerySelectedFieldsParser { } if ( - isFieldMetadataEntityOfType( + isFlatFieldMetadataOfType( fieldMetadata, FieldMetadataType.MORPH_RELATION, ) ) { const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + this.flatObjectMetadataMaps.byId[ + fieldMetadata.relationTargetObjectMetadataId + ]; if ( !fieldMetadata.settings?.relationType || @@ -182,24 +195,19 @@ export class GraphqlQuerySelectedFieldsParser { private parseConnectionField( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, accumulator: GraphqlQuerySelectedFieldsResult, - objectMetadataMaps: ObjectMetadataMaps, ): void { this.aggregateParser.parse( graphqlSelectedFields, - objectMetadataMapItem, + flatObjectMetadata, + this.flatFieldMetadataMaps, accumulator, ); const node = graphqlSelectedFields.edges.node; - this.parseRecordFields( - node, - objectMetadataMapItem, - accumulator, - objectMetadataMaps, - ); + this.parseRecordFields(node, flatObjectMetadata, accumulator); } private isRootConnection( @@ -210,7 +218,7 @@ export class GraphqlQuerySelectedFieldsParser { } private parseCompositeField( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, // eslint-disable-next-line @typescript-eslint/no-explicit-any fieldValue: any, // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts index 804f95ec6e8..62d70c022e7 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts @@ -6,10 +6,6 @@ import { type ObjectRecordOrderBy, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser'; import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; import { @@ -17,30 +13,35 @@ import { type GraphqlQuerySelectedFieldsResult, } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { type GroupByField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; export class GraphqlQueryParser { - private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - private objectMetadataMaps: ObjectMetadataMaps; + private flatObjectMetadata: FlatObjectMetadata; + private flatObjectMetadataMaps: FlatEntityMaps; + private flatFieldMetadataMaps: FlatEntityMaps; private filterConditionParser: GraphqlQueryFilterConditionParser; private orderFieldParser: GraphqlQueryOrderFieldParser; constructor( - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ) { - this.objectMetadataMapItem = objectMetadataMapItem; - this.objectMetadataMaps = objectMetadataMaps; + this.flatObjectMetadata = flatObjectMetadata; + this.flatObjectMetadataMaps = flatObjectMetadataMaps; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; this.filterConditionParser = new GraphqlQueryFilterConditionParser( - this.objectMetadataMapItem, + this.flatObjectMetadata, + this.flatFieldMetadataMaps, ); this.orderFieldParser = new GraphqlQueryOrderFieldParser( - this.objectMetadataMapItem, - this.objectMetadataMaps, + this.flatObjectMetadata, + this.flatObjectMetadataMaps, + this.flatFieldMetadataMaps, ); } @@ -143,26 +144,14 @@ export class GraphqlQueryParser { // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, ): GraphqlQuerySelectedFieldsResult { - const objectMetadataMapItem = getObjectMetadataMapItemByNameSingular( - this.objectMetadataMaps, - this.objectMetadataMapItem.nameSingular, - ); - - if (!objectMetadataMapItem) { - throw new GraphqlQueryRunnerException( - `Could not find object metadata for ${this.objectMetadataMapItem.nameSingular}`, - GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - const selectedFieldsParser = new GraphqlQuerySelectedFieldsParser( - this.objectMetadataMaps, + this.flatObjectMetadataMaps, + this.flatFieldMetadataMaps, ); return selectedFieldsParser.parse( graphqlSelectedFields, - this.objectMetadataMapItem, - this.objectMetadataMaps, + this.flatObjectMetadata, ); } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types.ts index 944953e6c4d..6d09f12c61a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types.ts @@ -1,21 +1,21 @@ import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types'; import { type FirstDayOfTheWeek } from 'twenty-shared/utils'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export type GroupByRegularField = { - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; subFieldName?: string; }; export type GroupByDateField = { - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; subFieldName?: string; dateGranularity: ObjectRecordGroupByDateGranularity; weekStartDay?: FirstDayOfTheWeek; }; export type GroupByRelationField = { - fieldMetadata: FieldMetadataEntity; - nestedFieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; + nestedFieldMetadata: FlatFieldMetadata; nestedSubFieldName?: string; dateGranularity?: ObjectRecordGroupByDateGranularity; weekStartDay?: FirstDayOfTheWeek; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util.ts index c5259f5edad..50b7445f708 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util.ts @@ -7,8 +7,9 @@ import { isDefined } from 'twenty-shared/utils'; import { type CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type'; import { type GroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-definition.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; export const formatResultWithGroupByDimensionValues = async ({ @@ -16,8 +17,9 @@ export const formatResultWithGroupByDimensionValues = async ({ groupByDefinitions, aggregateFieldNames, recordsResult, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, processRecord, }: { groupsResult: Record[]; @@ -25,8 +27,9 @@ export const formatResultWithGroupByDimensionValues = async ({ aggregateFieldNames: string[]; processRecord?: (record: ObjectRecord) => Promise; recordsResult?: Array>; - objectMetadataItemWithFieldMaps?: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps?: ObjectMetadataMaps; + flatObjectMetadata?: FlatObjectMetadata; + flatObjectMetadataMaps?: FlatEntityMaps; + flatFieldMetadataMaps?: FlatEntityMaps; }): Promise => { const formattedResult: CommonGroupByOutputItem[] = []; @@ -34,8 +37,9 @@ export const formatResultWithGroupByDimensionValues = async ({ if (isDefined(recordsResult)) { if ( - !isDefined(objectMetadataItemWithFieldMaps) || - !isDefined(objectMetadataMaps) + !isDefined(flatObjectMetadata) || + !isDefined(flatObjectMetadataMaps) || + !isDefined(flatFieldMetadataMaps) ) { throw new Error('Metadata are required to format result'); } @@ -55,8 +59,9 @@ export const formatResultWithGroupByDimensionValues = async ({ await processRecord( formatResult( record, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ), ), ), diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util.ts index 65e0c623527..6acbb14dd56 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util.ts @@ -1,5 +1,5 @@ -import { isDefined } from 'class-validator'; import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { type GroupByResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -7,19 +7,28 @@ import { type GroupByField } from 'src/engine/api/graphql/graphql-query-runner/g import { isGroupByDateFieldDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field-definition.util'; import { parseGroupByRelationField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util'; import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/validate-single-key-for-group-by-or-throw.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const parseGroupByArgs = ( args: GroupByResolverArgs, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ): GroupByField[] => { const groupByFieldNames = args.groupBy; const groupByFields: GroupByField[] = []; + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + for (const fieldNames of groupByFieldNames) { validateSingleKeyForGroupByOrThrow({ groupByKeys: Object.keys(fieldNames), @@ -29,17 +38,17 @@ export const parseGroupByArgs = ( for (const fieldName of Object.keys(fieldNames)) { const fieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName[fieldName] || - objectMetadataItemWithFieldMaps.fieldIdByJoinColumnName[fieldName]; - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + fieldIdByName[fieldName] || fieldIdByJoinColumnName[fieldName]; + const fieldMetadata = fieldMetadataId + ? flatFieldMetadataMaps.byId[fieldMetadataId] + : undefined; if (!isDefined(fieldMetadata) || !isDefined(fieldMetadataId)) { throw new Error(`Unidentified field in groupBy: ${fieldName}`); } const isGroupByRelationField = - isFieldMetadataRelationOrMorphRelation(fieldMetadata) && + isMorphOrRelationFlatFieldMetadata(fieldMetadata) && typeof fieldNames[fieldName] === 'object' && fieldNames[fieldName] !== null && !isGroupByDateFieldDefinition(fieldNames[fieldName]); @@ -50,7 +59,8 @@ export const parseGroupByArgs = ( fieldNames, fieldName, fieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, groupByFields, }); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts index bc1ed4326ba..832f1d72b7f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util.ts @@ -1,6 +1,6 @@ import { UserInputError } from 'apollo-server-core'; -import { isDefined } from 'class-validator'; import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { GraphqlQueryRunnerException, @@ -11,31 +11,41 @@ import { type DateFieldGroupByDefinition } from 'src/engine/api/graphql/graphql- import { type GroupByField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types'; import { isGroupByDateFieldDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field-definition.util'; import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/validate-single-key-for-group-by-or-throw.util'; -import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; const getNestedFieldMetadataDetails = ({ fieldNames, fieldName, fieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { fieldNames: Record; fieldName: string; - fieldMetadata: FieldMetadataEntity; - objectMetadataMaps: ObjectMetadataMaps; + fieldMetadata: FlatFieldMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }) => { const nestedFieldGroupByDefinitions = fieldNames[fieldName] as | Record | Record | Record; - const targetObjectMetadata = getTargetObjectMetadataOrThrow( - fieldMetadata, - objectMetadataMaps, - ); + if (!isDefined(fieldMetadata.relationTargetObjectMetadataId)) { + throw new UserInputError( + `Relation target object metadata id not found for field ${fieldMetadata.name}`, + ); + } + + const targetObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: fieldMetadata.relationTargetObjectMetadataId, + flatEntityMaps: flatObjectMetadataMaps, + }); const nestedFieldNames = Object.keys(nestedFieldGroupByDefinitions); @@ -46,10 +56,13 @@ const getNestedFieldMetadataDetails = ({ }); const nestedFieldName = nestedFieldNames[0]; - const nestedFieldMetadataId = - targetObjectMetadata.fieldIdByName[nestedFieldName]; - const nestedFieldMetadata = - targetObjectMetadata.fieldsById[nestedFieldMetadataId]; + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + targetObjectMetadata, + ); + + const nestedFieldMetadataId = fieldIdByName[nestedFieldName]; + const nestedFieldMetadata = flatFieldMetadataMaps.byId[nestedFieldMetadataId]; if (!isDefined(nestedFieldMetadata) || !isDefined(nestedFieldMetadataId)) { throw new GraphqlQueryRunnerException( @@ -83,8 +96,8 @@ const handleNestedCompositeField = ({ }: { nestedFieldGroupByDefinition: CompositeFieldGroupByDefinition; nestedFieldName: string; - fieldMetadata: FieldMetadataEntity; - nestedFieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; + nestedFieldMetadata: FlatFieldMetadata; groupByFields: GroupByField[]; }) => { if ( @@ -127,13 +140,15 @@ export const parseGroupByRelationField = ({ fieldNames, fieldName, fieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, groupByFields, }: { fieldNames: Record; fieldName: string; - fieldMetadata: FieldMetadataEntity; - objectMetadataMaps: ObjectMetadataMaps; + fieldMetadata: FlatFieldMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; groupByFields: GroupByField[]; }) => { const { nestedFieldGroupByDefinition, nestedFieldMetadata, nestedFieldName } = @@ -141,7 +156,8 @@ export const parseGroupByRelationField = ({ fieldNames, fieldName, fieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); // Handle date fields in nested relations @@ -150,8 +166,7 @@ export const parseGroupByRelationField = ({ nestedFieldMetadata.type === FieldMetadataType.DATE_TIME) && isGroupByDateFieldDefinition(nestedFieldGroupByDefinition) ) { - const dateFieldDefinition = - nestedFieldGroupByDefinition as DateFieldGroupByDefinition; + const dateFieldDefinition = nestedFieldGroupByDefinition; groupByFields.push({ fieldMetadata, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts index 312f914aeb9..3cff2bfa5f2 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service.ts @@ -18,8 +18,9 @@ import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/g import { getGroupLimit } from 'src/engine/api/graphql/graphql-query-runner/group-by/utils/get-group-limit.util'; import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; @@ -66,16 +67,18 @@ export class GroupByWithRecordsService { authContext, workspaceDataSource, rolePermissionConfig, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, repository, } = queryRunnerContext; const columnsToSelect = buildColumnsToSelect({ select: selectedFieldsResult.select, relations: selectedFieldsResult.relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps: objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const queryBuilderWithPartitionBy = this.addPartitionByToQueryBuilder({ @@ -85,16 +88,18 @@ export class GroupByWithRecordsService { groupByDefinitions, repository, orderByForRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); const recordsResult = await queryBuilderWithPartitionBy.getRawMany(); if (!isEmpty(selectedFieldsResult.relations)) { await this.processNestedRelationsHelper.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: objectMetadataItemWithFieldMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: flatObjectMetadata, parentObjectRecords: recordsResult.flatMap((group) => group.records), parentObjectRecordsAggregatedValues: {}, relations: selectedFieldsResult.relations, @@ -112,13 +117,15 @@ export class GroupByWithRecordsService { recordsResult, groupByDefinitions, aggregateFieldNames: Object.keys(selectedFieldsResult.aggregate), - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, processRecord: (record: ObjectRecord) => this.commonResultGettersService.processRecord( record, - objectMetadataItemWithFieldMaps.id, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext.workspace.id, ), }); @@ -131,8 +138,9 @@ export class GroupByWithRecordsService { groupByDefinitions, repository, orderByForRecords, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { queryBuilderForSubQuery: WorkspaceSelectQueryBuilder; columnsToSelect: Record; @@ -140,8 +148,9 @@ export class GroupByWithRecordsService { groupByDefinitions: GroupByDefinition[]; repository: WorkspaceRepository; orderByForRecords: ObjectRecordOrderBy; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }): WorkspaceSelectQueryBuilder { const groupByExpressions = groupByDefinitions .map((def) => def.expression) @@ -157,7 +166,7 @@ export class GroupByWithRecordsService { queryBuilderForSubQuery, ); - const objectAlias = getObjectAlias(objectMetadataItemWithFieldMaps); + const objectAlias = getObjectAlias(flatObjectMetadata); const recordSelectWithAlias = Object.keys(columnsToSelect) .map((col) => `"${objectAlias}"."${col}" as "${SUB_QUERY_PREFIX}${col}"`) @@ -175,14 +184,15 @@ export class GroupByWithRecordsService { if (!isEmpty(orderByForRecords)) { const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); graphqlQueryParser.applyOrderToBuilder( subQuery, orderByForRecords, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts index 8e1fcaadee9..35f825a5051 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts @@ -16,21 +16,29 @@ import { import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; -import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { isPlainObject } from 'src/utils/is-plain-object'; // TODO: Refacto-common - Rename CommonRecordsToGraphqlConnectionHelper export class ObjectRecordsToGraphqlConnectionHelper { - private objectMetadataMaps: ObjectMetadataMaps; + private flatObjectMetadataMaps: FlatEntityMaps; + private flatFieldMetadataMaps: FlatEntityMaps; + private objectIdByNameSingular: Record; - constructor(objectMetadataMaps: ObjectMetadataMaps) { - this.objectMetadataMaps = objectMetadataMaps; + constructor( + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + objectIdByNameSingular: Record, + ) { + this.flatObjectMetadataMaps = flatObjectMetadataMaps; + this.flatFieldMetadataMaps = flatFieldMetadataMaps; + this.objectIdByNameSingular = objectIdByNameSingular; } public createConnection({ @@ -154,22 +162,22 @@ export class ObjectRecordsToGraphqlConnectionHelper { ); } - const objectMetadata = getObjectMetadataMapItemByNameSingular( - this.objectMetadataMaps, - objectName, - ); + const objectMetadataId = this.objectIdByNameSingular[objectName]; - if (!objectMetadata) { - throw new GraphqlQueryRunnerException( - `Object metadata not found for ${objectName}`, - GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: objectMetadataId, + flatEntityMaps: this.flatObjectMetadataMaps, + }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const processedObjectRecord: Record = {}; - for (const fieldMetadata of Object.values(objectMetadata.fieldsById)) { + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const fieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: fieldId, + flatEntityMaps: this.flatFieldMetadataMaps, + }); + if (isCompositeFieldMetadataType(fieldMetadata.type)) { const objectValue = objectRecord[fieldMetadata.name]; @@ -183,75 +191,13 @@ export class ObjectRecordsToGraphqlConnectionHelper { continue; } - if (isFieldMetadataTypeRelation(fieldMetadata)) { - const fieldMetadataNameWithId = `${fieldMetadata.name}Id`; - - if (isDefined(objectRecord[fieldMetadataNameWithId])) { - processedObjectRecord[fieldMetadataNameWithId] = - objectRecord[fieldMetadataNameWithId]; - } - - const objectValue = objectRecord[fieldMetadata.name]; - - if (!isDefined(objectValue)) { - continue; - } - - if (Array.isArray(objectValue)) { - const targetObjectMetadata = getTargetObjectMetadataOrThrow( - fieldMetadata, - this.objectMetadataMaps, - ); - - processedObjectRecord[fieldMetadata.name] = this.createConnection({ - objectRecords: objectValue, - parentObjectRecord: objectRecord, - objectRecordsAggregatedValues: - objectRecordsAggregatedValues[fieldMetadata.name], - selectedAggregatedFields: - selectedAggregatedFields[fieldMetadata.name], - objectName: targetObjectMetadata.nameSingular, - take, - totalCount: - objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ?? - objectValue.length, - order, - hasNextPage: false, - hasPreviousPage: false, - depth: depth + 1, - }); - } else if (isPlainObject(objectValue)) { - const targetObjectMetadata = getTargetObjectMetadataOrThrow( - fieldMetadata, - this.objectMetadataMaps, - ); - - processedObjectRecord[fieldMetadata.name] = this.processRecord({ - objectRecord: objectValue, - objectRecordsAggregatedValues: - objectRecordsAggregatedValues[fieldMetadata.name], - selectedAggregatedFields: - selectedAggregatedFields[fieldMetadata.name], - objectName: targetObjectMetadata.nameSingular, - take, - totalCount, - order, - depth: depth + 1, - }); - } - continue; - } - - if (isFieldMetadataTypeMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { const targetObjectMetadata = - this.objectMetadataMaps.byId[ + this.flatObjectMetadataMaps.byId[ fieldMetadata.relationTargetObjectMetadataId ]; - if ( - !fieldMetadata.settings?.relationType || - !isDefined(targetObjectMetadata) - ) { + if (!isDefined(targetObjectMetadata)) { continue; } @@ -287,9 +233,9 @@ export class ObjectRecordsToGraphqlConnectionHelper { depth: depth + 1, }); } else if (isPlainObject(objectValue)) { - const targetObjectMetadata = getTargetObjectMetadataOrThrow( + const targetObjectMetadataOrThrow = getTargetObjectMetadataOrThrow( fieldMetadata, - this.objectMetadataMaps, + this.flatObjectMetadataMaps, ); processedObjectRecord[fieldMetadata.name] = this.processRecord({ @@ -298,7 +244,7 @@ export class ObjectRecordsToGraphqlConnectionHelper { objectRecordsAggregatedValues[fieldMetadata.name], selectedAggregatedFields: selectedAggregatedFields[fieldMetadata.name], - objectName: targetObjectMetadata.nameSingular, + objectName: targetObjectMetadataOrThrow.nameSingular, take, totalCount, order, @@ -324,7 +270,7 @@ export class ObjectRecordsToGraphqlConnectionHelper { } private processCompositeField( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, // eslint-disable-next-line @typescript-eslint/no-explicit-any fieldValue: any, // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts index 152d75eb3ac..7b22692a4c9 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts @@ -11,13 +11,14 @@ import { } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; -import { getFieldMetadataFromGraphQLField } from 'src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util'; import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @@ -28,7 +29,8 @@ export class ProcessNestedRelationsV2Helper { constructor() {} public async processNestedRelations({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, parentObjectRecords, parentObjectRecordsAggregatedValues = {}, @@ -40,8 +42,9 @@ export class ProcessNestedRelationsV2Helper { rolePermissionConfig, selectedFields, }: { - objectMetadataMaps: ObjectMetadataMaps; - parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + parentObjectMetadataItem: FlatObjectMetadata; parentObjectRecords: T[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any parentObjectRecordsAggregatedValues?: Record; @@ -57,7 +60,8 @@ export class ProcessNestedRelationsV2Helper { const processRelationTasks = Object.entries(relations).map( ([sourceFieldName, nestedRelations]) => this.processRelation({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, parentObjectRecords, parentObjectRecordsAggregatedValues, @@ -79,7 +83,8 @@ export class ProcessNestedRelationsV2Helper { } private async processRelation({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, parentObjectRecords, parentObjectRecordsAggregatedValues, @@ -92,8 +97,9 @@ export class ProcessNestedRelationsV2Helper { rolePermissionConfig, selectedFields, }: { - objectMetadataMaps: ObjectMetadataMaps; - parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + parentObjectMetadataItem: FlatObjectMetadata; parentObjectRecords: T[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any parentObjectRecordsAggregatedValues: Record; @@ -106,12 +112,20 @@ export class ProcessNestedRelationsV2Helper { rolePermissionConfig?: RolePermissionConfig; selectedFields: Record; }): Promise { - const sourceFieldMetadata = getFieldMetadataFromGraphQLField({ - objectMetadataItem: parentObjectMetadataItem, - graphQLField: sourceFieldName, - objectMetadataMaps, + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + parentObjectMetadataItem, + ); + + const sourceFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: fieldIdByName[sourceFieldName], + flatEntityMaps: flatFieldMetadataMaps, }); + if (!sourceFieldMetadata) { + return; + } + if ( !isFieldMetadataEntityOfType( sourceFieldMetadata, @@ -136,7 +150,8 @@ export class ProcessNestedRelationsV2Helper { const relationType = sourceFieldMetadata.settings?.relationType; const { targetRelationName, targetObjectMetadata, targetRelation } = this.getTargetObjectMetadata({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, sourceFieldName, }); @@ -155,8 +170,9 @@ export class ProcessNestedRelationsV2Helper { const columnsToSelect = buildColumnsToSelect({ select: selectedFields, relations: nestedRelations, - objectMetadataItemWithFieldMaps: targetObjectMetadata, - objectMetadataMaps, + flatObjectMetadata: targetObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); targetObjectQueryBuilder = targetObjectQueryBuilder.setFindOptions({ @@ -208,23 +224,11 @@ export class ProcessNestedRelationsV2Helper { relationType, }); - const targetObjectMetadataItemWithFieldsMaps = - getObjectMetadataMapItemByNameSingular( - objectMetadataMaps, - targetObjectMetadata.nameSingular, - ); - - if (!targetObjectMetadataItemWithFieldsMaps) { - throw new GraphqlQueryRunnerException( - `Object ${targetObjectMetadata.nameSingular} not found`, - GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - if (Object.keys(nestedRelations).length > 0) { await this.processNestedRelations({ - objectMetadataMaps, - parentObjectMetadataItem: targetObjectMetadataItemWithFieldsMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + parentObjectMetadataItem: targetObjectMetadata, parentObjectRecords: relationResults as ObjectRecord[], parentObjectRecordsAggregatedValues: relationAggregatedFieldsResult, relations: nestedRelations as Record< @@ -242,23 +246,36 @@ export class ProcessNestedRelationsV2Helper { } private getTargetObjectMetadata({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, sourceFieldName, }: { - objectMetadataMaps: ObjectMetadataMaps; - parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + parentObjectMetadataItem: FlatObjectMetadata; sourceFieldName: string; }) { - const targetFieldMetadata = getFieldMetadataFromGraphQLField({ - objectMetadataItem: parentObjectMetadataItem, - graphQLField: sourceFieldName, - objectMetadataMaps, + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + parentObjectMetadataItem, + ); + + const targetFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: fieldIdByName[sourceFieldName], + flatEntityMaps: flatFieldMetadataMaps, }); + if (!targetFieldMetadata) { + throw new GraphqlQueryRunnerException( + `Field ${sourceFieldName} not found on object ${parentObjectMetadataItem.nameSingular}`, + GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND, + ); + } + const targetObjectMetadata = getTargetObjectMetadataOrThrow( targetFieldMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, ); if ( @@ -272,14 +289,11 @@ export class ProcessNestedRelationsV2Helper { } const targetRelation = - objectMetadataMaps.byId[ - targetFieldMetadata.relationTargetObjectMetadataId - ]?.fieldsById[targetFieldMetadata.relationTargetFieldMetadataId]; + flatFieldMetadataMaps.byId[ + targetFieldMetadata.relationTargetFieldMetadataId + ]; - const targetRelationName = - objectMetadataMaps.byId[ - targetFieldMetadata.relationTargetObjectMetadataId - ]?.fieldsById[targetFieldMetadata.relationTargetFieldMetadataId]?.name; + const targetRelationName = targetRelation?.name; return { targetRelationName, targetObjectMetadata, targetRelation }; } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts index bcc5c5df796..a048a06a3f5 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts @@ -6,8 +6,9 @@ import { type FindOptionsRelations, type ObjectLiteral } from 'typeorm'; import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper'; import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @@ -18,7 +19,8 @@ export class ProcessNestedRelationsHelper { ) {} public async processNestedRelations({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, parentObjectRecords, parentObjectRecordsAggregatedValues = {}, @@ -30,8 +32,9 @@ export class ProcessNestedRelationsHelper { rolePermissionConfig, selectedFields, }: { - objectMetadataMaps: ObjectMetadataMaps; - parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + parentObjectMetadataItem: FlatObjectMetadata; parentObjectRecords: T[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any parentObjectRecordsAggregatedValues?: Record; @@ -45,7 +48,8 @@ export class ProcessNestedRelationsHelper { selectedFields: Record; }): Promise { return this.processNestedRelationsV2Helper.processNestedRelations({ - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, parentObjectMetadataItem, parentObjectRecords, parentObjectRecordsAggregatedValues, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts index 0cfa2649094..96002fb0571 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts @@ -3,115 +3,185 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; describe('buildColumnsToSelect', () => { - const mockObjectMetadataItemWithFieldMaps: any = { - id: 'a55d8cad-4c3d-4c8a-82b5-539a36de5605', - standardId: '20202020-e674-48e5-a542-72570eee7213', - dataSourceId: 'd161a12f-1daa-4a0b-887c-96a48a3d9ecf', - nameSingular: 'person', - namePlural: 'people', - labelSingular: 'Person', - labelPlural: 'People', - description: 'A person', - icon: 'IconUser', - standardOverrides: null, - targetTableName: 'DEPRECATED', - isCustom: false, - isRemote: false, - isActive: true, - isSystem: false, - isAuditLogged: true, - isSearchable: true, - duplicateCriteria: [ - ['nameFirstName', 'nameLastName'], - ['linkedinLinkPrimaryLinkUrl'], - ['emailsPrimaryEmail'], - ], - shortcut: 'P', - labelIdentifierFieldMetadataId: '92414583-c6a6-4c98-bae7-6ce318bd3423', - imageIdentifierFieldMetadataId: '6bfcecc4-1866-4254-ba6b-6f22246819bb', - isLabelSyncedWithName: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', - createdAt: new Date('2025-07-10T10:58:54.536Z'), - updatedAt: new Date('2025-07-10T10:58:54.536Z'), - indexMetadatas: [], - fieldsById: { - '92414583-c6a6-4c98-bae7-6ce318bd3423': { - id: '92414583-c6a6-4c98-bae7-6ce318bd3423', - type: FieldMetadataType.FULL_NAME, - name: 'name', - label: 'Name', - defaultValue: { - lastName: "''", - firstName: "''", - }, - description: "Contact's name", - icon: 'IconUser', - standardOverrides: null, - options: undefined, - settings: undefined, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', - isLabelSyncedWithName: true, - relationTargetFieldMetadataId: undefined, - relationTargetObjectMetadataId: undefined, - objectMetadataId: 'a55d8cad-4c3d-4c8a-82b5-539a36de5605', - createdAt: new Date('2025-07-10T10:58:54.536Z'), - updatedAt: new Date('2025-07-10T10:58:54.536Z'), - }, - '30dc1370-bb1a-42e3-abbc-a40edf1c6796': { - id: '30dc1370-bb1a-42e3-abbc-a40edf1c6796', - type: FieldMetadataType.RELATION, - name: 'company', - label: 'Company', - defaultValue: null, - description: "Contact's company", - icon: 'IconBuildingSkyscraper', - standardOverrides: null, - options: undefined, - settings: { - relationType: RelationType.MANY_TO_ONE, - joinColumnName: 'companyId', - }, - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', - isLabelSyncedWithName: true, - relationTargetFieldMetadataId: '83cd9e7f-dfbc-4b93-a0a7-4c04b76d2009', - relationTargetObjectMetadataId: '9af20778-2f2c-4e22-ae83-2e77e479b57c', - objectMetadataId: 'a55d8cad-4c3d-4c8a-82b5-539a36de5605', - createdAt: new Date('2025-07-10T10:58:54.536Z'), - updatedAt: new Date('2025-07-10T10:58:54.536Z'), - }, - }, - fieldIdByName: { - name: '92414583-c6a6-4c98-bae7-6ce318bd3423', - company: '30dc1370-bb1a-42e3-abbc-a40edf1c6796', - }, - fieldIdByJoinColumnName: { - companyId: '30dc1370-bb1a-42e3-abbc-a40edf1c6796', - }, - }; + const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419'; + const personObjectId = 'a55d8cad-4c3d-4c8a-82b5-539a36de5605'; + const companyObjectId = '9af20778-2f2c-4e22-ae83-2e77e479b57c'; - const mockObjectMetadataMaps = { - byId: { - '9af20778-2f2c-4e22-ae83-2e77e479b57c': { - id: '9af20778-2f2c-4e22-ae83-2e77e479b57c', - nameSingular: 'company', - namePlural: 'companies', - }, + const nameFieldId = '92414583-c6a6-4c98-bae7-6ce318bd3423'; + const companyFieldId = '30dc1370-bb1a-42e3-abbc-a40edf1c6796'; + + const createMockField = ( + overrides: Partial & { + id: string; + name: string; + type: FieldMetadataType; + objectMetadataId: string; }, - }; + ): FlatFieldMetadata => + ({ + workspaceId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date('2025-07-10T10:58:54.536Z'), + updatedAt: new Date('2025-07-10T10:58:54.536Z'), + universalIdentifier: overrides.id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + label: overrides.name, + isCustom: false, + isActive: true, + isSystem: false, + isUnique: false, + ...overrides, + }) as FlatFieldMetadata; + + const nameField = createMockField({ + id: nameFieldId, + type: FieldMetadataType.FULL_NAME, + name: 'name', + label: 'Name', + objectMetadataId: personObjectId, + defaultValue: { + lastName: "''", + firstName: "''", + }, + description: "Contact's name", + icon: 'IconUser', + }); + + const companyRelationField = createMockField({ + id: companyFieldId, + type: FieldMetadataType.RELATION, + name: 'company', + label: 'Company', + objectMetadataId: personObjectId, + defaultValue: null, + description: "Contact's company", + icon: 'IconBuildingSkyscraper', + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: 'companyId', + }, + relationTargetFieldMetadataId: '83cd9e7f-dfbc-4b93-a0a7-4c04b76d2009', + relationTargetObjectMetadataId: companyObjectId, + }); + + const buildMockFlatObjectMetadata = ( + fieldIds: string[], + ): FlatObjectMetadata => + ({ + id: personObjectId, + standardId: '20202020-e674-48e5-a542-72570eee7213', + nameSingular: 'person', + namePlural: 'people', + labelSingular: 'Person', + labelPlural: 'People', + description: 'A person', + icon: 'IconUser', + targetTableName: 'DEPRECATED', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + duplicateCriteria: [ + ['nameFirstName', 'nameLastName'], + ['linkedinLinkPrimaryLinkUrl'], + ['emailsPrimaryEmail'], + ], + shortcut: 'P', + labelIdentifierFieldMetadataId: nameFieldId, + imageIdentifierFieldMetadataId: '6bfcecc4-1866-4254-ba6b-6f22246819bb', + isLabelSyncedWithName: false, + workspaceId, + createdAt: new Date('2025-07-10T10:58:54.536Z'), + updatedAt: new Date('2025-07-10T10:58:54.536Z'), + universalIdentifier: personObjectId, + fieldMetadataIds: fieldIds, + indexMetadataIds: [], + viewIds: [], + applicationId: null, + }) as unknown as FlatObjectMetadata; + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); + + const buildFlatObjectMetadataMaps = ( + objects: FlatObjectMetadata[], + ): FlatEntityMaps => ({ + byId: objects.reduce( + (acc, obj) => { + acc[obj.id] = obj; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: objects.reduce( + (acc, obj) => { + if (obj.universalIdentifier) { + acc[obj.universalIdentifier] = obj.id; + } + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); + + const companyObjectMetadata: FlatObjectMetadata = { + id: companyObjectId, + nameSingular: 'company', + namePlural: 'companies', + fieldMetadataIds: [], + indexMetadataIds: [], + viewIds: [], + } as unknown as FlatObjectMetadata; it('should build columns to select with relation fields', () => { + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + companyRelationField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); + const select = { nameFirstName: true, company: { @@ -127,8 +197,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -139,6 +210,19 @@ describe('buildColumnsToSelect', () => { }); it('should build columns to select without relation fields', () => { + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + companyRelationField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); + const select = { nameFirstName: true, nameLastName: true, @@ -149,8 +233,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -161,6 +246,19 @@ describe('buildColumnsToSelect', () => { }); it('should filter out non-boolean values from select', () => { + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + companyRelationField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); + const select = { nameFirstName: true, nameLastName: false, @@ -172,8 +270,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -183,6 +282,19 @@ describe('buildColumnsToSelect', () => { }); it('should handle relation field that is not a relation type', () => { + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + companyRelationField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); + const select = { nameFirstName: true, }; @@ -194,8 +306,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -205,21 +318,32 @@ describe('buildColumnsToSelect', () => { }); it('should handle relation field that is not MANY_TO_ONE', () => { - const mockObjectMetadataWithOneToMany: any = { - ...mockObjectMetadataItemWithFieldMaps, - fieldsById: { - ...mockObjectMetadataItemWithFieldMaps.fieldsById, - '30dc1370-bb1a-42e3-abbc-a40edf1c6796': { - ...mockObjectMetadataItemWithFieldMaps.fieldsById[ - '30dc1370-bb1a-42e3-abbc-a40edf1c6796' - ], - settings: { - relationType: RelationType.ONE_TO_MANY, - joinColumnName: 'companyId', - }, - }, + const oneToManyCompanyField = createMockField({ + id: companyFieldId, + type: FieldMetadataType.RELATION, + name: 'company', + label: 'Company', + objectMetadataId: personObjectId, + defaultValue: null, + settings: { + relationType: RelationType.ONE_TO_MANY, + joinColumnName: 'companyId', }, - }; + relationTargetObjectMetadataId: companyObjectId, + }); + + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + oneToManyCompanyField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); const select = { nameFirstName: true, @@ -232,8 +356,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataWithOneToMany, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -243,21 +368,32 @@ describe('buildColumnsToSelect', () => { }); it('should handle relation field without joinColumnName', () => { - const mockObjectMetadataWithoutJoinColumn: any = { - ...mockObjectMetadataItemWithFieldMaps, - fieldsById: { - ...mockObjectMetadataItemWithFieldMaps.fieldsById, - '30dc1370-bb1a-42e3-abbc-a40edf1c6796': { - ...mockObjectMetadataItemWithFieldMaps.fieldsById[ - '30dc1370-bb1a-42e3-abbc-a40edf1c6796' - ], - settings: { - relationType: RelationType.MANY_TO_ONE, - joinColumnName: null, - }, - }, + const noJoinColumnCompanyField = createMockField({ + id: companyFieldId, + type: FieldMetadataType.RELATION, + name: 'company', + label: 'Company', + objectMetadataId: personObjectId, + defaultValue: null, + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: null, }, - }; + relationTargetObjectMetadataId: companyObjectId, + }); + + const flatObjectMetadata = buildMockFlatObjectMetadata([ + nameFieldId, + companyFieldId, + ]); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + noJoinColumnCompanyField, + ]); + const flatObjectMetadataMaps = buildFlatObjectMetadataMaps([ + flatObjectMetadata, + companyObjectMetadata, + ]); const select = { nameFirstName: true, @@ -270,8 +406,9 @@ describe('buildColumnsToSelect', () => { const result = buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps: mockObjectMetadataWithoutJoinColumn, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); expect(result).toEqual({ diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts index c4a7ab3a4d8..c647e449cf2 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts @@ -1,24 +1,28 @@ import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const buildColumnsToReturn = ({ select, relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { select: Record; relations: Record; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }): string[] => { return Object.entries( buildColumnsToSelect({ select, relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }), ) .filter(([_columnName, value]) => value === true) diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts index ad025dcd7e9..261d2bc6218 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts @@ -1,32 +1,32 @@ +import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; -import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + export const buildColumnsToSelect = ({ select, relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { select: Record; relations: Record; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: { - byId: Partial< - Record< - string, - Pick - > - >; - }; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }) => { const requiredRelationColumns = getRequiredRelationColumns( relations, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); const fieldsToSelect: Record = Object.entries(select) @@ -44,20 +44,19 @@ export const buildColumnsToSelect = ({ const getRequiredRelationColumns = ( relations: Record, - objectMetadataItem: Pick, - objectMetadataMaps: { - byId: Partial< - Record< - string, - Pick - > - >; - }, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ): string[] => { const requiredColumns: string[] = []; - for (const fieldMetadata of Object.values(objectMetadataItem.fieldsById)) { - if (isFieldMetadataTypeRelation(fieldMetadata)) { + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const fieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: fieldId, + flatEntityMaps: flatFieldMetadataMaps, + }); + + if (isFlatFieldMetadataOfType(fieldMetadata, FieldMetadataType.RELATION)) { const relationValue = relations[fieldMetadata.name]; if ( @@ -71,9 +70,14 @@ const getRequiredRelationColumns = ( requiredColumns.push(fieldMetadata.settings.joinColumnName); } - if (isFieldMetadataTypeMorphRelation(fieldMetadata)) { - const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + if ( + isFlatFieldMetadataOfType(fieldMetadata, FieldMetadataType.MORPH_RELATION) + ) { + const targetObjectMetadata = fieldMetadata.relationTargetObjectMetadataId + ? flatObjectMetadataMaps.byId[ + fieldMetadata.relationTargetObjectMetadataId + ] + : undefined; if ( !fieldMetadata.settings?.relationType || diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts index 48819f701ed..1c6bfb479e4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts @@ -2,56 +2,71 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; type GetFieldMetadataFromGraphQLFieldArgs = { - objectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; graphQLField: string; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }; export function getFieldMetadataFromGraphQLField({ - objectMetadataItem, + flatObjectMetadata, graphQLField, - objectMetadataMaps, -}: GetFieldMetadataFromGraphQLFieldArgs) { - const sourceFieldMetadataId = objectMetadataItem.fieldIdByName[graphQLField]; - let sourceFieldMetadata = - objectMetadataItem.fieldsById[sourceFieldMetadataId]; + flatObjectMetadataMaps, + flatFieldMetadataMaps, +}: GetFieldMetadataFromGraphQLFieldArgs): FlatFieldMetadata | undefined { + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + const sourceFieldMetadataId = fieldIdByName[graphQLField]; + let sourceFieldMetadata = sourceFieldMetadataId + ? flatFieldMetadataMaps.byId[sourceFieldMetadataId] + : undefined; // If empty, it could be a morph relation if (!isDefined(sourceFieldMetadata)) { - const morphRelationsWithTargetObjectMetadata = Object.values( - objectMetadataItem.fieldsById, - ) - .filter( - (fieldMetadata) => - fieldMetadata.type === FieldMetadataType.MORPH_RELATION, + const morphRelationsWithTargetObjectMetadata = + getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ) - .map((fieldMetadata) => { - const targetObjectMetadata = getTargetObjectMetadataOrThrow( - fieldMetadata, - objectMetadataMaps, - ); + .filter( + (fieldMetadata) => + fieldMetadata.type === FieldMetadataType.MORPH_RELATION, + ) + .map((fieldMetadata) => { + const targetObjectMetadata = getTargetObjectMetadataOrThrow( + fieldMetadata, + flatObjectMetadataMaps, + ); - return { - fieldMetadata, - targetObjectMetadata, - }; - }); + return { + fieldMetadata, + targetObjectMetadata, + }; + }); const possibleGraphQLFieldNames: { graphQLField: string; - fieldMetadata: FieldMetadataEntity; - targetObjectMetadata: ObjectMetadataItemWithFieldMaps; + fieldMetadata: FlatFieldMetadata; + targetObjectMetadata: FlatObjectMetadata; }[] = []; morphRelationsWithTargetObjectMetadata.map((morphRelation) => { if ( - !isFieldMetadataTypeMorphRelation(morphRelation.fieldMetadata) || + !isFlatFieldMetadataOfType( + morphRelation.fieldMetadata, + FieldMetadataType.MORPH_RELATION, + ) || !morphRelation.fieldMetadata.settings?.relationType ) { return; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts index 9efd28327ba..72a4ab6bf66 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts @@ -2,13 +2,14 @@ import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const getTargetObjectMetadataOrThrow = ( - fieldMetadata: FieldMetadataEntity, - objectMetadataMaps: ObjectMetadataMaps, -) => { + fieldMetadata: FlatFieldMetadata, + flatObjectMetadataMaps: FlatEntityMaps, +): FlatObjectMetadata => { if (!fieldMetadata.relationTargetObjectMetadataId) { throw new GraphqlQueryRunnerException( `Relation target object metadata id not found for field ${fieldMetadata.name}`, @@ -17,7 +18,7 @@ export const getTargetObjectMetadataOrThrow = ( } const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + flatObjectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; if (!targetObjectMetadata) { throw new GraphqlQueryRunnerException( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts deleted file mode 100644 index 6ea833fc9a8..00000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type GraphQLResolveInfo } from 'graphql'; - -import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; - -export interface WorkspaceQueryRunnerOptions { - authContext: AuthContext; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - info: GraphQLResolveInfo; -} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts index dae563cee80..12349f31676 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util.ts @@ -1,9 +1,7 @@ import { msg } from '@lingui/core/macro'; -import { isDefined } from 'twenty-shared/utils'; import { type QueryFailedError } from 'typeorm'; -import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMException, TwentyORMExceptionCode, @@ -14,64 +12,16 @@ interface PostgreSQLError extends QueryFailedError { } export const handleDuplicateKeyError = ( - error: PostgreSQLError, - objectMetadata: ObjectMetadataItemWithFieldMaps, + _error: PostgreSQLError, + _objectMetadata: FlatObjectMetadata, ) => { - const indexNameMatch = error.message.match(/"([^"]+)"/); - - const duplicatedValues = error?.detail?.match(/=\(([^)]+)\)/)?.[1]; - - if (indexNameMatch) { - const indexName = indexNameMatch[1]; - - const deletedAtFieldMetadataId = objectMetadata.fieldIdByName['deletedAt']; - - const affectedColumns = objectMetadata.indexMetadatas - .find((index) => index.name === indexName) - ?.indexFieldMetadatas?.filter( - (field) => field.fieldMetadataId !== deletedAtFieldMetadataId, - ) - .map((indexField) => { - const fieldMetadata = - objectMetadata.fieldsById[indexField.fieldMetadataId]; - - return fieldMetadata?.label; - }); - - if (!isDefined(affectedColumns)) { - throw new TwentyORMException( - `A duplicate entry was detected`, - TwentyORMExceptionCode.DUPLICATE_ENTRY_DETECTED, - { - userFriendlyMessage: msg`This record already exists. Please check your data and try again.`, - }, - ); - } - - const columnNames = affectedColumns.join(', '); - - if (affectedColumns?.length === 1) { - const fieldName = columnNames.toLowerCase(); - - throw new UserInputError( - `Duplicate ${columnNames} ${duplicatedValues ? `with value ${duplicatedValues}` : ''}. Please set a unique one.`, - { - userFriendlyMessage: duplicatedValues - ? msg`This ${fieldName} with value ${duplicatedValues} is already taken. Please choose a different value.` - : msg`This ${fieldName} is already taken. Please choose a different value.`, - isExpected: true, - }, - ); - } - - const fieldNames = columnNames.toLowerCase(); - - throw new TwentyORMException( - `A duplicate entry was detected. The combination of ${columnNames} must be unique.`, - TwentyORMExceptionCode.DUPLICATE_ENTRY_DETECTED, - { - userFriendlyMessage: msg`This combination of ${fieldNames} already exists. Please use different values.`, - }, - ); - } + // Since we no longer have indexMetadatas in FlatObjectMetadata, + // we provide a generic error message + throw new TwentyORMException( + `A duplicate entry was detected`, + TwentyORMExceptionCode.DUPLICATE_ENTRY_DETECTED, + { + userFriendlyMessage: msg`This record already exists. Please check your data and try again.`, + }, + ); }; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts index d02a8d8d094..bd1296f080b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts @@ -41,14 +41,15 @@ export class CreateManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return records.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }), diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts index ecee4e26ab0..6550acccc6e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class CreateOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts index febdd06de2b..608ccf58133 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts @@ -41,14 +41,15 @@ export class DeleteManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return records.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }), diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts index 6d829828478..1bc0ae1acf8 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class DeleteOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts index 1a48865f823..46f57ecfc2e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts @@ -41,14 +41,15 @@ export class DestroyManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return records.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }), diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts index 86a33573cc4..d7afd06d553 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class DestroyOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts index b964ae4ba4c..15f6192899f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts @@ -42,14 +42,15 @@ export class FindDuplicatesResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return paginatedDuplicates.map((duplicate) => typeORMObjectRecordsParser.createConnection({ objectRecords: duplicate.records, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: duplicate.records.length, totalCount: duplicate.totalCount, order: [{ id: OrderByDirection.AscNullsFirst }], diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts index 3c71ed3efb5..f9b173ace9a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts @@ -47,15 +47,16 @@ export class FindManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.createConnection({ objectRecords: records, objectRecordsAggregatedValues: aggregatedValues, selectedAggregatedFields: selectedFieldsResult.aggregate, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: args.first ?? args.last ?? QUERY_MAX_RECORDS, totalCount, order: args.orderBy, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts index 6e82e7389e0..fe8160001c5 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class FindOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts index 5710f187b05..946b216c24f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory.ts @@ -40,7 +40,9 @@ export class GroupByResolverFactory try { const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); const results = await this.commonGroupByQueryRunnerService.execute( @@ -51,8 +53,7 @@ export class GroupByResolverFactory const formattedResults = results.map((group) => { const formattedRecords = typeORMObjectRecordsParser.createConnection({ objectRecords: group.records ?? [], - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, objectRecordsAggregatedValues: {}, selectedAggregatedFields: {}, take: group.records?.length || 0, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts index dd2adec1810..86345942d5f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory.ts @@ -40,13 +40,14 @@ export class MergeManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts index 6c5b732674d..444fee1a9b6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts @@ -41,14 +41,15 @@ export class RestoreManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return records.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }), diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts index ef59ff6d515..3bf4b686c01 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class RestoreOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts index 8a0c477244e..90a08688260 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts @@ -41,14 +41,15 @@ export class UpdateManyResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return records.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }), diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts index fd75c558ece..c31aef9150f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts @@ -40,13 +40,14 @@ export class UpdateOneResolverFactory const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper( - internalContext.objectMetadataMaps, + internalContext.flatObjectMetadataMaps, + internalContext.flatFieldMetadataMaps, + internalContext.objectIdByNameSingular, ); return typeORMObjectRecordsParser.processRecord({ objectRecord: record, - objectName: - internalContext.objectMetadataItemWithFieldMaps.nameSingular, + objectName: internalContext.flatObjectMetadata.nameSingular, take: 1, totalCount: 1, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts index 15722ca67c5..0db00351d5a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts @@ -17,11 +17,10 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getResolverName } from 'src/engine/utils/get-resolver-name.util'; -import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; -import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; import { CreateManyResolverFactory } from './factories/create-many-resolver.factory'; import { CreateOneResolverFactory } from './factories/create-one-resolver.factory'; @@ -57,12 +56,13 @@ export class WorkspaceResolverFactory { private readonly mergeManyResolverFactory: MergeManyResolverFactory, private readonly groupByResolverFactory: GroupByResolverFactory, private readonly workspaceResolverBuilderService: WorkspaceResolverBuilderService, - private readonly featureFlagService: FeatureFlagService, ) {} async create( authContext: AuthContext, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + objectIdByNameSingular: Record, workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods, ): Promise { const factories = new Map< @@ -99,30 +99,17 @@ export class WorkspaceResolverFactory { ); } - const workspaceFeatureFlagsMap = - await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId); - - for (const objectMetadata of Object.values(objectMetadataMaps.byId).filter( - isDefined, - )) { - if ( - shouldExcludeFromWorkspaceApi( - objectMetadata, - standardObjectMetadataDefinitions, - workspaceFeatureFlagsMap, - ) - ) { - continue; - } - + for (const flatObjectMetadata of Object.values( + flatObjectMetadataMaps.byId, + ).filter(isDefined)) { // Generate query resolvers for (const methodName of workspaceResolverBuilderMethods.queries) { - const resolverName = getResolverName(objectMetadata, methodName); + const resolverName = getResolverName(flatObjectMetadata, methodName); const resolverFactory = factories.get(methodName); if (!resolverFactory) { this.logger.error(`Unknown query resolver type: ${methodName}`, { - objectMetadata, + flatObjectMetadata, methodName, resolverName, }); @@ -132,27 +119,29 @@ export class WorkspaceResolverFactory { if ( this.workspaceResolverBuilderService.shouldBuildResolver( - objectMetadata, + flatObjectMetadata, methodName, ) ) { // @ts-expect-error legacy noImplicitAny resolvers.Query[resolverName] = resolverFactory.create({ authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps: objectMetadata, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }); } } // Generate mutation resolvers for (const methodName of workspaceResolverBuilderMethods.mutations) { - const resolverName = getResolverName(objectMetadata, methodName); + const resolverName = getResolverName(flatObjectMetadata, methodName); const resolverFactory = factories.get(methodName); if (!resolverFactory) { this.logger.error(`Unknown mutation resolver type: ${methodName}`, { - objectMetadata, + flatObjectMetadata, methodName, resolverName, }); @@ -162,15 +151,17 @@ export class WorkspaceResolverFactory { if ( this.workspaceResolverBuilderService.shouldBuildResolver( - objectMetadata, + flatObjectMetadata, methodName, ) ) { // @ts-expect-error legacy noImplicitAny resolvers.Mutation[resolverName] = resolverFactory.create({ authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps: objectMetadata, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/enum-field-metadata-gql-enum-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/enum-field-metadata-gql-enum-type.generator.ts index a11a78ff1e3..70ca1bc31ed 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/enum-field-metadata-gql-enum-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/enum-field-metadata-gql-enum-type.generator.ts @@ -9,9 +9,9 @@ import { type FieldMetadataComplexOption, type FieldMetadataDefaultOption, } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { transformEnumValue } from 'src/engine/utils/transform-enum-value'; import { pascalCase } from 'src/utils/pascal-case'; @@ -23,25 +23,28 @@ export class EnumFieldMetadataGqlEnumTypeGenerator { constructor(private readonly gqlTypesStorage: GqlTypesStorage) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { - for (const fieldMetadata of objectMetadata.fields) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + ) { + for (const fieldMetadata of fields) { if (!isEnumFieldMetadataType(fieldMetadata.type)) { continue; } this.gqlTypesStorage.addGqlType( computeEnumFieldGqlTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, fieldMetadata.name, ), - this.generateEnum(objectMetadata.nameSingular, fieldMetadata), + this.generateEnum(flatObjectMetadata.nameSingular, fieldMetadata), ); } } private generateEnum( objectName: string, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ): GraphQLEnumType { // FixMe: It's a hack until Typescript get fixed on union types for reduce function // https://github.com/microsoft/TypeScript/issues/36390 diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/gql-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/gql-type.generator.ts index e03557963ec..97dbd9086e1 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/gql-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/gql-type.generator.ts @@ -4,6 +4,7 @@ import { type CompositeType, compositeTypeDefinitions, } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { CompositeFieldMetadataGqlEnumTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/composite-field-metadata-gql-enum-type.generator'; import { EnumFieldMetadataGqlEnumTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/enum-field-metadata-gql-enum-type.generator'; @@ -19,9 +20,10 @@ import { ObjectMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/wor import { ObjectMetadataWithRelationsGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator'; import { MutationTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator'; import { QueryTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator'; -import { objectContainsMorphRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util'; -import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; @Injectable() export class GqlTypeGenerator { @@ -44,14 +46,14 @@ export class GqlTypeGenerator { private readonly mutationTypeGenerator: MutationTypeGenerator, ) {} - async buildAndStore(objectMetadataCollection: ObjectMetadataEntity[]) { + async buildAndStore(context: SchemaGenerationContext) { const compositeTypeCollection = [...compositeTypeDefinitions.values()]; this.buildAndStoreCompositeFieldMetadataGqlTypes(compositeTypeCollection); this.buildAndStoreDateFieldMetadataGroupByGqlTypes(); - this.buildAndStoreObjectMetadataGqlTypes(objectMetadataCollection); - await this.queryTypeGenerator.buildAndStore(objectMetadataCollection); - this.mutationTypeGenerator.buildAndStore(objectMetadataCollection); + this.buildAndStoreObjectMetadataGqlTypes(context); + await this.queryTypeGenerator.buildAndStore(context); + this.mutationTypeGenerator.buildAndStore(context); } private buildAndStoreCompositeFieldMetadataGqlTypes( @@ -75,36 +77,67 @@ export class GqlTypeGenerator { } private buildAndStoreObjectMetadataGqlTypes( - dynamicObjectMetadataCollection: ObjectMetadataEntity[], + context: SchemaGenerationContext, ) { + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = context; + const objectMetadataCollection = Object.values( + flatObjectMetadataMaps.byId, + ).filter(isDefined); + this.logger.log( - `Generating metadata objects: [${dynamicObjectMetadataCollection + `Generating metadata objects: [${objectMetadataCollection .map((object) => object.nameSingular) .join(', ')}]`, ); - for (const objectMetadata of dynamicObjectMetadataCollection) { - this.enumFieldMetadataGqlEnumTypeGenerator.buildAndStore(objectMetadata); - this.objectMetadataGqlObjectTypeGenerator.buildAndStore(objectMetadata); - this.edgeGqlObjectTypeGenerator.buildAndStore(objectMetadata); - this.connectionGqlObjectTypeGenerator.buildAndStore(objectMetadata); - this.groupByConnectionGqlObjectTypeGenerator.buildAndStore( - objectMetadata, - ); - this.relationConnectGqlInputTypeGenerator.buildAndStore(objectMetadata); - this.objectMetadataGqlInputTypeGenerator.buildAndStore( - objectMetadata, - dynamicObjectMetadataCollection, + for (const flatObjectMetadata of objectMetadataCollection) { + const fields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ); - if ( - objectContainsRelationField(objectMetadata) || - objectContainsMorphRelationField(objectMetadata) - ) + this.enumFieldMetadataGqlEnumTypeGenerator.buildAndStore( + flatObjectMetadata, + fields, + ); + this.objectMetadataGqlObjectTypeGenerator.buildAndStore( + flatObjectMetadata, + fields, + ); + this.edgeGqlObjectTypeGenerator.buildAndStore(flatObjectMetadata); + this.connectionGqlObjectTypeGenerator.buildAndStore( + flatObjectMetadata, + fields, + ); + this.groupByConnectionGqlObjectTypeGenerator.buildAndStore( + flatObjectMetadata, + ); + this.relationConnectGqlInputTypeGenerator.buildAndStore( + flatObjectMetadata, + fields, + context, + ); + this.objectMetadataGqlInputTypeGenerator.buildAndStore( + flatObjectMetadata, + fields, + context, + ); + + if (this.objectContainsRelationOrMorphField(fields)) { this.objectMetadataWithRelationsGqlObjectTypeGenerator.buildAndStore( - objectMetadata, - dynamicObjectMetadataCollection, + flatObjectMetadata, + fields, + context, ); + } } } + + private objectContainsRelationOrMorphField( + fields: FlatFieldMetadata[], + ): boolean { + return fields.some((field) => + isMorphOrRelationFieldMetadataType(field.type), + ); + } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator.ts index 6e6b638e29e..b257f26b9e3 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator.ts @@ -16,15 +16,16 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util'; import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util'; import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -38,15 +39,20 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { private readonly typeMapperService: TypeMapperService, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + _context: SchemaGenerationContext, + ) { const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Create.toString()}Input`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Create.toString()}Input`, + description: flatObjectMetadata.description, + fields: () => + this.generateFields(flatObjectMetadata.nameSingular, fields), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.Create, ); @@ -54,11 +60,12 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], ): GraphQLInputFieldConfigMap { const allGeneratedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { + for (const fieldMetadata of fields) { const typeOptions = computeFieldInputTypeOptions( fieldMetadata, GqlInputTypeDefinitionKind.Create, @@ -66,24 +73,26 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { let generatedFields; - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { + const relationFieldMetadata = fieldMetadata; + generatedFields = { ...this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldCreateOrUpdateInputType( { - fieldMetadata, + fieldMetadata: relationFieldMetadata, typeOptions, }, ), ...this.relationFieldMetadataGqlInputTypeGenerator.generateConnectRelationFieldInputType( { - fieldMetadata, + fieldMetadata: relationFieldMetadata, typeOptions, }, ), }; } else if (isEnumFieldMetadataType(fieldMetadata.type)) { generatedFields = this.generateEnumFieldCreateInputType( - objectMetadata, + objectNameSingular, fieldMetadata, typeOptions, ); @@ -106,12 +115,12 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { } private generateEnumFieldCreateInputType( - objectMetadata: ObjectMetadataEntity, - fieldMetadata: FieldMetadataEntity, + objectNameSingular: string, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const key = computeEnumFieldGqlTypeKey( - objectMetadata.nameSingular, + objectNameSingular, fieldMetadata.name, ); @@ -141,7 +150,7 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { } private generateCompositeFieldCreateInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const key = computeCompositeFieldInputTypeKey( @@ -175,7 +184,7 @@ export class ObjectMetadataCreateGqlInputTypeGenerator { } private generateAtomicFieldCreateInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const type = this.typeMapperService.mapToScalarType( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator.ts index bd41575b98c..65248d0580e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator.ts @@ -21,11 +21,11 @@ import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/worksp import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; import { createGqlEnumFilterType } from 'src/engine/api/graphql/workspace-schema-builder/utils/create-gql-enum-filter-type.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -39,15 +39,19 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { private readonly typeMapperService: TypeMapperService, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + ) { const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Filter.toString()}Input`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata, inputType), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Filter.toString()}Input`, + description: flatObjectMetadata.description, + fields: () => + this.generateFields(flatObjectMetadata.nameSingular, fields, inputType), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.Filter, ); @@ -55,7 +59,8 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], inputType: GraphQLInputObjectType, ): GraphQLInputFieldConfigMap { const andOrType = this.typeMapperService.applyTypeOptions(inputType, { @@ -65,17 +70,20 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { }); const allGeneratedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - fieldMetadata.isNullable = true; + for (const fieldMetadata of fields) { + const modifiedFieldMetadata = { + ...fieldMetadata, + isNullable: true, + }; const typeOptions = computeFieldInputTypeOptions( - fieldMetadata, + modifiedFieldMetadata, GqlInputTypeDefinitionKind.Filter, ); let generatedFields; - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { generatedFields = this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldFilterInputType( { @@ -85,7 +93,7 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { ); } else if (isEnumFieldMetadataType(fieldMetadata.type)) { generatedFields = this.generateEnumFieldFilterInputType( - objectMetadata, + objectNameSingular, fieldMetadata, typeOptions, ); @@ -120,12 +128,12 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { } private generateEnumFieldFilterInputType( - objectMetadata: ObjectMetadataEntity, - fieldMetadata: FieldMetadataEntity, + objectNameSingular: string, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const key = computeEnumFieldGqlTypeKey( - objectMetadata.nameSingular, + objectNameSingular, fieldMetadata.name, ); @@ -152,7 +160,7 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { } private generateCompositeFieldFilterInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const key = computeCompositeFieldInputTypeKey( @@ -181,7 +189,7 @@ export class ObjectMetadataFilterGqlInputTypeGenerator { } private generateAtomicFieldFilterInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const type = this.typeMapperService.mapToFilterType( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator.ts index 1529ce0c760..c4171ed49a6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator.ts @@ -16,14 +16,13 @@ import { GROUP_BY_DATE_GRANULARITY_INPUT_KEY } from 'src/engine/api/graphql/work import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator'; import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -31,8 +30,6 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator { private readonly logger = new Logger( ObjectMetadataGroupByGqlInputTypeGenerator.name, ); - private objectMetadataCollection: ObjectMetadataEntity[] = []; - private objectMetadataMaps: ObjectMetadataMaps | undefined; constructor( private readonly gqlTypesStorage: GqlTypesStorage, @@ -41,24 +38,18 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator { ) {} public buildAndStore( - objectMetadata: ObjectMetadataEntity, - objectMetadataCollection?: ObjectMetadataEntity[], + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ) { - if (isDefined(objectMetadataCollection)) { - this.objectMetadataCollection = objectMetadataCollection; - this.objectMetadataMaps = generateObjectMetadataMaps( - objectMetadataCollection, - ); - } - const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.GroupBy.toString()}Input`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.GroupBy.toString()}Input`, + description: flatObjectMetadata.description, + fields: () => this.generateFields(fields, context), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.GroupBy, ); @@ -66,17 +57,16 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator { } private generateFields( - objectMetadata: ObjectMetadataEntity, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ): GraphQLInputFieldConfigMap { const allGeneratedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - const generatedField = isFieldMetadataRelationOrMorphRelation( - fieldMetadata, - ) + for (const fieldMetadata of fields) { + const generatedField = isMorphOrRelationFlatFieldMetadata(fieldMetadata) ? this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldGroupByInputType( fieldMetadata, - this.objectMetadataMaps, + context, ) : this.generateField(fieldMetadata); @@ -86,7 +76,7 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator { return allGeneratedFields; } - private generateField(fieldMetadata: FieldMetadataEntity) { + private generateField(fieldMetadata: FlatFieldMetadata) { if (isCompositeFieldMetadataType(fieldMetadata.type)) return this.generateCompositeFieldGroupByInputType(fieldMetadata); @@ -124,7 +114,7 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator { } private generateCompositeFieldGroupByInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ) { const key = computeCompositeFieldInputTypeKey( fieldMetadata.type, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator.ts index 4b2c5f6df4b..765d1a81507 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator.ts @@ -6,7 +6,9 @@ import { ObjectMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graph import { ObjectMetadataOrderByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator'; import { ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-with-group-by-gql-input-type.generator'; import { ObjectMetadataUpdateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class ObjectMetadataGqlInputTypeGenerator { @@ -20,28 +22,37 @@ export class ObjectMetadataGqlInputTypeGenerator { ) {} public buildAndStore( - objectMetadata: ObjectMetadataEntity, - objectMetadataCollection?: ObjectMetadataEntity[], + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ) { this.objectMetadataCreateGqlInputTypeGenerator.buildAndStore( - objectMetadata, + flatObjectMetadata, + fields, + context, ); this.objectMetadataUpdateGqlInputTypeGenerator.buildAndStore( - objectMetadata, + flatObjectMetadata, + fields, + context, ); this.objectMetadataFilterGqlInputTypeGenerator.buildAndStore( - objectMetadata, + flatObjectMetadata, + fields, ); this.objectMetadataOrderByGqlInputTypeGenerator.buildAndStore({ - objectMetadata, + flatObjectMetadata, + fields, }); this.objectMetadataOrderByWithGroupByGqlInputTypeGenerator.buildAndStore({ - objectMetadata, - objectMetadataCollection, + flatObjectMetadata, + fields, + context, }); this.objectMetadataGroupByGqlInputTypeGenerator.buildAndStore( - objectMetadata, - objectMetadataCollection, + flatObjectMetadata, + fields, + context, ); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-base.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-base.generator.ts index 23d8bd6433e..51267b540fd 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-base.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-base.generator.ts @@ -12,13 +12,12 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util'; import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; @Injectable() export class ObjectMetadataOrderByBaseGenerator { @@ -29,35 +28,38 @@ export class ObjectMetadataOrderByBaseGenerator { ) {} public generateFields({ - objectMetadata, + fields, logger, orderByDateGranularity, - objectMetadataMaps, + context, }: { - objectMetadata: ObjectMetadataEntity; + fields: FlatFieldMetadata[]; logger: Logger; orderByDateGranularity?: boolean; - objectMetadataMaps?: ObjectMetadataMaps; + context?: SchemaGenerationContext; }): GraphQLInputFieldConfigMap { const allGeneratedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - fieldMetadata.isNullable = true; + for (const fieldMetadata of fields) { + const modifiedFieldMetadata = { + ...fieldMetadata, + isNullable: true, + }; const typeOptions = computeFieldInputTypeOptions( - fieldMetadata, + modifiedFieldMetadata, GqlInputTypeDefinitionKind.OrderBy, ); let generatedFields; - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { generatedFields = this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldOrderByInputType( { fieldMetadata, typeOptions, - objectMetadataMaps, + context, }, ); } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { @@ -82,7 +84,7 @@ export class ObjectMetadataOrderByBaseGenerator { } private generateCompositeFieldOrderByInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, logger: Logger, ) { @@ -117,7 +119,7 @@ export class ObjectMetadataOrderByBaseGenerator { logger, orderByDateGranularity, }: { - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; typeOptions: TypeOptions; logger: Logger; orderByDateGranularity?: boolean; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator.ts index dcadf0d3633..17aaba5d6e7 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator.ts @@ -6,7 +6,8 @@ import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-sch import { ObjectMetadataOrderByBaseGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-base.generator'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -20,22 +21,24 @@ export class ObjectMetadataOrderByGqlInputTypeGenerator { ) {} public buildAndStore({ - objectMetadata, + flatObjectMetadata, + fields, }: { - objectMetadata: ObjectMetadataEntity; + flatObjectMetadata: FlatObjectMetadata; + fields: FlatFieldMetadata[]; }) { const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.OrderBy.toString()}Input`, - description: objectMetadata.description, + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.OrderBy.toString()}Input`, + description: flatObjectMetadata.description, fields: () => this.objectMetadataOrderByBaseGenerator.generateFields({ - objectMetadata, + fields, logger: this.logger, }), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.OrderBy, ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-with-group-by-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-with-group-by-gql-input-type.generator.ts index c477428f1c1..3169796f938 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-with-group-by-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-with-group-by-gql-input-type.generator.ts @@ -15,16 +15,15 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util'; import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; import { getAvailableAggregationsFromObjectFields } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -39,29 +38,24 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { private readonly typeMapperService: TypeMapperService, ) {} - private objectMetadataMaps?: ObjectMetadataMaps; - public buildAndStore({ - objectMetadata, - objectMetadataCollection, + flatObjectMetadata, + fields, + context, }: { - objectMetadata: ObjectMetadataEntity; - objectMetadataCollection?: ObjectMetadataEntity[]; + flatObjectMetadata: FlatObjectMetadata; + fields: FlatFieldMetadata[]; + context: SchemaGenerationContext; }) { - if (isDefined(objectMetadataCollection)) { - this.objectMetadataMaps = generateObjectMetadataMaps( - objectMetadataCollection, - ); - } - const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.OrderByWithGroupBy.toString()}Input`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.OrderByWithGroupBy.toString()}Input`, + description: flatObjectMetadata.description, + fields: () => + this.generateFields(flatObjectMetadata.nameSingular, fields, context), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.OrderByWithGroupBy, ); @@ -69,36 +63,47 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ): GraphQLInputFieldConfigMap { return { - ...this.generateOrderByOnAggregateFields(objectMetadata), - ...this.generateOrderByOnDimensionValuesFields(objectMetadata), + ...this.generateOrderByOnAggregateFields( + objectNameSingular, + fields, + context, + ), + ...this.generateOrderByOnDimensionValuesFields(fields, context), }; } private generateOrderByOnAggregateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ): GraphQLInputFieldConfigMap { const allAggregatedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - fieldMetadata.isNullable = true; + for (const fieldMetadata of fields) { + const modifiedFieldMetadata = { + ...fieldMetadata, + isNullable: true, + }; const typeOptions = computeFieldInputTypeOptions( - fieldMetadata, + modifiedFieldMetadata, GqlInputTypeDefinitionKind.OrderBy, ); let generatedFields; - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { generatedFields = this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldOrderByInputType( { fieldMetadata, typeOptions, - objectMetadataMaps: this.objectMetadataMaps, + context, }, ); } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { @@ -117,7 +122,7 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { } const aggregateInputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.OrderByWithGroupBy.toString()}AggregateInput`, + name: `${pascalCase(objectNameSingular)}${GqlInputTypeDefinitionKind.OrderByWithGroupBy.toString()}AggregateInput`, description: 'Aggregate-based ordering', fields: () => allAggregatedFields, }) as GraphQLInputObjectType; @@ -131,18 +136,19 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { } private generateOrderByOnDimensionValuesFields( - objectMetadata: ObjectMetadataEntity, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ) { return this.objectMetadataOrderByBaseGenerator.generateFields({ - objectMetadata, + fields, logger: this.logger, orderByDateGranularity: true, - objectMetadataMaps: this.objectMetadataMaps, + context, }); } private generateCompositeFieldOrderByInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const key = computeCompositeFieldInputTypeKey( @@ -169,7 +175,7 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { } private generateAtomicFieldOrderByInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, typeOptions: TypeOptions, ) { const orderByType = this.typeMapperService.mapToOrderByType( @@ -193,7 +199,7 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator { } private generateAggregateFieldOrderByInputType( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ) { const aggregations = getAvailableAggregationsFromObjectFields([ fieldMetadata, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-connect-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-connect-gql-input-type.generator.ts index 5e568d983d9..b3eb6a407b4 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-connect-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-connect-gql-input-type.generator.ts @@ -8,15 +8,16 @@ import { GraphQLString, } from 'graphql'; import { RELATION_NESTED_QUERY_KEYWORDS } from 'twenty-shared/constants'; -import { getUniqueConstraintsFields } from 'twenty-shared/utils'; import { compositeTypeDefinitions } from 'twenty-shared/types'; +import { getUniqueConstraintsFields, isDefined } from 'twenty-shared/utils'; import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeRelationConnectInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-relation-connect-input-type-key.util'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -26,44 +27,76 @@ export class RelationConnectGqlInputTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { - const relationConnectGqlInputType = - this.generateRelationConnectInputType(objectMetadata); - const key = computeRelationConnectInputTypeKey(objectMetadata.id); + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, + ) { + const relationConnectGqlInputType = this.generateRelationConnectInputType( + flatObjectMetadata, + fields, + context, + ); + const key = computeRelationConnectInputTypeKey(flatObjectMetadata.id); this.gqlTypesStorage.addGqlType(key, relationConnectGqlInputType); } private generateRelationConnectInputType( - objectMetadata: ObjectMetadataEntity, + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ): GraphQLInputObjectType { return new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}RelationInput`, + name: `${pascalCase(flatObjectMetadata.nameSingular)}RelationInput`, fields: () => ({ [RELATION_NESTED_QUERY_KEYWORDS.CONNECT]: { type: new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}ConnectInput`, - fields: this.generateRelationWhereInputType(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}ConnectInput`, + fields: this.generateRelationWhereInputType( + flatObjectMetadata, + fields, + context, + ), }), - description: `Connect to a ${objectMetadata.nameSingular} record`, + description: `Connect to a ${flatObjectMetadata.nameSingular} record`, }, [RELATION_NESTED_QUERY_KEYWORDS.DISCONNECT]: { type: GraphQLBoolean, - description: `Disconnect from a ${objectMetadata.nameSingular} record`, + description: `Disconnect from a ${flatObjectMetadata.nameSingular} record`, }, }), }); } private generateRelationWhereInputType( - objectMetadata: ObjectMetadataEntity, + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + context: SchemaGenerationContext, ): Record { - const uniqueConstraints = getUniqueConstraintsFields< - FieldMetadataEntity, - ObjectMetadataEntity - >(objectMetadata); + const { flatIndexMaps } = context; - const fields: Record< + const indexMetadatas = flatObjectMetadata.indexMetadataIds + .map((indexId) => flatIndexMaps.byId[indexId]) + .filter(isDefined) + .map((flatIndex) => ({ + id: flatIndex.id, + isUnique: flatIndex.isUnique, + indexFieldMetadatas: flatIndex.flatIndexFieldMetadatas, + })); + + const objectWithIndexes = { + id: flatObjectMetadata.id, + indexMetadatas, + fields, + }; + + const uniqueConstraints = getUniqueConstraintsFields< + FlatFieldMetadata, + typeof objectWithIndexes + >(objectWithIndexes); + + const inputFields: Record< string, { type: GraphQLInputType; description: string } > = {}; @@ -101,11 +134,11 @@ export class RelationConnectGqlInputTypeGenerator { }); const compositeInputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${pascalCase(field.name)}WhereInput`, + name: `${pascalCase(flatObjectMetadata.nameSingular)}${pascalCase(field.name)}WhereInput`, fields: () => compositeFields, }); - fields[field.name] = { + inputFields[field.name] = { type: compositeInputType, description: `Connect by ${field.label || field.name}`, }; @@ -116,7 +149,7 @@ export class RelationConnectGqlInputTypeGenerator { { settings: field.settings, isIdField: field.name === 'id' }, ); - fields[field.name] = { + inputFields[field.name] = { type: scalarType || GraphQLString, description: `Connect by ${field.label || field.name}`, }; @@ -127,15 +160,15 @@ export class RelationConnectGqlInputTypeGenerator { return { [RELATION_NESTED_QUERY_KEYWORDS.CONNECT_WHERE]: { type: new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}WhereUniqueInput`, - fields: () => fields, + name: `${pascalCase(flatObjectMetadata.nameSingular)}WhereUniqueInput`, + fields: () => inputFields, }), - description: `Find a ${objectMetadata.nameSingular} record based on its unique fields: ${this.formatConstraints(uniqueConstraints)}`, + description: `Find a ${flatObjectMetadata.nameSingular} record based on its unique fields: ${this.formatConstraints(uniqueConstraints)}`, }, }; } - private formatConstraints(constraints: FieldMetadataEntity[][]) { + private formatConstraints(constraints: FlatFieldMetadata[][]) { return constraints .map((constraint) => constraint.map((field) => field.name).join(' and ')) .join(' or '); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts index 57b17b0ac3d..381d218079b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator.ts @@ -14,11 +14,11 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; import { computeRelationConnectInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-relation-connect-input-type-key.util'; import { extractGraphQLRelationFieldNames } from 'src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; @Injectable() export class RelationFieldMetadataGqlInputTypeGenerator { @@ -35,7 +35,7 @@ export class RelationFieldMetadataGqlInputTypeGenerator { fieldMetadata, typeOptions, }: { - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >; typeOptions: TypeOptions; @@ -77,7 +77,7 @@ export class RelationFieldMetadataGqlInputTypeGenerator { fieldMetadata, typeOptions, }: { - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >; typeOptions: TypeOptions; @@ -118,13 +118,13 @@ export class RelationFieldMetadataGqlInputTypeGenerator { public generateSimpleRelationFieldOrderByInputType({ fieldMetadata, typeOptions, - objectMetadataMaps, + context, }: { - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >; typeOptions: TypeOptions; - objectMetadataMaps?: ObjectMetadataMaps; + context?: SchemaGenerationContext; }) { if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) return {}; @@ -158,10 +158,12 @@ export class RelationFieldMetadataGqlInputTypeGenerator { if ( isDefined(fieldMetadata.relationTargetObjectMetadataId) && - isDefined(objectMetadataMaps) + isDefined(context) ) { const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + context.flatObjectMetadataMaps.byId[ + fieldMetadata.relationTargetObjectMetadataId + ]; if (isDefined(targetObjectMetadata)) { const targetOrderByInputTypeKey = computeObjectMetadataInputTypeKey( @@ -189,10 +191,10 @@ export class RelationFieldMetadataGqlInputTypeGenerator { } public generateSimpleRelationFieldGroupByInputType( - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >, - objectMetadataMaps?: ObjectMetadataMaps, + context?: SchemaGenerationContext, ): GraphQLInputFieldConfigMap { if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) return {}; @@ -204,10 +206,12 @@ export class RelationFieldMetadataGqlInputTypeGenerator { if ( isDefined(fieldMetadata.relationTargetObjectMetadataId) && - isDefined(objectMetadataMaps) + isDefined(context) ) { const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + context.flatObjectMetadataMaps.byId[ + fieldMetadata.relationTargetObjectMetadataId + ]; if (isDefined(targetObjectMetadata)) { const targetGroupByInputTypeKey = computeObjectMetadataInputTypeKey( @@ -238,7 +242,7 @@ export class RelationFieldMetadataGqlInputTypeGenerator { fieldMetadata, typeOptions, }: { - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >; typeOptions: TypeOptions; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator.ts index 35bb08e441b..3fe76af2702 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator.ts @@ -15,14 +15,16 @@ import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-sch import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator'; import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util'; import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util'; import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util'; import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -36,15 +38,20 @@ export class ObjectMetadataUpdateGqlInputTypeGenerator { private readonly typeMapperService: TypeMapperService, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + _context: SchemaGenerationContext, + ) { const inputType = new GraphQLInputObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Update.toString()}Input`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.Update.toString()}Input`, + description: flatObjectMetadata.description, + fields: () => + this.generateFields(flatObjectMetadata.nameSingular, fields), }) as GraphQLInputObjectType; const key = computeObjectMetadataInputTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, GqlInputTypeDefinitionKind.Update, ); @@ -52,21 +59,25 @@ export class ObjectMetadataUpdateGqlInputTypeGenerator { } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], ): GraphQLInputFieldConfigMap { const allGeneratedFields: GraphQLInputFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - fieldMetadata.isNullable = true; + for (const fieldMetadata of fields) { + const modifiedFieldMetadata = { + ...fieldMetadata, + isNullable: true, + }; const typeOptions = computeFieldInputTypeOptions( - fieldMetadata, + modifiedFieldMetadata, GqlInputTypeDefinitionKind.Update, ); let generatedFields; - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { generatedFields = { ...this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldCreateOrUpdateInputType( { @@ -90,7 +101,7 @@ export class ObjectMetadataUpdateGqlInputTypeGenerator { if (isEnumFieldMetadataType(fieldMetadata.type)) { const key = computeEnumFieldGqlTypeKey( - objectMetadata.nameSingular, + objectNameSingular, fieldMetadata.name, ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/aggregation-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/aggregation-type.generator.ts index ad9617e42e6..8ef1600f758 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/aggregation-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/aggregation-type.generator.ts @@ -4,18 +4,17 @@ import { type AggregationField, getAvailableAggregationsFromObjectFields, } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; type AggregationGraphQLType = Pick; @Injectable() export class AggregationObjectTypeGenerator { public generate( - objectMetadata: ObjectMetadataEntity, + flatFields: FlatFieldMetadata[], ): Record { - const availableAggregations = getAvailableAggregationsFromObjectFields( - objectMetadata.fields, - ); + const availableAggregations = + getAvailableAggregationsFromObjectFields(flatFields); return Object.entries(availableAggregations).reduce< Record diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator.ts index 690772e8927..bf6cd3fdc16 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator.ts @@ -10,7 +10,8 @@ import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-build import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -23,51 +24,52 @@ export class ConnectionGqlObjectTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + flatFields: FlatFieldMetadata[], + ) { const kind = ObjectTypeDefinitionKind.Connection; const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, kind, ); this.gqlTypesStorage.addGqlType( key, new GraphQLObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${kind.toString()}`, + description: flatObjectMetadata.description, + fields: () => this.generateFields(flatObjectMetadata, flatFields), }), ); } private generateFields( - objectMetadata: ObjectMetadataEntity, + flatObjectMetadata: FlatObjectMetadata, + flatFields: FlatFieldMetadata[], ): GraphQLOutputTypeFieldConfigMap { const fields: GraphQLOutputTypeFieldConfigMap = {}; const aggregatedFields = - this.aggregationObjectTypeGenerator.generate(objectMetadata); + this.aggregationObjectTypeGenerator.generate(flatFields); Object.assign(fields, aggregatedFields); const edgeType = this.gqlTypesStorage.getGqlTypeByKey( computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, ObjectTypeDefinitionKind.Edge, ), ); if (!isDefined(edgeType) || isInputObjectType(edgeType)) { this.logger.error( - `Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, - { - objectMetadata, - }, + `Edge type for ${flatObjectMetadata.nameSingular} was not found. Please, check if you have defined it.`, ); throw new Error( - `Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, + `Edge type for ${flatObjectMetadata.nameSingular} was not found. Please, check if you have defined it.`, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator.ts index aa9f1f418c9..9392adde925 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator.ts @@ -9,7 +9,7 @@ import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-build import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -21,30 +21,30 @@ export class EdgeGqlObjectTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore(flatObjectMetadata: FlatObjectMetadata) { const kind = ObjectTypeDefinitionKind.Edge; const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, kind, ); this.gqlTypesStorage.addGqlType( key, new GraphQLObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${kind.toString()}`, + description: flatObjectMetadata.description, + fields: () => this.generateFields(flatObjectMetadata.nameSingular), }), ); } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, ): GraphQLOutputTypeFieldConfigMap { const fields: GraphQLOutputTypeFieldConfigMap = {}; const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + objectNameSingular, ObjectTypeDefinitionKind.Plain, ); @@ -52,14 +52,11 @@ export class EdgeGqlObjectTypeGenerator { if (!isDefined(objectType) || isInputObjectType(objectType)) { this.logger.error( - `Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, - { - objectMetadata, - }, + `Node type for ${objectNameSingular} was not found. Please, check if you have defined it.`, ); throw new Error( - `Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, + `Node type for ${objectNameSingular} was not found. Please, check if you have defined it.`, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator.ts index 5a04ca46769..f70315776c5 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator.ts @@ -9,7 +9,7 @@ import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-build import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -23,46 +23,43 @@ export class GroupByConnectionGqlObjectTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore(flatObjectMetadata: FlatObjectMetadata) { const kind = ObjectTypeDefinitionKind.GroupByConnection; const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, kind, ); this.gqlTypesStorage.addGqlType( key, new GraphQLObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, - description: objectMetadata.description, - fields: () => this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${kind.toString()}`, + description: flatObjectMetadata.description, + fields: () => this.generateFields(flatObjectMetadata.nameSingular), }), ); } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, ): GraphQLOutputTypeFieldConfigMap { const fields: GraphQLOutputTypeFieldConfigMap = {}; const connection = this.gqlTypesStorage.getGqlTypeByKey( computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + objectNameSingular, ObjectTypeDefinitionKind.Connection, ), ); if (!isDefined(connection) || !isObjectType(connection)) { this.logger.error( - `Connection type for ${objectMetadata.nameSingular} was not found.`, - { - objectMetadata, - }, + `Connection type for ${objectNameSingular} was not found.`, ); throw new Error( - `Connection type for ${objectMetadata.nameSingular} was not found.`, + `Connection type for ${objectNameSingular} was not found.`, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator.ts index 834d2ca87ec..be61a9db536 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator.ts @@ -16,11 +16,11 @@ import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspac import { computeCompositeFieldObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-object-type-key.util'; import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { pascalCase } from 'src/utils/pascal-case'; @Injectable() @@ -35,29 +35,33 @@ export class ObjectMetadataGqlObjectTypeGenerator { private readonly typeMapperService: TypeMapperService, ) {} - public buildAndStore(objectMetadata: ObjectMetadataEntity) { + public buildAndStore( + flatObjectMetadata: FlatObjectMetadata, + fields: FlatFieldMetadata[], + ) { const kind = ObjectTypeDefinitionKind.Plain; const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, ObjectTypeDefinitionKind.Plain, ); this.gqlTypesStorage.addGqlType( key, new GraphQLObjectType({ - name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, - description: objectMetadata.description, - fields: this.generateFields(objectMetadata), + name: `${pascalCase(flatObjectMetadata.nameSingular)}${kind.toString()}`, + description: flatObjectMetadata.description, + fields: this.generateFields(flatObjectMetadata.nameSingular, fields), }), ); } private generateFields( - objectMetadata: ObjectMetadataEntity, + objectNameSingular: string, + fields: FlatFieldMetadata[], ): GraphQLOutputTypeFieldConfigMap { const allGeneratedFields: GraphQLOutputTypeFieldConfigMap = {}; - for (const field of objectMetadata.fields) { + for (const field of fields) { const typeFactoryOptions = { nullable: field.isNullable ?? undefined, isArray: field.type === FieldMetadataType.MULTI_SELECT, @@ -66,13 +70,11 @@ export class ObjectMetadataGqlObjectTypeGenerator { isIdField: false, }; - if (isMorphOrRelationFieldMetadataType(field.type)) { + if (isMorphOrRelationFlatFieldMetadata(field)) { const relationFieldObjectType = this.relationFieldMetadataGqlObjectTypeGenerator.generateRelationFieldObjectType( { - fieldMetadata: field as FieldMetadataEntity< - FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION - >, + fieldMetadata: field, typeOptions: typeFactoryOptions, }, ); @@ -96,7 +98,7 @@ export class ObjectMetadataGqlObjectTypeGenerator { !isDefined(compositeFieldObjectType) || isInputObjectType(compositeFieldObjectType) ) { - const message = `Could not find a GraphQL output type for ${field.name} composite field metadata of object ${objectMetadata.nameSingular}`; + const message = `Could not find a GraphQL output type for ${field.name} composite field metadata of object ${objectNameSingular}`; this.logger.error(message, { type: field.type, @@ -109,7 +111,7 @@ export class ObjectMetadataGqlObjectTypeGenerator { type = compositeFieldObjectType; } else if (isEnumFieldMetadataType(field.type)) { const enumFieldEnumTypeKey = computeEnumFieldGqlTypeKey( - objectMetadata.nameSingular, + objectNameSingular, field.name, ); @@ -120,7 +122,7 @@ export class ObjectMetadataGqlObjectTypeGenerator { !isDefined(enumFieldEnumType) || isInputObjectType(enumFieldEnumType) ) { - const message = `Could not find a GraphQL output type for ${field.name} enum field metadata of object ${objectMetadata.nameSingular}`; + const message = `Could not find a GraphQL output type for ${field.name} enum field metadata of object ${objectNameSingular}`; this.logger.error(message, { type: field.type, @@ -138,7 +140,7 @@ export class ObjectMetadataGqlObjectTypeGenerator { ); if (!isDefined(type)) { - const message = `Could not find a GraphQL output type for ${field.name} scalar field metadata of object ${objectMetadata.nameSingular}`; + const message = `Could not find a GraphQL output type for ${field.name} scalar field metadata of object ${objectNameSingular}`; this.logger.error(message, { type: field.type, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator.ts index 1bda5a5e23e..7f7fcf9ba83 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator.ts @@ -15,12 +15,13 @@ import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schem import { ArgsTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/args-type/args-type.generator'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util'; -import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; @Injectable() export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { @@ -34,11 +35,12 @@ export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { ) {} public buildAndStore( - objectMetadata: ObjectMetadataEntity, - objectMetadataCollection: ObjectMetadataEntity[], + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadatas: FlatFieldMetadata[], + context: SchemaGenerationContext, ) { const key = computeObjectMetadataObjectTypeKey( - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, ObjectTypeDefinitionKind.Plain, ); @@ -46,28 +48,26 @@ export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { if (!isDefined(gqlType) || !isObjectType(gqlType)) { this.logger.error( - `Could not find a GraphQL type for ${objectMetadata.id.toString()}`, - { - objectMetadata, - }, + `Could not find a GraphQL type for ${flatObjectMetadata.id.toString()}`, ); throw new Error( - `Could not find a GraphQL type for ${objectMetadata.id.toString()}`, + `Could not find a GraphQL type for ${flatObjectMetadata.id.toString()}`, ); } + const containsRelationOrMorphField = flatFieldMetadatas.some((field) => + isMorphOrRelationFieldMetadataType(field.type), + ); + // Security check to avoid extending an object that does not need to be extended - if (!objectContainsRelationField(objectMetadata)) { + if (!containsRelationOrMorphField) { this.logger.error( - `This object does not need to be extended: ${objectMetadata.id.toString()}`, - { - objectMetadata, - }, + `This object does not need to be extended: ${flatObjectMetadata.id.toString()}`, ); throw new Error( - `This object does not need to be extended: ${objectMetadata.id.toString()}`, + `This object does not need to be extended: ${flatObjectMetadata.id.toString()}`, ); } @@ -81,66 +81,57 @@ export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { ...config, fields: () => ({ ...config.fields, - ...this.generateFields(objectMetadata, objectMetadataCollection), + ...this.generateFields(flatFieldMetadatas, context), }), }), ); } private generateFields( - objectMetadata: ObjectMetadataEntity, - objectMetadataCollection: ObjectMetadataEntity[], + flatFieldMetadatas: FlatFieldMetadata[], + context: SchemaGenerationContext, ): GraphQLOutputTypeFieldConfigMap { - const fields: GraphQLOutputTypeFieldConfigMap = {}; + const outputFields: GraphQLOutputTypeFieldConfigMap = {}; - for (const fieldMetadata of objectMetadata.fields) { - // Ignore non-relation fields as they are already defined - const isRelation = - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.RELATION, - ) || - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.MORPH_RELATION, - ); - - if (!isRelation) { + for (const flatFieldMetadata of flatFieldMetadatas) { + if (!isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) { continue; } - if (!fieldMetadata.settings) { + if (!flatFieldMetadata.settings) { throw new Error( - `Field Metadata of type RELATION or MORPH_RELATION with id ${fieldMetadata.id} has no settings`, + `Field Metadata of type RELATION or MORPH_RELATION with id ${flatFieldMetadata.id} has no settings`, ); } - if (!fieldMetadata.relationTargetObjectMetadataId) { + if (!flatFieldMetadata.relationTargetObjectMetadataId) { throw new Error( - `Field Metadata of type RELATION or MORPH_RELATION with id ${fieldMetadata.id} has no relation target object metadata id`, + `Field Metadata of type RELATION or MORPH_RELATION with id ${flatFieldMetadata.id} has no relation target object metadata id`, ); } - const objectMetadataTarget = objectMetadataCollection.find( - (objectMetadata) => - objectMetadata.id === fieldMetadata.relationTargetObjectMetadataId, - ); + const objectMetadataTarget = + context.flatObjectMetadataMaps.byId[ + flatFieldMetadata.relationTargetObjectMetadataId + ]; if (!isDefined(objectMetadataTarget)) { throw new Error( - `Field Metadata of type RELATION or MORPH_RELATION with id ${fieldMetadata.id} has no relation target object metadata`, + `Field Metadata of type RELATION or MORPH_RELATION with id ${flatFieldMetadata.id} has no relation target object metadata`, ); } const targetObjectMetadataGqlObjectType = this.fetchTargetObjectMetadataGqlObjectType( - fieldMetadata, + flatFieldMetadata, objectMetadataTarget, ); let argsType: GraphQLFieldConfigArgumentMap | undefined = undefined; - if (fieldMetadata.settings.relationType === RelationType.ONE_TO_MANY) { + if ( + flatFieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY + ) { const args = getResolverArgs('findMany'); argsType = this.argsTypeGenerator.generate({ @@ -149,31 +140,31 @@ export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { }); } - fields[fieldMetadata.name] = { + outputFields[flatFieldMetadata.name] = { type: targetObjectMetadataGqlObjectType, args: argsType, - description: fieldMetadata.description, + description: flatFieldMetadata.description, }; } - return fields; + return outputFields; } private fetchTargetObjectMetadataGqlObjectType( - fieldMetadata: FieldMetadataEntity< + flatFieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >, - objectMetadataTarget: ObjectMetadataEntity, + objectMetadataTarget: FlatObjectMetadata, ): GraphQLOutputType { - if (!fieldMetadata.settings) { + if (!flatFieldMetadata.settings) { throw new Error( - `Field Metadata of type RELATION or MORPH_RELATION with id ${fieldMetadata.id} has no settings`, + `Field Metadata of type RELATION or MORPH_RELATION with id ${flatFieldMetadata.id} has no settings`, ); } const key = computeObjectMetadataObjectTypeKey( objectMetadataTarget.nameSingular, - fieldMetadata.settings.relationType === RelationType.ONE_TO_MANY + flatFieldMetadata.settings.relationType === RelationType.ONE_TO_MANY ? ObjectTypeDefinitionKind.Connection : ObjectTypeDefinitionKind.Plain, ); @@ -186,13 +177,12 @@ export class ObjectMetadataWithRelationsGqlObjectTypeGenerator { !isObjectType(targetObjectMetadataGqlObjectType) ) { this.logger.error( - `Could not find a relation type for ${fieldMetadata.id}`, - { - fieldMetadata, - }, + `Could not find a relation type for ${flatFieldMetadata.id}`, ); - throw new Error(`Could not find a relation type for ${fieldMetadata.id}`); + throw new Error( + `Could not find a relation type for ${flatFieldMetadata.id}`, + ); } return targetObjectMetadataGqlObjectType; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/relation-field-metadata-gql-object-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/relation-field-metadata-gql-object-type.generator.ts index 89cbcc8b2dc..fa6fd522556 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/relation-field-metadata-gql-object-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/relation-field-metadata-gql-object-type.generator.ts @@ -8,7 +8,7 @@ import { TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { extractGraphQLRelationFieldNames } from 'src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; @Injectable() export class RelationFieldMetadataGqlObjectTypeGenerator { @@ -22,7 +22,7 @@ export class RelationFieldMetadataGqlObjectTypeGenerator { fieldMetadata, typeOptions, }: { - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >; typeOptions: TypeOptions; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator.ts index cb0d144643a..84ef8655467 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator.ts @@ -6,7 +6,7 @@ import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/work import { GqlOperation } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-operation.enum'; import { RootTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; @Injectable() export class MutationTypeGenerator { @@ -15,9 +15,9 @@ export class MutationTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - buildAndStore(objectMetadataCollection: ObjectMetadataEntity[]) { + buildAndStore(context: SchemaGenerationContext) { return this.rootTypeGenerator.buildAndStore( - objectMetadataCollection, + context, [...workspaceResolverBuilderMethodNames.mutations], GqlOperation.Mutation, ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator.ts index 30c3fca1ca1..368cede098e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator.ts @@ -6,7 +6,7 @@ import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/work import { GqlOperation } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-operation.enum'; import { RootTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; @Injectable() export class QueryTypeGenerator { @@ -15,9 +15,9 @@ export class QueryTypeGenerator { private readonly gqlTypesStorage: GqlTypesStorage, ) {} - async buildAndStore(objectMetadataCollection: ObjectMetadataEntity[]) { + async buildAndStore(context: SchemaGenerationContext) { return this.rootTypeGenerator.buildAndStore( - objectMetadataCollection, + context, workspaceResolverBuilderMethodNames.queries, GqlOperation.Query, ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator.ts index 131d3357b3e..e5e660227ad 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/root-type.generator.ts @@ -11,10 +11,11 @@ import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schem import { ArgsTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/args-type/args-type.generator'; import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; import { GraphQLRootTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types'; import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util'; import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getResolverName } from 'src/engine/utils/get-resolver-name.util'; @Injectable() @@ -29,7 +30,7 @@ export class RootTypeGenerator { ) {} buildAndStore( - objectMetadataCollection: ObjectMetadataEntity[], + context: SchemaGenerationContext, workspaceResolverMethodNames: readonly WorkspaceResolverBuilderMethodNames[], objectTypeName: GqlOperation, ) { @@ -47,6 +48,10 @@ export class RootTypeGenerator { ); } + const objectMetadataCollection = Object.values( + context.flatObjectMetadataMaps.byId, + ).filter(isDefined); + this.gqlTypesStorage.addGqlType( objectTypeName, new GraphQLObjectType({ @@ -60,7 +65,7 @@ export class RootTypeGenerator { } private generateFields( - objectMetadataCollection: ObjectMetadataEntity[], + objectMetadataCollection: FlatObjectMetadata[], workspaceResolverMethodNames: readonly WorkspaceResolverBuilderMethodNames[], ): GraphQLRootTypeFieldConfigMap { const fieldConfigMap: GraphQLRootTypeFieldConfigMap = {}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts index 470fc18cca8..1b7737ac387 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts @@ -1,9 +1,12 @@ import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export interface WorkspaceSchemaBuilderContext { authContext: AuthContext; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type.ts new file mode 100644 index 00000000000..1919f5feada --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type.ts @@ -0,0 +1,10 @@ +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +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'; + +export type SchemaGenerationContext = { + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatIndexMaps: FlatEntityMaps; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util.ts index 706c73fa250..d07e17d4e01 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util.ts @@ -2,15 +2,15 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum'; import { type TypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -export const computeFieldInputTypeOptions = ( - fieldMetadata: FieldMetadataEntity, +export const computeFieldInputTypeOptions = ( + fieldMetadata: FlatFieldMetadata, kind: GqlInputTypeDefinitionKind, ): TypeOptions => { return { nullable: fieldMetadata.isNullable ?? undefined, - defaultValue: fieldMetadata.defaultValue, + defaultValue: fieldMetadata.defaultValue ?? undefined, isArray: kind !== GqlInputTypeDefinitionKind.Filter && fieldMetadata.type === FieldMetadataType.MULTI_SELECT, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts index 8fb0c525d35..7426cafd880 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts @@ -1,13 +1,14 @@ import { type FieldMetadataType } from 'twenty-shared/types'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export const extractGraphQLRelationFieldNames = ( - fieldMetadata: FieldMetadataEntity< + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >, ) => { - const joinColumnName = fieldMetadata.settings?.joinColumnName; + const settings = fieldMetadata.settings; + const joinColumnName = settings?.joinColumnName; if (!joinColumnName) { throw new Error('Join column name is not defined'); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts index 92c59a84913..3c872a52465 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util.ts @@ -6,7 +6,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { capitalize, isFieldMetadataDateKind } from 'twenty-shared/utils'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { getSubfieldsForAggregateOperation } from 'src/engine/twenty-orm/utils/get-subfields-for-aggregate-operation.util'; export type AggregationField = { @@ -20,7 +20,7 @@ export type AggregationField = { }; export const getAvailableAggregationsFromObjectFields = ( - fields: FieldMetadataEntity[], + fields: FlatFieldMetadata[], ): Record => { return fields.reduce>( (acc, field) => { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util.ts new file mode 100644 index 00000000000..3c7fcfc5967 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util.ts @@ -0,0 +1,14 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +export const getFlatFieldsFromFlatObjectMetadata = ( + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, +): FlatFieldMetadata[] => { + return flatObjectMetadata.fieldMetadataIds + .map((fieldId) => flatFieldMetadataMaps.byId[fieldId]) + .filter(isDefined); +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util.ts deleted file mode 100644 index 58a4ea5a136..00000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { isMorphRelationFieldMetadataType } from 'src/engine/utils/is-morph-relation-field-metadata-type.util'; - -export const objectContainsMorphRelationField = ( - objectMetadata: ObjectMetadataEntity, -): boolean => { - return objectMetadata.fields.some((field) => - isMorphRelationFieldMetadataType(field.type), - ); -}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field.ts deleted file mode 100644 index 33e70833933..00000000000 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; - -export const objectContainsRelationField = ( - objectMetadata: ObjectMetadataEntity, -): boolean => { - return objectMetadata.fields.some((field) => - isMorphOrRelationFieldMetadataType(field.type), - ); -}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts index 980d6f79d28..bc2c62503a7 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts @@ -6,7 +6,7 @@ import { GqlTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builde import { OrphanedTypesGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/orphaned-types.generator'; import { MutationTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator'; import { QueryTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/query-type.generator'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type SchemaGenerationContext } from 'src/engine/api/graphql/workspace-schema-builder/types/schema-generation-context.type'; @Injectable() export class WorkspaceGraphQLSchemaGenerator { @@ -18,9 +18,9 @@ export class WorkspaceGraphQLSchemaGenerator { ) {} async generateSchema( - objectMetadataCollection: ObjectMetadataEntity[], + context: SchemaGenerationContext, ): Promise { - await this.gqlTypeGenerator.buildAndStore(objectMetadataCollection); + await this.gqlTypeGenerator.buildAndStore(context); // Assemble schema const schema = new GraphQLSchema({ diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts index 91cf818db5e..988dd0890a5 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts @@ -14,7 +14,6 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FlatEntityMapsException, @@ -22,11 +21,7 @@ import { } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; -import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; -import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; @Injectable() export class WorkspaceSchemaFactory { @@ -37,7 +32,6 @@ export class WorkspaceSchemaFactory { private readonly workspaceResolverFactory: WorkspaceResolverFactory, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, - private readonly featureFlagService: FeatureFlagService, ) {} async createGraphQLSchema(authContext: AuthContext): Promise { @@ -82,8 +76,12 @@ export class WorkspaceSchemaFactory { ); } - const workspaceFeatureFlagsMap = - await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId); + if (!isDefined(flatFieldMetadataMaps)) { + throw new FlatEntityMapsException( + 'Field metadata collection not found', + FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND, + ); + } let metadataVersion = await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); @@ -99,37 +97,7 @@ export class WorkspaceSchemaFactory { const { idByNameSingular } = buildObjectIdByNameMaps( flatObjectMetadataMaps, ); - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, - }; - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } - - const objectMetadataCollection = Object.values(objectMetadataMaps.byId) - .filter(isDefined) - .map((objectMetadataItem) => ({ - ...objectMetadataItem, - fields: Object.values(objectMetadataItem.fieldsById), - indexes: objectMetadataItem.indexMetadatas, - })) - .filter((objectMetadata) => { - return !shouldExcludeFromWorkspaceApi( - objectMetadata, - standardObjectMetadataDefinitions, - workspaceFeatureFlagsMap, - ); - }); - - // Get typeDefs from cache let typeDefs = await this.workspaceCacheStorageService.getGraphQLTypeDefs( authContext.workspace.id, metadataVersion, @@ -140,12 +108,13 @@ export class WorkspaceSchemaFactory { metadataVersion, ); - // If typeDefs are not cached, generate them if (!typeDefs || !usedScalarNames) { const autoGeneratedSchema = - await this.workspaceGraphQLSchemaGenerator.generateSchema( - objectMetadataCollection, - ); + await this.workspaceGraphQLSchemaGenerator.generateSchema({ + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, + }); usedScalarNames = this.scalarsExplorerService.getUsedScalarNames(autoGeneratedSchema); @@ -165,7 +134,9 @@ export class WorkspaceSchemaFactory { const autoGeneratedResolvers = await this.workspaceResolverFactory.create( authContext, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + idByNameSingular, workspaceResolverBuilderMethodNames, ); const scalarsResolvers = diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts index 800d9a9c3ec..84a1b7b9897 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts @@ -22,15 +22,16 @@ import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspac import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { PermissionsException, PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; @@ -128,21 +129,24 @@ export abstract class RestApiBaseHandler { async computeSelectedFields({ authContext, depth, - objectMetadataMapItem, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { authContext: WorkspaceAuthContext; depth?: Depth | undefined; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise { const { objectsPermissions } = await this.getObjectsPermissions(authContext); return this.restToCommonSelectedFieldsHandler.computeFromDepth({ objectsPermissions, - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, depth, }); } @@ -150,15 +154,21 @@ export abstract class RestApiBaseHandler { async buildCommonOptions(request: AuthenticatedRequest) { const { object: parsedObject } = parseCorePath(request); - const { objectMetadataMaps, objectMetadataMapItem } = - await this.getObjectMetadata(request, parsedObject); + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = await this.getObjectMetadata(request, parsedObject); const authContext = this.getAuthContextFromRequest(request); return { authContext, - objectMetadataItemWithFieldMaps: objectMetadataMapItem, - objectMetadataMaps: objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }; } @@ -166,8 +176,11 @@ export abstract class RestApiBaseHandler { request: AuthenticatedRequest, parsedObject: string, ): Promise<{ - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatIndexMaps: FlatEntityMaps; + objectIdByNameSingular: Record; }> { const { workspace } = await this.accessTokenService.validateTokenByRequest(request); @@ -242,16 +255,10 @@ export abstract class RestApiBaseHandler { const workspaceFeatureFlagsMap = await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspace.id); - const objectMetadataItem = buildObjectMetadataItemWithFieldMaps( - flatObjectMetadataItem, - flatFieldMetadataMaps, - flatIndexMaps, - ); - // Check if this entity is workspace-gated and should be blocked from workspace API if ( shouldExcludeFromWorkspaceApi( - objectMetadataItem, + flatObjectMetadataItem, standardObjectMetadataDefinitions, workspaceFeatureFlagsMap, ) @@ -261,24 +268,12 @@ export abstract class RestApiBaseHandler { ); } - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, - }; - - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } - return { - objectMetadataMaps, - objectMetadataMapItem: objectMetadataItem, + flatObjectMetadata: flatObjectMetadataItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, + objectIdByNameSingular: idByNameSingular, }; } } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts index 0cb0b8a0ec3..61b40ae6c40 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-many.handler.ts @@ -23,14 +23,17 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -38,15 +41,14 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler { { data, selectedFields, upsert }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - records, - objectMetadataItemWithFieldMaps.namePlural, - ); + return this.formatRestResponse(records, flatObjectMetadata.namePlural); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts index 5694046a476..97f4b67de76 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-create-one.handler.ts @@ -24,14 +24,17 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -39,15 +42,14 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler { { data, selectedFields, upsert }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts index 85e19dd1c22..e7fff3d16ad 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts @@ -31,23 +31,27 @@ export class RestApiDeleteManyHandler extends RestApiBaseHandler { ); } - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - await this.buildCommonOptions(request); + const { + authContext, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = await this.buildCommonOptions(request); try { const records = await this.commonDeleteManyQueryRunnerService.execute( { filter, selectedFields: { id: true } }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - records, - objectMetadataItemWithFieldMaps.namePlural, - ); + return this.formatRestResponse(records, flatObjectMetadata.namePlural); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts index 93e95b030ca..9f85b317e9a 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-one.handler.ts @@ -23,23 +23,24 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const record = await this.commonDeleteOneQueryRunnerService.execute( { id, selectedFields: { id: true } }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts index 3f16703d624..5e675588373 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts @@ -31,23 +31,27 @@ export class RestApiDestroyManyHandler extends RestApiBaseHandler { ); } - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - await this.buildCommonOptions(request); + const { + authContext, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = await this.buildCommonOptions(request); try { const records = await this.commonDestroyManyQueryRunnerService.execute( { filter, selectedFields: { id: true } }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - records, - objectMetadataItemWithFieldMaps.namePlural, - ); + return this.formatRestResponse(records, flatObjectMetadata.namePlural); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts index b5feb4be1c2..572b95e65d1 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-one.handler.ts @@ -23,23 +23,24 @@ export class RestApiDestroyOneHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const record = await this.commonDestroyOneQueryRunnerService.execute( { id, selectedFields: { id: true } }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts index 66b0125baa8..ba1e29a308a 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-duplicates.handler.ts @@ -22,14 +22,17 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -38,14 +41,16 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler { { data, ids, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); return this.formatRestResponse( duplicateConnections, - objectMetadataItemWithFieldMaps.nameSingular, + flatObjectMetadata.nameSingular, ); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts index 7ffda7a5816..6d6e9909e44 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-many.handler.ts @@ -29,14 +29,17 @@ export class RestApiFindManyHandler extends RestApiBaseHandler { const parsedArgs = this.parseRequestArgs(request); const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth: parsedArgs.depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -48,15 +51,17 @@ export class RestApiFindManyHandler extends RestApiBaseHandler { }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); return this.formatRestResponse( records, aggregatedValues, - objectMetadataItemWithFieldMaps.namePlural, + flatObjectMetadata.namePlural, pageInfo, ); } catch (error) { diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts index a018ed6660a..6f5cabdfe1d 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-find-one.handler.ts @@ -22,14 +22,17 @@ export class RestApiFindOneHandler extends RestApiBaseHandler { const { filter, depth } = await this.parseRequestArgs(request); const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -37,15 +40,14 @@ export class RestApiFindOneHandler extends RestApiBaseHandler { { filter, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts index d8fe20944a0..72af8d35e6e 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-group-by.handler.ts @@ -29,8 +29,10 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { groupBy, selectedFields, authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, includeRecords, orderByForRecords, } = await this.parseRequestArgs(request); @@ -47,8 +49,10 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); } catch (error) { @@ -57,8 +61,13 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { } private async parseRequestArgs(request: AuthenticatedRequest) { - const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = - await this.buildCommonOptions(request); + const { + authContext, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = await this.buildCommonOptions(request); const orderByWithGroupBy = parseOrderByWithGroupByRestRequest(request); const orderByForRecordsWithGroupBy = @@ -73,8 +82,9 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { if (includeRecords) { const selectableFields = await this.computeSelectedFields({ depth: 0, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -83,8 +93,10 @@ export class RestApiGroupByHandler extends RestApiBaseHandler { return { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, filter, orderBy: orderByWithGroupBy, orderByForRecords: orderByForRecordsWithGroupBy, diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts index f624570ce9c..34689a286b0 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-merge-many.handler.ts @@ -3,8 +3,8 @@ import { Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonMergeManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-merge-many-query-runner.service'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; @@ -22,14 +22,17 @@ export class RestApiMergeManyHandler extends RestApiBaseHandler { const { depth, ...restArgs } = await this.parseRequestArgs(request); const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -37,15 +40,14 @@ export class RestApiMergeManyHandler extends RestApiBaseHandler { { ...restArgs, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts index 11519881490..c9788916c7c 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-many.handler.ts @@ -3,8 +3,8 @@ import { Injectable } from '@nestjs/common'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonRestoreManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-restore-many-query-runner.service'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util'; import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; @@ -28,14 +28,17 @@ export class RestApiRestoreManyHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -43,15 +46,14 @@ export class RestApiRestoreManyHandler extends RestApiBaseHandler { { filter, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - records, - objectMetadataItemWithFieldMaps.namePlural, - ); + return this.formatRestResponse(records, flatObjectMetadata.namePlural); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts index b248987e93e..575dab82d6c 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-restore-one.handler.ts @@ -24,14 +24,17 @@ export class RestApiRestoreOneHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -39,15 +42,14 @@ export class RestApiRestoreOneHandler extends RestApiBaseHandler { { id, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts index 2746f10a55e..28f081a386b 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-many.handler.ts @@ -24,14 +24,17 @@ export class RestApiUpdateManyHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -39,15 +42,14 @@ export class RestApiUpdateManyHandler extends RestApiBaseHandler { { data, filter, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - records, - objectMetadataItemWithFieldMaps.namePlural, - ); + return this.formatRestResponse(records, flatObjectMetadata.namePlural); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts index b881ba80988..efb138b04cc 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-update-one.handler.ts @@ -24,14 +24,17 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler { const { authContext, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, } = await this.buildCommonOptions(request); const selectedFields = await this.computeSelectedFields({ depth, - objectMetadataMapItem: objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, authContext, }); @@ -39,15 +42,14 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler { { id, data, selectedFields }, { authContext, - objectMetadataMaps, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }, ); - return this.formatRestResponse( - record, - objectMetadataItemWithFieldMaps.nameSingular, - ); + return this.formatRestResponse(record, flatObjectMetadata.nameSingular); } catch (error) { return workspaceQueryRunnerRestApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts index 7635f675b76..2f3fc4fee5d 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/selected-fields-handler.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { FieldMetadataType, ObjectsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -7,9 +7,11 @@ import { CommonSelectedFields } from 'src/engine/api/common/types/common-selecte import { getAllSelectableFields } from 'src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util'; import { MAX_DEPTH } from 'src/engine/api/rest/input-request-parsers/constants/max-depth.constant'; import { Depth } from 'src/engine/api/rest/input-request-parsers/types/depth.type'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; type SelectFields = { [key: string]: boolean | SelectFields; @@ -19,30 +21,32 @@ type SelectFields = { export class RestToCommonSelectedFieldsHandler { computeFromDepth = ({ objectsPermissions, - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, depth, }: { objectsPermissions: ObjectsPermissions; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatObjectMetadata: FlatObjectMetadata; depth: Depth | undefined; }): CommonSelectedFields => { const restrictedFields = - objectsPermissions[objectMetadataMapItem.id].restrictedFields; + objectsPermissions[flatObjectMetadata.id].restrictedFields; const relationsSelectFields = this.getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, objectsPermissions, depth, }); const selectableFields = getAllSelectableFields({ restrictedFields, - objectMetadata: { - objectMetadataMapItem, - }, + flatObjectMetadata, + flatFieldMetadataMaps, }); return { @@ -52,13 +56,15 @@ export class RestToCommonSelectedFieldsHandler { }; private getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, objectsPermissions, depth, }: { - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatObjectMetadata: FlatObjectMetadata; objectsPermissions: ObjectsPermissions; depth: Depth | undefined; }) { @@ -66,46 +72,49 @@ export class RestToCommonSelectedFieldsHandler { let relationsSelectFields: SelectFields = {}; - for (const field of Object.values(objectMetadataMapItem.fieldsById)) { - if (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const flatField = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); + + if (!isFlatFieldMetadataOfType(flatField, FieldMetadataType.RELATION)) continue; const relationTargetObjectMetadata = - objectMetadataMaps.byId[field.relationTargetObjectMetadataId]; + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: flatField.relationTargetObjectMetadataId, + }); - if (!isDefined(relationTargetObjectMetadata)) { - throw new BadRequestException( - `Object metadata relation target not found for relation creation payload`, - ); - } const relationFieldSelectFields = getAllSelectableFields({ restrictedFields: objectsPermissions[relationTargetObjectMetadata.id].restrictedFields, - objectMetadata: { - objectMetadataMapItem: relationTargetObjectMetadata, - }, + flatObjectMetadata: relationTargetObjectMetadata, + flatFieldMetadataMaps, }); if (Object.keys(relationFieldSelectFields).length === 0) continue; if ( depth === MAX_DEPTH && - isDefined(field.relationTargetObjectMetadataId) + isDefined(flatField.relationTargetObjectMetadataId) ) { const depth2RelationsSelectFields = this.getRelationsAndRelationsSelectFields({ - objectMetadataMaps, - objectMetadataMapItem: relationTargetObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata: relationTargetObjectMetadata, objectsPermissions, depth: 1, }); - relationsSelectFields[field.name] = { + relationsSelectFields[flatField.name] = { ...relationFieldSelectFields, ...depth2RelationsSelectFields, }; } else { - relationsSelectFields[field.name] = relationFieldSelectFields; + relationsSelectFields[flatField.name] = relationFieldSelectFields; } } diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/__tests__/get-all-selectable-fields.util.spec.ts b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/__tests__/get-all-selectable-fields.util.spec.ts index 1325e1f4912..3f49092f821 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/__tests__/get-all-selectable-fields.util.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/__tests__/get-all-selectable-fields.util.spec.ts @@ -1,30 +1,97 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { getAllSelectableFields } from 'src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; describe('getAllSelectableFields', () => { + const createMockField = ( + overrides: Partial & { + id: string; + name: string; + type: FieldMetadataType; + }, + ): FlatFieldMetadata => + ({ + objectMetadataId: 'object-id', + workspaceId: 'workspace-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: overrides.id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + label: overrides.name, + ...overrides, + }) as FlatFieldMetadata; + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); + + const buildFlatObjectMetadata = (fieldIds: string[]): FlatObjectMetadata => + ({ + id: 'object-id', + workspaceId: 'workspace-id', + nameSingular: 'testObject', + namePlural: 'testObjects', + labelSingular: 'Test Object', + labelPlural: 'Test Objects', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'object-id', + fieldMetadataIds: fieldIds, + indexMetadataIds: [], + viewIds: [], + applicationId: null, + }) as unknown as FlatObjectMetadata; + it('should return all fields as selectable when no restrictions', () => { - const objectMetadata = { - objectMetadataMapItem: { - fieldsById: { - 'field-1': { - id: 'field-1', - name: 'name', - type: FieldMetadataType.TEXT, - }, - 'field-2': { - id: 'field-2', - name: 'email', - type: FieldMetadataType.TEXT, - }, - }, - } as any as ObjectMetadataItemWithFieldMaps, - }; + const field1 = createMockField({ + id: 'field-1', + name: 'name', + type: FieldMetadataType.TEXT, + }); + const field2 = createMockField({ + id: 'field-2', + name: 'email', + type: FieldMetadataType.TEXT, + }); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([field1, field2]); + const flatObjectMetadata = buildFlatObjectMetadata(['field-1', 'field-2']); const result = getAllSelectableFields({ restrictedFields: {}, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -34,28 +101,25 @@ describe('getAllSelectableFields', () => { }); it('should not return restricted fields', () => { - const objectMetadata = { - objectMetadataMapItem: { - fieldsById: { - 'field-1': { - id: 'field-1', - name: 'name', - type: FieldMetadataType.TEXT, - }, - 'field-2': { - id: 'field-2', - name: 'email', - type: FieldMetadataType.TEXT, - }, - }, - } as any as ObjectMetadataItemWithFieldMaps, - }; + const field1 = createMockField({ + id: 'field-1', + name: 'name', + type: FieldMetadataType.TEXT, + }); + const field2 = createMockField({ + id: 'field-2', + name: 'email', + type: FieldMetadataType.TEXT, + }); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([field1, field2]); + const flatObjectMetadata = buildFlatObjectMetadata(['field-1', 'field-2']); const result = getAllSelectableFields({ restrictedFields: { 'field-2': { canRead: false }, }, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -64,31 +128,36 @@ describe('getAllSelectableFields', () => { }); it('should create nested objects for composite fields', () => { - const objectMetadata = { - objectMetadataMapItem: { - fieldsById: { - 'field-1': { - id: 'field-1', - name: 'name', - type: FieldMetadataType.TEXT, - }, - 'field-2': { - id: 'field-2', - name: 'fullName', - type: FieldMetadataType.FULL_NAME, - }, - 'field-3': { - id: 'field-3', - name: 'domainName', - type: FieldMetadataType.LINKS, - }, - }, - } as any as ObjectMetadataItemWithFieldMaps, - }; + const field1 = createMockField({ + id: 'field-1', + name: 'name', + type: FieldMetadataType.TEXT, + }); + const field2 = createMockField({ + id: 'field-2', + name: 'fullName', + type: FieldMetadataType.FULL_NAME, + }); + const field3 = createMockField({ + id: 'field-3', + name: 'domainName', + type: FieldMetadataType.LINKS, + }); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + field1, + field2, + field3, + ]); + const flatObjectMetadata = buildFlatObjectMetadata([ + 'field-1', + 'field-2', + 'field-3', + ]); const result = getAllSelectableFields({ restrictedFields: {}, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -106,28 +175,25 @@ describe('getAllSelectableFields', () => { }); it('should restrict all sub-fields when composite field is restricted', () => { - const objectMetadata = { - objectMetadataMapItem: { - fieldsById: { - 'field-1': { - id: 'field-1', - name: 'name', - type: FieldMetadataType.TEXT, - }, - 'field-2': { - id: 'field-2', - name: 'fullName', - type: FieldMetadataType.FULL_NAME, - }, - }, - } as any as ObjectMetadataItemWithFieldMaps, - }; + const field1 = createMockField({ + id: 'field-1', + name: 'name', + type: FieldMetadataType.TEXT, + }); + const field2 = createMockField({ + id: 'field-2', + name: 'fullName', + type: FieldMetadataType.FULL_NAME, + }); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([field1, field2]); + const flatObjectMetadata = buildFlatObjectMetadata(['field-1', 'field-2']); const result = getAllSelectableFields({ restrictedFields: { 'field-2': { canRead: false }, }, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }); expect(result).toEqual({ @@ -136,39 +202,46 @@ describe('getAllSelectableFields', () => { }); it('should handle mixed regular and composite fields with restrictions', () => { - const objectMetadata = { - objectMetadataMapItem: { - fieldsById: { - 'field-1': { - id: 'field-1', - name: 'name', - type: FieldMetadataType.TEXT, - }, - 'field-2': { - id: 'field-2', - name: 'email', - type: FieldMetadataType.TEXT, - }, - 'field-3': { - id: 'field-3', - name: 'fullName', - type: FieldMetadataType.FULL_NAME, - }, - 'field-4': { - id: 'field-4', - name: 'address', - type: FieldMetadataType.ADDRESS, - }, - }, - } as any as ObjectMetadataItemWithFieldMaps, - }; + const field1 = createMockField({ + id: 'field-1', + name: 'name', + type: FieldMetadataType.TEXT, + }); + const field2 = createMockField({ + id: 'field-2', + name: 'email', + type: FieldMetadataType.TEXT, + }); + const field3 = createMockField({ + id: 'field-3', + name: 'fullName', + type: FieldMetadataType.FULL_NAME, + }); + const field4 = createMockField({ + id: 'field-4', + name: 'address', + type: FieldMetadataType.ADDRESS, + }); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + field1, + field2, + field3, + field4, + ]); + const flatObjectMetadata = buildFlatObjectMetadata([ + 'field-1', + 'field-2', + 'field-3', + 'field-4', + ]); const result = getAllSelectableFields({ restrictedFields: { 'field-2': { canRead: false }, 'field-4': { canRead: false }, }, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }); expect(result).toEqual({ diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util.ts b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util.ts index f4da0546bfc..d62d4c0570d 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-to-common-args-handlers/utils/get-all-selectable-fields.util.ts @@ -7,8 +7,11 @@ import { import { isDefined } from 'twenty-shared/utils'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; type SelectableFieldsStructured = Record< string, @@ -17,24 +20,29 @@ type SelectableFieldsStructured = Record< export const getAllSelectableFields = ({ restrictedFields, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, }: { restrictedFields: RestrictedFieldsPermissions; - objectMetadata: { objectMetadataMapItem: ObjectMetadataItemWithFieldMaps }; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; }): SelectableFieldsStructured => { const result: SelectableFieldsStructured = {}; - const fields = Object.values(objectMetadata.objectMetadataMapItem.fieldsById); + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const flatField = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); - for (const field of fields) { - if (restrictedFields[field.id]?.canRead === false) continue; + if (restrictedFields[flatField.id]?.canRead === false) continue; - if (isCompositeFieldMetadataType(field.type)) { - const compositeType = compositeTypeDefinitions.get(field.type); + if (isCompositeFieldMetadataType(flatField.type)) { + const compositeType = compositeTypeDefinitions.get(flatField.type); if (!compositeType) { throw new Error( - `Composite type definition not found for type: ${field.type}`, + `Composite type definition not found for type: ${flatField.type}`, ); } @@ -44,15 +52,15 @@ export const getAllSelectableFields = ({ compositeFields[property.name] = true; } - result[field.name] = compositeFields; + result[flatField.name] = compositeFields; } else if ( - isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) && - field.settings.relationType === RelationType.MANY_TO_ONE && - isDefined(field.settings.joinColumnName) + isFlatFieldMetadataOfType(flatField, FieldMetadataType.RELATION) && + flatField.settings.relationType === RelationType.MANY_TO_ONE && + isDefined(flatField.settings.joinColumnName) ) { - result[field.settings.joinColumnName] = true; + result[flatField.settings.joinColumnName] = true; } else { - result[field.name] = true; + result[flatField.name] = true; } } diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-enum-values.util.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-enum-values.util.spec.ts deleted file mode 100644 index bc2fba7b388..00000000000 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/check-filter-enum-values.util.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; - -import { - fieldSelectMock, - objectMetadataItemMock, -} from 'src/engine/api/__mocks__/object-metadata-item.mock'; -import { checkFilterEnumValues } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; - -describe('checkFilterEnumValues', () => { - const completeFieldSelectMock = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', - objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: 'field-select-id', - type: fieldSelectMock.type, - name: fieldSelectMock.name, - label: 'Field Select', - isNullable: fieldSelectMock.isNullable, - defaultValue: fieldSelectMock.defaultValue, - options: fieldSelectMock.options, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const fieldsById: FieldMetadataMap = { - 'field-select-id': completeFieldSelectMock, - }; - - const mockObjectMetadataWithFieldMaps = { - ...objectMetadataItemMock, - fieldsById, - fieldIdByName: { - [completeFieldSelectMock.name]: completeFieldSelectMock.id, - }, - fieldIdByJoinColumnName: {}, - indexMetadatas: [], - }; - - it('should check properly', () => { - expect(() => - checkFilterEnumValues( - FieldMetadataType.SELECT, - fieldSelectMock.name, - 'OPTION_1', - mockObjectMetadataWithFieldMaps, - ), - ).not.toThrow(); - - expect(() => - checkFilterEnumValues( - FieldMetadataType.SELECT, - fieldSelectMock.name, - 'MISSING_OPTION', - mockObjectMetadataWithFieldMaps, - ), - ).toThrow( - `'filter' enum value 'MISSING_OPTION' not available in '${fieldSelectMock.name}' enum. Available enum values are ['OPTION_1', 'OPTION_2']`, - ); - }); - - it('should allow filter by NULL or NOT_NULL values', () => { - expect(() => - checkFilterEnumValues( - FieldMetadataType.SELECT, - fieldSelectMock.name, - 'NULL', - mockObjectMetadataWithFieldMaps, - ), - ).not.toThrow(); - - expect(() => - checkFilterEnumValues( - FieldMetadataType.SELECT, - fieldSelectMock.name, - 'NOT_NULL', - mockObjectMetadataWithFieldMaps, - ), - ).not.toThrow(); - }); -}); diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util.ts deleted file mode 100644 index 49b273214c6..00000000000 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/check-filter-enum-values.util.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; - -import { FieldMetadataType } from 'twenty-shared/types'; - -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export const checkFilterEnumValues = ( - fieldType: FieldMetadataType | undefined, - fieldName: string, - value: string, - objectMetadataItem: ObjectMetadataItemWithFieldMaps, -): void => { - if ( - !fieldType || - ![FieldMetadataType.MULTI_SELECT, FieldMetadataType.SELECT].includes( - fieldType, - ) - ) { - return; - } - const fieldMetadataId = objectMetadataItem.fieldIdByName[fieldName]; - const field = objectMetadataItem.fieldsById[fieldMetadataId]; - - const values = /^\[.*\]$/.test(value) - ? value.slice(1, -1).split(',') - : [value]; - const enumValues = field?.options?.map((option) => option.value); - - if (!enumValues) { - return; - } - const allowedEnumValues = ['NULL', 'NOT_NULL', ...enumValues]; - - values.forEach((val) => { - if (!allowedEnumValues.includes(val)) { - throw new BadRequestException( - `'filter' enum value '${val}' not available in '${fieldName}' enum. Available enum values are ['${enumValues.join( - "', '", - )}']`, - ); - } - }); -}; diff --git a/packages/twenty-server/src/engine/api/utils/__tests__/build-duplicate-conditions.utils.spec.ts b/packages/twenty-server/src/engine/api/utils/__tests__/build-duplicate-conditions.utils.spec.ts index 042d885522c..4bdc6bb3c1d 100644 --- a/packages/twenty-server/src/engine/api/utils/__tests__/build-duplicate-conditions.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/utils/__tests__/build-duplicate-conditions.utils.spec.ts @@ -1,11 +1,17 @@ -import { mockPersonObjectMetadataWithFieldMaps } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata'; +import { + mockPersonFlatFieldMetadataMaps, + mockPersonFlatObjectMetadata, + mockPersonFlatObjectMetadataMaps, +} from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata'; import { mockPersonRecords } from 'src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonRecords'; import { buildDuplicateConditions } from 'src/engine/api/utils/build-duplicate-conditions.utils'; describe('buildDuplicateConditions', () => { it('should build conditions based on duplicate criteria from composite field', () => { const duplicateConditons = buildDuplicateConditions( - mockPersonObjectMetadataWithFieldMaps([['emailsPrimaryEmail']]), + mockPersonFlatObjectMetadata([['emailsPrimaryEmail']]), + mockPersonFlatObjectMetadataMaps([['emailsPrimaryEmail']]), + mockPersonFlatFieldMetadataMaps(), mockPersonRecords, 'recordId', ); @@ -28,7 +34,9 @@ describe('buildDuplicateConditions', () => { it('should build conditions based on duplicate criteria from basic field', () => { const duplicateConditons = buildDuplicateConditions( - mockPersonObjectMetadataWithFieldMaps([['jobTitle']]), + mockPersonFlatObjectMetadata([['jobTitle']]), + mockPersonFlatObjectMetadataMaps([['jobTitle']]), + mockPersonFlatFieldMetadataMaps(), mockPersonRecords, 'recordId', ); @@ -49,7 +57,9 @@ describe('buildDuplicateConditions', () => { it('should not build conditions based on duplicate criteria if record value is null or too small', () => { const duplicateConditons = buildDuplicateConditions( - mockPersonObjectMetadataWithFieldMaps([['linkedinLinkPrimaryLinkUrl']]), + mockPersonFlatObjectMetadata([['linkedinLinkPrimaryLinkUrl']]), + mockPersonFlatObjectMetadataMaps([['linkedinLinkPrimaryLinkUrl']]), + mockPersonFlatFieldMetadataMaps(), mockPersonRecords, 'recordId', ); @@ -59,7 +69,9 @@ describe('buildDuplicateConditions', () => { it('should build conditions based on duplicate criteria and without recordId filter', () => { const duplicateConditons = buildDuplicateConditions( - mockPersonObjectMetadataWithFieldMaps([['jobTitle']]), + mockPersonFlatObjectMetadata([['jobTitle']]), + mockPersonFlatObjectMetadataMaps([['jobTitle']]), + mockPersonFlatFieldMetadataMaps(), mockPersonRecords, ); diff --git a/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts b/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts index 773491cac57..998a43baa5d 100644 --- a/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts @@ -2,79 +2,119 @@ import { FieldMetadataType, OrderByDirection } from 'twenty-shared/types'; import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; describe('computeCursorArgFilter', () => { - const objectMetadataItemWithFieldMaps = - getMockObjectMetadataItemWithFieldsMaps({ - id: 'object-id', - workspaceId: 'workspace-id', - nameSingular: 'person', - namePlural: 'people', - isCustom: false, - isRemote: false, - labelSingular: 'Person', - labelPlural: 'People', - targetTableName: 'person', - indexMetadatas: [], - isSystem: false, - isActive: true, - isAuditLogged: false, - isSearchable: false, - fieldIdByJoinColumnName: {}, - icon: 'Icon123', - fieldIdByName: { - name: 'name-id', - age: 'age-id', - fullName: 'fullname-id', + const workspaceId = 'workspace-id'; + const objectMetadataId = 'object-id'; + + const createMockField = ( + overrides: Partial & { + id: string; + name: string; + type: FieldMetadataType; + }, + ): FlatFieldMetadata => + ({ + workspaceId, + objectMetadataId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: overrides.id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + label: overrides.name, + ...overrides, + }) as FlatFieldMetadata; + + const nameField = createMockField({ + id: 'name-id', + type: FieldMetadataType.TEXT, + name: 'name', + label: 'Name', + }); + + const ageField = createMockField({ + id: 'age-id', + type: FieldMetadataType.NUMBER, + name: 'age', + label: 'Age', + }); + + const fullNameField = createMockField({ + id: 'fullname-id', + type: FieldMetadataType.FULL_NAME, + name: 'fullName', + label: 'Full Name', + }); + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; }, - fieldsById: { - 'name-id': getMockFieldMetadataEntity({ - workspaceId: 'workspace-id', - objectMetadataId: 'object-id', - id: 'name-id', - type: FieldMetadataType.TEXT, - name: 'name', - label: 'Name', - isLabelSyncedWithName: true, - isNullable: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - 'age-id': getMockFieldMetadataEntity({ - workspaceId: 'workspace-id', - objectMetadataId: 'object-id', - id: 'age-id', - type: FieldMetadataType.NUMBER, - name: 'age', - label: 'Age', - isLabelSyncedWithName: true, - isNullable: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - 'fullname-id': getMockFieldMetadataEntity({ - workspaceId: 'workspace-id', - objectMetadataId: 'object-id', - id: 'fullname-id', - type: FieldMetadataType.FULL_NAME, - name: 'fullName', - label: 'Full Name', - isLabelSyncedWithName: true, - isNullable: true, - createdAt: new Date(), - updatedAt: new Date(), - }), + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; }, - }); + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }); + + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([ + nameField, + ageField, + fullNameField, + ]); + + const flatObjectMetadata: FlatObjectMetadata = { + id: objectMetadataId, + workspaceId, + nameSingular: 'person', + namePlural: 'people', + labelSingular: 'Person', + labelPlural: 'People', + targetTableName: 'person', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: false, + isSearchable: false, + icon: 'Icon123', + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: objectMetadataId, + fieldMetadataIds: ['name-id', 'age-id', 'fullname-id'], + indexMetadataIds: [], + viewIds: [], + applicationId: null, + } as unknown as FlatObjectMetadata; describe('basic cursor filtering', () => { it('should return empty array when cursor is empty', () => { const result = computeCursorArgFilter( {}, [], - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ); @@ -88,7 +128,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ); @@ -102,7 +143,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, false, ); @@ -121,7 +163,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ); @@ -149,7 +192,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ); @@ -195,7 +239,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ); @@ -224,7 +269,8 @@ describe('computeCursorArgFilter', () => { const result = computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, false, ); @@ -265,7 +311,8 @@ describe('computeCursorArgFilter', () => { computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ), ).toThrow(GraphqlQueryRunnerException); @@ -279,7 +326,8 @@ describe('computeCursorArgFilter', () => { computeCursorArgFilter( cursor, orderBy, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, true, ), ).toThrow(GraphqlQueryRunnerException); diff --git a/packages/twenty-server/src/engine/api/utils/build-cursor-where-condition.utils.ts b/packages/twenty-server/src/engine/api/utils/build-cursor-where-condition.utils.ts index cadf79f2820..5772ecde9bc 100644 --- a/packages/twenty-server/src/engine/api/utils/build-cursor-where-condition.utils.ts +++ b/packages/twenty-server/src/engine/api/utils/build-cursor-where-condition.utils.ts @@ -16,14 +16,19 @@ import { computeOperator } from 'src/engine/api/utils/compute-operator.utils'; import { isAscendingOrder } from 'src/engine/api/utils/is-ascending-order.utils'; import { validateAndGetOrderByForScalarField } from 'src/engine/api/utils/validate-and-get-order-by.utils'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; type BuildCursorWhereConditionParams = { cursorKey: keyof ObjectRecord; cursorValue: | ObjectRecordCursorLeafScalarValue | ObjectRecordCursorLeafCompositeValue; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; orderBy: ObjectRecordOrderBy; isForwardPagination: boolean; isEqualityCondition?: boolean; @@ -32,15 +37,23 @@ type BuildCursorWhereConditionParams = { export const buildCursorWhereCondition = ({ cursorKey, cursorValue, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, orderBy, isForwardPagination, isEqualityCondition = false, }: BuildCursorWhereConditionParams): Record => { - const fieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName[cursorKey]; - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + const fieldMetadataId = fieldIdByName[cursorKey]; + + const fieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldMetadataId, + }); if (!fieldMetadata) { throw new GraphqlQueryRunnerException( diff --git a/packages/twenty-server/src/engine/api/utils/build-duplicate-conditions.utils.ts b/packages/twenty-server/src/engine/api/utils/build-duplicate-conditions.utils.ts index b5f154c52c7..5ab8eb72ccc 100644 --- a/packages/twenty-server/src/engine/api/utils/build-duplicate-conditions.utils.ts +++ b/packages/twenty-server/src/engine/api/utils/build-duplicate-conditions.utils.ts @@ -4,12 +4,16 @@ import { type ObjectRecord } from 'twenty-shared/types'; import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { settings } from 'src/engine/constants/settings'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { getCompositeFieldMetadataMap } from 'src/engine/twenty-orm/utils/format-result.util'; export const buildDuplicateConditions = ( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, records?: Partial[] | undefined, filteringByExistingRecordId?: string, ): Partial => { @@ -17,13 +21,17 @@ export const buildDuplicateConditions = ( return {}; } - const criteriaCollection = - objectMetadataItemWithFieldMaps.duplicateCriteria || []; + const criteriaCollection = flatObjectMetadata.duplicateCriteria || []; - const formattedRecords = formatData(records, objectMetadataItemWithFieldMaps); + const formattedRecords = formatData( + records, + flatObjectMetadata, + flatFieldMetadataMaps, + ); const compositeFieldMetadataMap = getCompositeFieldMetadataMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const conditions = formattedRecords.flatMap((record) => { diff --git a/packages/twenty-server/src/engine/api/utils/compute-cursor-arg-filter.utils.ts b/packages/twenty-server/src/engine/api/utils/compute-cursor-arg-filter.utils.ts index a8196032f0e..04289618e6d 100644 --- a/packages/twenty-server/src/engine/api/utils/compute-cursor-arg-filter.utils.ts +++ b/packages/twenty-server/src/engine/api/utils/compute-cursor-arg-filter.utils.ts @@ -10,12 +10,15 @@ import { import { buildCursorCumulativeWhereCondition } from 'src/engine/api/utils/build-cursor-cumulative-where-conditions.utils'; import { buildCursorWhereCondition } from 'src/engine/api/utils/build-cursor-where-condition.utils'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const computeCursorArgFilter = ( cursor: ObjectRecordCursor, orderBy: ObjectRecordOrderBy, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, isForwardPagination = true, ): ObjectRecordFilter[] => { const cursorEntries = Object.entries(cursor) @@ -42,7 +45,8 @@ export const computeCursorArgFilter = ( buildCursorWhereCondition({ cursorKey, cursorValue, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, orderBy, isForwardPagination: true, isEqualityCondition: true, @@ -51,7 +55,8 @@ export const computeCursorArgFilter = ( buildCursorWhereCondition({ cursorKey, cursorValue, - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, orderBy, isForwardPagination, }), diff --git a/packages/twenty-server/src/engine/api/utils/get-all-selectable-column-names.utils.ts b/packages/twenty-server/src/engine/api/utils/get-all-selectable-column-names.utils.ts index c3512ac6879..acfc535ddb0 100644 --- a/packages/twenty-server/src/engine/api/utils/get-all-selectable-column-names.utils.ts +++ b/packages/twenty-server/src/engine/api/utils/get-all-selectable-column-names.utils.ts @@ -1,12 +1,17 @@ import { type RestrictedFieldsPermissions } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getFieldMetadataIdToColumnNamesMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util'; type GetAllSelectableFieldsArgs = { restrictedFields: RestrictedFieldsPermissions; - objectMetadata: { objectMetadataMapItem: ObjectMetadataItemWithFieldMaps }; + objectMetadata: { + objectMetadataMapItem: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; + }; }; export const getAllSelectableColumnNames = ({ @@ -19,6 +24,7 @@ export const getAllSelectableColumnNames = ({ const fieldMetadataIdToColumnNamesMap = getFieldMetadataIdToColumnNamesMap( objectMetadata.objectMetadataMapItem, + objectMetadata.flatFieldMetadataMaps, ); const restrictedFieldsColumnNames: string[] = restrictedFieldsIds diff --git a/packages/twenty-server/src/engine/core-modules/__mocks__/mockFlatObjectMetadatas.ts b/packages/twenty-server/src/engine/core-modules/__mocks__/mockFlatObjectMetadatas.ts new file mode 100644 index 00000000000..69a0a10493d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/__mocks__/mockFlatObjectMetadatas.ts @@ -0,0 +1,168 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +const workspaceId = '20202020-0000-0000-0000-000000000000'; + +const personNameFieldId = 'person-name-field-id'; +const companyNameFieldId = 'company-name-field-id'; +const companyDomainNameFieldId = 'company-domain-name-field-id'; +const customObjectNameFieldId = 'custom-object-name-field-id'; +const customObjectImageFieldId = 'custom-object-image-field-id'; + +export const mockFlatObjectMetadatas: FlatObjectMetadata[] = [ + getFlatObjectMetadataMock({ + id: '20202020-8dec-43d5-b2ff-6eef05095bec', + nameSingular: 'person', + namePlural: 'people', + labelSingular: 'Person', + labelPlural: 'People', + description: 'A person', + icon: 'test-person-icon', + isCustom: false, + isSearchable: true, + labelIdentifierFieldMetadataId: personNameFieldId, + imageIdentifierFieldMetadataId: null, + workspaceId, + fieldMetadataIds: [personNameFieldId], + universalIdentifier: 'person-universal-id', + applicationId: workspaceId, + }), + getFlatObjectMetadataMock({ + id: '20202020-c03c-45d6-a4b0-04afe1357c5c', + nameSingular: 'company', + namePlural: 'companies', + labelSingular: 'Company', + labelPlural: 'Companies', + description: 'A company', + icon: 'test-company-icon', + isCustom: false, + isSearchable: true, + labelIdentifierFieldMetadataId: companyNameFieldId, + imageIdentifierFieldMetadataId: null, + workspaceId, + fieldMetadataIds: [companyNameFieldId, companyDomainNameFieldId], + universalIdentifier: 'company-universal-id', + applicationId: workspaceId, + }), + getFlatObjectMetadataMock({ + id: '20202020-3d75-4aab-bacd-ee176c5f63ca', + nameSingular: 'regular-custom-object', + namePlural: 'regular-custom-objects', + labelSingular: 'Regular Custom Object', + labelPlural: 'Regular Custom Objects', + description: 'A regular custom object', + icon: 'test-regular-custom-object-icon', + isCustom: true, + isSearchable: true, + labelIdentifierFieldMetadataId: customObjectNameFieldId, + imageIdentifierFieldMetadataId: customObjectImageFieldId, + workspaceId, + fieldMetadataIds: [customObjectNameFieldId, customObjectImageFieldId], + universalIdentifier: 'custom-object-universal-id', + applicationId: workspaceId, + }), + getFlatObjectMetadataMock({ + id: '20202020-540c-4397-b872-2a90ea2fb809', + nameSingular: 'non-searchable-object', + namePlural: 'non-searchable-objects', + labelSingular: '', + labelPlural: '', + description: '', + icon: 'test-non-searchable-object-icon', + isCustom: false, + isSystem: true, + isSearchable: false, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + workspaceId, + fieldMetadataIds: [], + universalIdentifier: 'non-searchable-object-universal-id', + applicationId: workspaceId, + }), +]; + +export const mockFlatFieldMetadataMaps: FlatEntityMaps = { + byId: { + [personNameFieldId]: getFlatFieldMetadataMock({ + id: personNameFieldId, + type: FieldMetadataType.FULL_NAME, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + description: "Contact's name", + isCustom: false, + defaultValue: { lastName: "''", firstName: "''" }, + objectMetadataId: '20202020-8dec-43d5-b2ff-6eef05095bec', + workspaceId, + universalIdentifier: 'person-name-field-universal-id', + applicationId: workspaceId, + }), + [companyNameFieldId]: getFlatFieldMetadataMock({ + id: companyNameFieldId, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + description: null, + isCustom: false, + defaultValue: '', + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', + workspaceId, + universalIdentifier: 'company-name-field-universal-id', + applicationId: workspaceId, + }), + [companyDomainNameFieldId]: getFlatFieldMetadataMock({ + id: companyDomainNameFieldId, + type: FieldMetadataType.LINKS, + icon: 'test-field-icon', + name: 'domainName', + label: 'Domain Name', + description: null, + isCustom: false, + defaultValue: { + primaryLinkLabel: '', + primaryLinkUrl: '', + secondaryLinks: [], + }, + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', + workspaceId, + universalIdentifier: 'company-domain-name-field-universal-id', + applicationId: workspaceId, + }), + [customObjectNameFieldId]: getFlatFieldMetadataMock({ + id: customObjectNameFieldId, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + description: null, + isCustom: false, + defaultValue: '', + objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca', + workspaceId, + universalIdentifier: 'custom-object-name-field-universal-id', + applicationId: workspaceId, + }), + [customObjectImageFieldId]: getFlatFieldMetadataMock({ + id: customObjectImageFieldId, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'imageIdentifierFieldName', + label: 'Image Identifier Field Name', + description: null, + isCustom: false, + defaultValue: '', + objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca', + workspaceId, + universalIdentifier: 'custom-object-image-field-universal-id', + applicationId: workspaceId, + }), + }, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, +}; diff --git a/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts b/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts index 297b2a8e828..43d4dfed135 100644 --- a/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts +++ b/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts @@ -1,204 +1,290 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock'; +import { type ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; const workspaceId = '20202020-0000-0000-0000-000000000000'; -export const mockObjectMetadataItemsWithFieldMaps = [ - getMockObjectMetadataItemWithFieldsMaps({ - id: '20202020-8dec-43d5-b2ff-6eef05095bec', - standardId: '', - nameSingular: 'person', - namePlural: 'people', - labelSingular: 'Person', - labelPlural: 'People', - description: 'A person', - icon: 'test-person-icon', - targetTableName: 'DEPRECATED', - isCustom: false, - isRemote: false, - isActive: true, - isSystem: false, - isAuditLogged: true, - isSearchable: true, - labelIdentifierFieldMetadataId: 'nameFieldMetadataId', - imageIdentifierFieldMetadataId: '', - workspaceId, - indexMetadatas: [], - fieldsById: { - nameFieldMetadataId: getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '20202020-8dec-43d5-b2ff-6eef05095bec', - id: 'nameFieldMetadataId', - type: FieldMetadataType.FULL_NAME, - icon: 'test-field-icon', - name: 'name', - label: 'Name', - defaultValue: { - lastName: "''", - firstName: "''", - }, - description: "Contact's name", - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - }, - fieldIdByName: { - name: 'nameFieldMetadataId', - }, - fieldIdByJoinColumnName: {}, - }), - getMockObjectMetadataItemWithFieldsMaps({ - id: '20202020-c03c-45d6-a4b0-04afe1357c5c', - standardId: '', - nameSingular: 'company', - namePlural: 'companies', - labelSingular: 'Company', - labelPlural: 'Companies', - description: 'A company', - icon: 'test-company-icon', - targetTableName: 'DEPRECATED', - isCustom: false, - isRemote: false, - isActive: true, - isSystem: false, - isAuditLogged: true, - isSearchable: true, - labelIdentifierFieldMetadataId: 'nameFieldMetadataId', - imageIdentifierFieldMetadataId: '', - workspaceId, - indexMetadatas: [], - fieldsById: { - nameFieldMetadataId: getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', - id: 'nameFieldMetadataId', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'name', - label: 'Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - domainNameFieldMetadataId: getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', - id: 'domainNameFieldMetadataId', - type: FieldMetadataType.LINKS, - icon: 'test-field-icon', - name: 'domainName', - label: 'Domain Name', - defaultValue: { - primaryLinkLabel: '', - primaryLinkUrl: '', - secondaryLinks: [], - }, - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - }, - fieldIdByName: { - name: 'nameFieldMetadataId', - domainName: 'domainNameFieldMetadataId', - }, - fieldIdByJoinColumnName: {}, - }), - getMockObjectMetadataItemWithFieldsMaps({ - id: '20202020-3d75-4aab-bacd-ee176c5f63ca', - standardId: '', - nameSingular: 'regular-custom-object', - namePlural: 'regular-custom-objects', - labelSingular: 'Regular Custom Object', - labelPlural: 'Regular Custom Objects', - description: 'A regular custom object', - icon: 'test-regular-custom-object-icon', - targetTableName: 'DEPRECATED', - isCustom: true, - isRemote: false, - isActive: true, - isSystem: false, - isAuditLogged: true, - isSearchable: true, - labelIdentifierFieldMetadataId: 'nameFieldMetadataId', - imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId', - workspaceId, - indexMetadatas: [], - fieldsById: { - nameFieldMetadataId: getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca', - id: 'nameFieldMetadataId', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'name', - label: 'Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - imageIdentifierFieldMetadataId: getMockFieldMetadataEntity({ - workspaceId, - objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca', - id: 'imageIdentifierFieldMetadataId', - type: FieldMetadataType.TEXT, - icon: 'test-field-icon', - name: 'imageIdentifierFieldName', - label: 'Image Identifier Field Name', - defaultValue: '', - isCustom: false, - isNullable: true, - isUnique: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - }, - fieldIdByName: { - name: 'nameFieldMetadataId', - imageIdentifierFieldName: 'imageIdentifierFieldMetadataId', - }, - fieldIdByJoinColumnName: {}, - }), - getMockObjectMetadataItemWithFieldsMaps({ - id: '20202020-540c-4397-b872-2a90ea2fb809', - standardId: '', - nameSingular: 'non-searchable-object', - namePlural: 'non-searchable-objects', - labelSingular: '', - labelPlural: '', - description: '', - icon: 'test-non-searchable-object-icon', - targetTableName: 'DEPRECATED', - isCustom: false, - isRemote: false, - isActive: true, - isSystem: true, - isAuditLogged: true, - isSearchable: false, - labelIdentifierFieldMetadataId: '', - imageIdentifierFieldMetadataId: '', - workspaceId, - indexMetadatas: [], - fieldsById: {}, - fieldIdByName: {}, - fieldIdByJoinColumnName: {}, - }), +const personNameFieldId = 'nameFieldMetadataId-person'; +const companyNameFieldId = 'nameFieldMetadataId-company'; +const companyDomainNameFieldId = 'domainNameFieldMetadataId'; +const customObjectNameFieldId = 'nameFieldMetadataId-custom'; +const customObjectImageFieldId = 'imageIdentifierFieldMetadataId'; + +const personFlatObject = getFlatObjectMetadataMock({ + id: '20202020-8dec-43d5-b2ff-6eef05095bec', + standardId: '20202020-8dec-43d5-b2ff-6eef05095bec', + nameSingular: 'person', + namePlural: 'people', + labelSingular: 'Person', + labelPlural: 'People', + description: 'A person', + icon: 'test-person-icon', + targetTableName: 'DEPRECATED', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + labelIdentifierFieldMetadataId: personNameFieldId, + imageIdentifierFieldMetadataId: null, + workspaceId, + universalIdentifier: '20202020-8dec-43d5-b2ff-6eef05095bec', + fieldMetadataIds: [personNameFieldId], + indexMetadataIds: [], + viewIds: [], +}); + +const companyFlatObject = getFlatObjectMetadataMock({ + id: '20202020-c03c-45d6-a4b0-04afe1357c5c', + standardId: '20202020-c03c-45d6-a4b0-04afe1357c5c', + nameSingular: 'company', + namePlural: 'companies', + labelSingular: 'Company', + labelPlural: 'Companies', + description: 'A company', + icon: 'test-company-icon', + targetTableName: 'DEPRECATED', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + labelIdentifierFieldMetadataId: companyNameFieldId, + imageIdentifierFieldMetadataId: null, + workspaceId, + universalIdentifier: '20202020-c03c-45d6-a4b0-04afe1357c5c', + fieldMetadataIds: [companyNameFieldId, companyDomainNameFieldId], + indexMetadataIds: [], + viewIds: [], +}); + +const customObjectFlatObject = getFlatObjectMetadataMock({ + id: '20202020-3d75-4aab-bacd-ee176c5f63ca', + standardId: null, + nameSingular: 'regular-custom-object', + namePlural: 'regular-custom-objects', + labelSingular: 'Regular Custom Object', + labelPlural: 'Regular Custom Objects', + description: 'A regular custom object', + icon: 'test-regular-custom-object-icon', + targetTableName: 'DEPRECATED', + isCustom: true, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + labelIdentifierFieldMetadataId: customObjectNameFieldId, + imageIdentifierFieldMetadataId: customObjectImageFieldId, + workspaceId, + universalIdentifier: '20202020-3d75-4aab-bacd-ee176c5f63ca', + fieldMetadataIds: [customObjectNameFieldId, customObjectImageFieldId], + indexMetadataIds: [], + viewIds: [], +}); + +const nonSearchableFlatObject = getFlatObjectMetadataMock({ + id: '20202020-540c-4397-b872-2a90ea2fb809', + standardId: '20202020-540c-4397-b872-2a90ea2fb809', + nameSingular: 'non-searchable-object', + namePlural: 'non-searchable-objects', + labelSingular: '', + labelPlural: '', + description: '', + icon: 'test-non-searchable-object-icon', + targetTableName: 'DEPRECATED', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: true, + isAuditLogged: true, + isSearchable: false, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + workspaceId, + universalIdentifier: '20202020-540c-4397-b872-2a90ea2fb809', + fieldMetadataIds: [], + indexMetadataIds: [], + viewIds: [], +}); + +const personNameField = getFlatFieldMetadataMock({ + id: personNameFieldId, + objectMetadataId: personFlatObject.id, + type: FieldMetadataType.FULL_NAME, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + defaultValue: { + lastName: "''", + firstName: "''", + }, + description: "Contact's name", + isCustom: false, + isNullable: true, + isUnique: false, + isLabelSyncedWithName: true, + universalIdentifier: personNameFieldId, + workspaceId, +}); + +const companyNameField = getFlatFieldMetadataMock({ + id: companyNameFieldId, + objectMetadataId: companyFlatObject.id, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + defaultValue: '', + isCustom: false, + isNullable: true, + isUnique: false, + isLabelSyncedWithName: true, + universalIdentifier: companyNameFieldId, + workspaceId, +}); + +const companyDomainNameField = getFlatFieldMetadataMock({ + id: companyDomainNameFieldId, + objectMetadataId: companyFlatObject.id, + type: FieldMetadataType.LINKS, + icon: 'test-field-icon', + name: 'domainName', + label: 'Domain Name', + defaultValue: { + primaryLinkLabel: '', + primaryLinkUrl: '', + secondaryLinks: [], + }, + isCustom: false, + isNullable: true, + isUnique: false, + isLabelSyncedWithName: true, + universalIdentifier: companyDomainNameFieldId, + workspaceId, +}); + +const customObjectNameField = getFlatFieldMetadataMock({ + id: customObjectNameFieldId, + objectMetadataId: customObjectFlatObject.id, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'name', + label: 'Name', + defaultValue: '', + isCustom: false, + isNullable: true, + isUnique: false, + isLabelSyncedWithName: true, + universalIdentifier: customObjectNameFieldId, + workspaceId, +}); + +const customObjectImageField = getFlatFieldMetadataMock({ + id: customObjectImageFieldId, + objectMetadataId: customObjectFlatObject.id, + type: FieldMetadataType.TEXT, + icon: 'test-field-icon', + name: 'imageIdentifierFieldName', + label: 'Image Identifier Field Name', + defaultValue: '', + isCustom: false, + isNullable: true, + isUnique: false, + isLabelSyncedWithName: true, + universalIdentifier: customObjectImageFieldId, + workspaceId, +}); + +const ALL_FLAT_OBJECTS = [ + personFlatObject, + companyFlatObject, + customObjectFlatObject, + nonSearchableFlatObject, ]; + +const ALL_FLAT_FIELDS = [ + personNameField, + companyNameField, + companyDomainNameField, + customObjectNameField, + customObjectImageField, +]; + +export const mockFlatObjectMetadataMaps: FlatEntityMaps = + ALL_FLAT_OBJECTS.reduce( + (acc, object) => ({ + ...acc, + byId: { + ...acc.byId, + [object.id]: object, + }, + idByUniversalIdentifier: { + ...acc.idByUniversalIdentifier, + [object.universalIdentifier]: object.id, + }, + }), + createEmptyFlatEntityMaps() as FlatEntityMaps, + ); + +export const mockFlatFieldMetadataMaps: FlatEntityMaps = + ALL_FLAT_FIELDS.reduce( + (acc, field) => ({ + ...acc, + byId: { + ...acc.byId, + [field.id]: field, + }, + idByUniversalIdentifier: { + ...acc.idByUniversalIdentifier, + [field.universalIdentifier]: field.id, + }, + }), + createEmptyFlatEntityMaps() as FlatEntityMaps, + ); + +export const mockObjectIdByNameSingular: Record = + ALL_FLAT_OBJECTS.reduce( + (acc, object) => ({ + ...acc, + [object.nameSingular]: object.id, + }), + {} as Record, + ); + +export const getMockObjectMetadataInfo = ( + nameSingular: string, +): ObjectMetadataInfo => { + const objectId = mockObjectIdByNameSingular[nameSingular]; + const flatObjectMetadata = mockFlatObjectMetadataMaps.byId[objectId]; + + if (!flatObjectMetadata) { + throw new Error( + `Object metadata not found for nameSingular: ${nameSingular}`, + ); + } + + return { + flatObjectMetadata, + flatObjectMetadataMaps: mockFlatObjectMetadataMaps, + flatFieldMetadataMaps: mockFlatFieldMetadataMaps, + }; +}; + +export const mockPersonObjectMetadataInfo = getMockObjectMetadataInfo('person'); +export const mockCompanyObjectMetadataInfo = + getMockObjectMetadataInfo('company'); +export const mockCustomObjectMetadataInfo = getMockObjectMetadataInfo( + 'regular-custom-object', +); +export const mockNonSearchableObjectMetadataInfo = getMockObjectMetadataInfo( + 'non-searchable-object', +); diff --git a/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts b/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts index 5da432f910b..fd7b66031b6 100644 --- a/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts +++ b/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts @@ -8,7 +8,7 @@ import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/acto import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { buildFieldMapsForObject } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-for-object.util'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -55,8 +55,10 @@ export class CreatedByFromAuthContextService { : undefined; const fieldIdByName = objectMetadata - ? buildFieldMapsForObject(flatFieldMetadataMaps, objectMetadata.id) - .fieldIdByName + ? buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + objectMetadata, + ).fieldIdByName : {}; this.logger.log( diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts index 5eb7d1e271e..285c0515280 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/__tests__/object-record-changed-values.spec.ts @@ -1,27 +1,46 @@ +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; -const mockObjectMetadata = getMockObjectMetadataItemWithFieldsMaps({ +const mockObjectMetadata = { id: '1', icon: 'Icon123', nameSingular: 'Object', namePlural: 'Objects', labelSingular: 'Object', labelPlural: 'Objects', - description: 'Test object metadata', targetTableName: 'test_table', workspaceId: '1', - fieldsById: {}, - fieldIdByName: {}, + universalIdentifier: '1', isSystem: false, isCustom: false, isActive: true, isRemote: false, isAuditLogged: true, isSearchable: true, - indexMetadatas: [], - fieldIdByJoinColumnName: {}, -}); + indexMetadataIds: [], + fieldMetadataIds: [], + viewIds: [], + applicationId: null, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + shortcut: null, + description: null, + standardOverrides: null, + isUIReadOnly: false, + standardId: null, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + duplicateCriteria: null, +} as FlatObjectMetadata; + +const mockFlatFieldMetadataMaps: FlatEntityMaps = { + byId: {}, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, +}; describe('objectRecordChangedValues', () => { it('detects changes in scalar values correctly', () => { @@ -40,6 +59,7 @@ describe('objectRecordChangedValues', () => { oldRecord, newRecord, mockObjectMetadata, + mockFlatFieldMetadataMaps, ); expect(result).toEqual({ @@ -61,6 +81,7 @@ describe('objectRecordChangedValues', () => { oldRecord, newRecord, mockObjectMetadata, + mockFlatFieldMetadataMaps, ); expect(result).toEqual({}); @@ -82,6 +103,7 @@ describe('objectRecordChangedValues', () => { oldRecord, newRecord, mockObjectMetadata, + mockFlatFieldMetadataMaps, ); expect(result).toEqual({}); @@ -111,6 +133,7 @@ describe('objectRecordChangedValues', () => { oldRecord, newRecord, mockObjectMetadata, + mockFlatFieldMetadataMaps, ); expect(result).toEqual(expectedChanges); diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts index 395cc81a6bb..eed9d7af47c 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts @@ -2,10 +2,13 @@ import deepEqual from 'deep-equal'; import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; const isWorkflowVersionStepsOrTrigger = ( - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + objectMetadataItem: FlatObjectMetadata, key: string, ) => { return ( @@ -15,7 +18,7 @@ const isWorkflowVersionStepsOrTrigger = ( }; const isWorkflowAutomatedTriggerSettings = ( - objectMetadataItem: Pick, + objectMetadataItem: Pick, key: string, ) => { return ( @@ -27,12 +30,18 @@ const isWorkflowAutomatedTriggerSettings = ( export const objectRecordChangedValues = ( oldRecord: Partial, newRecord: Partial, - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + objectMetadataItem: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) => { + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + objectMetadataItem, + ); + return Object.keys(newRecord).reduce( (acc, key) => { - const field = - objectMetadataItem.fieldsById[objectMetadataItem.fieldIdByName[key]]; + const fieldId = fieldIdByName[key]; + const field = fieldId ? flatFieldMetadataMaps.byId[fieldId] : undefined; const oldRecordValue = oldRecord[key]; const newRecordValue = newRecord[key]; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts index 1666013cfe9..e61bd8db862 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.module.ts @@ -4,10 +4,16 @@ import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { OpenApiController } from 'src/engine/core-modules/open-api/open-api.controller'; import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; @Module({ - imports: [ObjectMetadataModule, AuthModule, FeatureFlagModule], + imports: [ + ObjectMetadataModule, + AuthModule, + FeatureFlagModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, + ], controllers: [OpenApiController], providers: [OpenApiService], }) diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts index 6305e114367..24a562d4e6c 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts @@ -4,6 +4,7 @@ import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; describe('OpenApiService', () => { @@ -29,6 +30,12 @@ describe('OpenApiService', () => { provide: FeatureFlagService, useValue: {}, }, + { + provide: WorkspaceManyOrAllFlatEntityMapsCacheService, + useValue: { + getOrRecomputeManyOrAllFlatEntityMaps: jest.fn(), + }, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts index b9c141c8ed7..3984f93a099 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts @@ -45,10 +45,9 @@ import { getUpdateOneResponse200, } from 'src/engine/core-modules/open-api/utils/responses.utils'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; import { getServerUrl } from 'src/utils/get-server-url'; @@ -58,7 +57,7 @@ export class OpenApiService { constructor( private readonly accessTokenService: AccessTokenService, private readonly twentyConfigService: TwentyConfigService, - private readonly objectMetadataService: ObjectMetadataServiceV2, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly featureFlagService: FeatureFlagService, ) {} @@ -75,15 +74,28 @@ export class OpenApiService { } } - private async getObjectMetadataItems(workspace: WorkspaceEntity) { - return await this.objectMetadataService.findManyWithinWorkspace( - workspace.id, - { - order: { - namePlural: 'ASC', + private async getFlatObjectMetadataArray(workspaceId: string) { + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], }, - }, + ); + + const flatObjectMetadataArray = Object.values( + flatObjectMetadataMaps.byId, + ).filter(isDefined); + + flatObjectMetadataArray.sort((a, b) => + a.namePlural.localeCompare(b.namePlural), ); + + return { + flatObjectMetadataArray, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + }; } async generateCoreSchema(request: Request): Promise { @@ -105,33 +117,65 @@ export class OpenApiService { return schema; } - const objectMetadataItems = await this.getObjectMetadataItems(workspace); + const { + flatObjectMetadataArray, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = await this.getFlatObjectMetadataArray(workspace.id); - if (!objectMetadataItems.length) { + if (!flatObjectMetadataArray.length) { return schema; } const workspaceFeatureFlagsMap = await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspace.id); - const filteredObjectMetadataItems = objectMetadataItems.filter((item) => { - return !shouldExcludeFromWorkspaceApi( - item, - standardObjectMetadataDefinitions, - workspaceFeatureFlagsMap, - ); - }); + const filteredObjectMetadataItems = flatObjectMetadataArray.filter( + (item) => { + return !shouldExcludeFromWorkspaceApi( + item, + standardObjectMetadataDefinitions, + workspaceFeatureFlagsMap, + ); + }, + ); schema.paths = filteredObjectMetadataItems.reduce((paths, item) => { - paths[`/${item.namePlural}`] = computeManyResultPath(item); - paths[`/batch/${item.namePlural}`] = computeBatchPath(item); - paths[`/${item.namePlural}/{id}`] = computeSingleResultPath(item); - paths[`/${item.namePlural}/duplicates`] = - computeDuplicatesResultPath(item); - paths[`/restore/${item.namePlural}/{id}`] = - computeRestoreOneResultPath(item); - paths[`/restore/${item.namePlural}`] = computeRestoreManyResultPath(item); - paths[`/${item.namePlural}/merge`] = computeMergeManyResultPath(item); + paths[`/${item.namePlural}`] = computeManyResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/batch/${item.namePlural}`] = computeBatchPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/${item.namePlural}/{id}`] = computeSingleResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/${item.namePlural}/duplicates`] = computeDuplicatesResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/restore/${item.namePlural}/{id}`] = computeRestoreOneResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/restore/${item.namePlural}`] = computeRestoreManyResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); + paths[`/${item.namePlural}/merge`] = computeMergeManyResultPath( + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); return paths; }, schema.paths as OpenAPIV3_1.PathsObject); @@ -143,19 +187,34 @@ export class OpenApiService { DatabaseEventAction.CREATED, item.nameSingular, ) - ] = computeWebhooks(DatabaseEventAction.CREATED, item); + ] = computeWebhooks( + DatabaseEventAction.CREATED, + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); paths[ this.createWebhookEventName( DatabaseEventAction.UPDATED, item.nameSingular, ) - ] = computeWebhooks(DatabaseEventAction.UPDATED, item); + ] = computeWebhooks( + DatabaseEventAction.UPDATED, + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); paths[ this.createWebhookEventName( DatabaseEventAction.DELETED, item.nameSingular, ) - ] = computeWebhooks(DatabaseEventAction.DELETED, item); + ] = computeWebhooks( + DatabaseEventAction.DELETED, + item, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); return paths; }, @@ -167,7 +226,11 @@ export class OpenApiService { schema.components = { ...schema.components, // components.securitySchemes is defined in base Schema - schemas: computeSchemaComponents(filteredObjectMetadataItems), + schemas: computeSchemaComponents( + filteredObjectMetadataItems, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ), parameters: computeParameterComponents(), responses: { '400': get400ErrorResponses(), @@ -347,7 +410,7 @@ export class OpenApiService { metadata.map((item) => ({ nameSingular: item.nameSingular, namePlural: item.namePlural, - })) as ObjectMetadataEntity[], + })) as FlatObjectMetadata[], ); return schema; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts index 125330843db..dc3515575d5 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts @@ -5,15 +5,48 @@ import { FieldMetadataType, NumberDataType } from 'twenty-shared/types'; import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { computeSchemaComponents } from 'src/engine/core-modules/open-api/utils/components.utils'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; describe('computeSchemaComponents', () => { faker.seed(1); it('should compute schema components', () => { + const flatObjectMetadata: FlatObjectMetadata = { + ...objectMetadataItemMock, + universalIdentifier: 'objectName', + fieldMetadataIds: objectMetadataItemMock.fields.map((f) => f.id), + } as any; + + const flatFieldMetadataMaps = { + byId: Object.fromEntries( + objectMetadataItemMock.fields.map((f) => [f.id, f as any]), + ), + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }; + + const relationTargetObjectMetadata: FlatObjectMetadata = { + id: 'relationTargetObjectId', + nameSingular: 'relationTargetObject', + namePlural: 'relationTargetObjects', + universalIdentifier: 'relationTargetObject', + fieldMetadataIds: [], + } as any; + + const flatObjectMetadataMaps = { + byId: { + [flatObjectMetadata.id]: flatObjectMetadata, + [relationTargetObjectMetadata.id]: relationTargetObjectMetadata, + }, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }; + expect( - computeSchemaComponents([ - objectMetadataItemMock, - ] as ObjectMetadataEntity[]), + computeSchemaComponents( + [flatObjectMetadata], + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ), ).toMatchInlineSnapshot(` { "ObjectName": { @@ -819,17 +852,37 @@ describe('computeSchemaComponents', () => { ]; it.each(testsCases)('$title', ({ context: field }) => { + const flatObjectMetadata: FlatObjectMetadata = { + targetTableName: 'testingObject', + id: 'mockObjectId', + nameSingular: 'objectName', + namePlural: 'objectsName', + universalIdentifier: 'objectName', + fieldMetadataIds: [field.id], + } as any; + + const flatFieldMetadataMaps = { + byId: { + [field.id]: field as any, + }, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }; + + const flatObjectMetadataMaps = { + byId: { + [flatObjectMetadata.id]: flatObjectMetadata, + }, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }; + expect( - computeSchemaComponents([ - { - targetTableName: 'testingObject', - id: 'mockObjectId', - nameSingular: 'objectName', - namePlural: 'objectsName', - //@ts-expect-error Passing partial FieldMetadataEntity array - fields: [field], - }, - ]), + computeSchemaComponents( + [flatObjectMetadata], + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ), ).toMatchSnapshot(); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index 55ef3053198..a553eda44e8 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -17,6 +17,10 @@ import { computeStartingAfterParameters, computeUpsertParameters, } from 'src/engine/core-modules/open-api/utils/parameters.utils'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { convertObjectMetadataToSchemaProperties } from 'src/engine/utils/convert-object-metadata-to-schema-properties.util'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; @@ -31,12 +35,18 @@ type Properties = { type OpenApiExample = Record; const getSchemaComponentsExample = ( - item: ObjectMetadataEntity, + item: FlatObjectMetadata, + flatFieldMetadatas: FlatFieldMetadata[], ): OpenApiExample => { - return item.fields.reduce((node, field) => { + return flatFieldMetadatas.reduce((node, field) => { // If field is required if (!field.isNullable && field.defaultValue === null) { - return { ...node, [field.name]: generateRandomFieldValue({ field }) }; + return { + ...node, + [field.name]: generateRandomFieldValue({ + field, + }), + }; } switch (field.type) { @@ -60,7 +70,9 @@ const getSchemaComponentsExample = ( case FieldMetadataType.PHONES: { return { ...node, - [field.name]: generateRandomFieldValue({ field }), + [field.name]: generateRandomFieldValue({ + field, + }), }; } @@ -72,9 +84,13 @@ const getSchemaComponentsExample = ( }; const getSchemaComponentsRelationProperties = ( - item: ObjectMetadataEntity, + flatFieldMetadatas: FlatFieldMetadata[], + flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], ): Properties => { - return item.fields.reduce((node, field) => { + return flatFieldMetadatas.reduce((node, field) => { if (field.type !== FieldMetadataType.RELATION) { return node; } @@ -82,13 +98,20 @@ const getSchemaComponentsRelationProperties = ( let itemProperty = {} as Property; if (isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)) { + const targetObjectMetadata = + flatObjectMetadataMaps.byId[field.relationTargetObjectMetadataId]; + + if (!targetObjectMetadata) { + return node; + } + if (field.settings?.relationType === RelationType.MANY_TO_ONE) { itemProperty = { type: 'object', oneOf: [ { $ref: `#/components/schemas/${capitalize( - field.relationTargetObjectMetadata.nameSingular, + targetObjectMetadata.nameSingular, )}ForResponse`, }, ], @@ -98,7 +121,7 @@ const getSchemaComponentsRelationProperties = ( type: 'array', items: { $ref: `#/components/schemas/${capitalize( - field.relationTargetObjectMetadata.nameSingular, + targetObjectMetadata.nameSingular, )}ForResponse`, }, }; @@ -117,8 +140,10 @@ const getSchemaComponentsRelationProperties = ( }, {} as Properties); }; -const getRequiredFields = (item: ObjectMetadataEntity): string[] => { - return item.fields.reduce((required, field) => { +const getRequiredFields = ( + flatFieldMetadatas: FlatFieldMetadata[], +): string[] => { + return flatFieldMetadatas.reduce((required, field) => { if (!field.isNullable && field.defaultValue === null) { required.push(field.name); @@ -131,10 +156,17 @@ const getRequiredFields = (item: ObjectMetadataEntity): string[] => { const computeSchemaComponent = ({ item, + flatFieldMetadatas, + flatObjectMetadataMaps, forResponse, forUpdate, }: { - item: ObjectMetadataEntity; + item: FlatObjectMetadata; + flatFieldMetadatas: FlatFieldMetadata[]; + flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps']; forResponse: boolean; forUpdate: boolean; }): OpenAPIV3_1.SchemaObject => { @@ -142,20 +174,31 @@ const computeSchemaComponent = ({ const withRequiredFields = !forResponse && !forUpdate; + // Create a temporary object that looks like ObjectMetadataEntity for the converter + const tempItem = { + ...item, + fields: flatFieldMetadatas, + } as unknown as ObjectMetadataEntity; + const result: OpenAPIV3_1.SchemaObject = { type: 'object', description: item.description ?? undefined, properties: convertObjectMetadataToSchemaProperties({ - item, + item: tempItem, forResponse, }) as Properties, - ...(!forResponse ? { example: getSchemaComponentsExample(item) } : {}), + ...(!forResponse + ? { example: getSchemaComponentsExample(item, flatFieldMetadatas) } + : {}), }; if (withRelations) { result.properties = { ...result.properties, - ...getSchemaComponentsRelationProperties(item), + ...getSchemaComponentsRelationProperties( + flatFieldMetadatas, + flatObjectMetadataMaps, + ), }; } @@ -163,7 +206,7 @@ const computeSchemaComponent = ({ return result; } - const requiredFields = getRequiredFields(item); + const requiredFields = getRequiredFields(flatFieldMetadatas); if (requiredFields?.length) { result.required = requiredFields; @@ -173,24 +216,44 @@ const computeSchemaComponent = ({ }; export const computeSchemaComponents = ( - objectMetadataItems: ObjectMetadataEntity[], + flatObjectMetadataItems: FlatObjectMetadata[], + flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): Record => { - return objectMetadataItems.reduce( + return flatObjectMetadataItems.reduce( (schemas, item) => { + const flatFieldMetadatas = + findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityIds: item.fieldMetadataIds, + }); + schemas[capitalize(item.nameSingular)] = computeSchemaComponent({ item, + flatFieldMetadatas, + flatObjectMetadataMaps, forResponse: false, forUpdate: false, }); schemas[capitalize(item.nameSingular) + 'ForUpdate'] = computeSchemaComponent({ item, + flatFieldMetadatas, + flatObjectMetadataMaps, forResponse: false, forUpdate: true, }); schemas[capitalize(item.nameSingular) + 'ForResponse'] = computeSchemaComponent({ item, + flatFieldMetadatas, + flatObjectMetadataMaps, forResponse: true, forUpdate: false, }); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/compute-schema-tags.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/compute-schema-tags.utils.ts index 9fd013f0b50..bff393dcae7 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/compute-schema-tags.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/compute-schema-tags.utils.ts @@ -1,10 +1,10 @@ import { type OpenAPIV3_1 } from 'openapi-types'; import { capitalize } from 'twenty-shared/utils'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const computeSchemaTags = ( - items: ObjectMetadataEntity[], + items: Pick[], ): OpenAPIV3_1.TagObject[] => { const results = [{ name: 'General', description: 'General requests' }]; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts index efa50eeee4b..d7c10ae9e8e 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/computeWebhooks.utils.ts @@ -2,11 +2,20 @@ import { type OpenAPIV3_1 } from 'openapi-types'; import { capitalize } from 'twenty-shared/utils'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const computeWebhooks = ( type: DatabaseEventAction, - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { const updatedFields = { type: 'array', diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts index 9f285702168..ca46acac848 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts @@ -6,11 +6,12 @@ import { v4 } from 'uuid'; import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export const generateRandomFieldValue = ({ field, }: { - field: FieldMetadataEntity; + field: FieldMetadataEntity | FlatFieldMetadata; }): FieldMetadataDefaultValue => { switch (field.type) { case FieldMetadataType.UUID: { diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts index 17d3de5acb7..2f53964f432 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/path.utils.ts @@ -23,10 +23,19 @@ import { getUpdateManyResponse200, getUpdateOneResponse200, } from 'src/engine/core-modules/open-api/utils/responses.utils'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const computeBatchPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { post: { @@ -48,7 +57,15 @@ export const computeBatchPath = ( }; export const computeManyResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { get: { @@ -118,7 +135,15 @@ export const computeManyResultPath = ( }; export const computeSingleResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { get: { @@ -189,7 +214,15 @@ export const computeOpenApiPath = ( }; export const computeDuplicatesResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { post: { @@ -209,7 +242,15 @@ export const computeDuplicatesResultPath = ( }; export const computeRestoreOneResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { patch: { @@ -230,7 +271,15 @@ export const computeRestoreOneResultPath = ( }; export const computeRestoreManyResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { patch: { @@ -251,7 +300,15 @@ export const computeRestoreManyResultPath = ( }; export const computeMergeManyResultPath = ( - item: ObjectMetadataEntity, + item: Pick, + _flatObjectMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatObjectMetadataMaps' + >['flatObjectMetadataMaps'], + _flatFieldMetadataMaps: Pick< + AllFlatEntityMaps, + 'flatFieldMetadataMaps' + >['flatFieldMetadataMaps'], ): OpenAPIV3_1.PathItemObject => { return { patch: { diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts index 88aba47abee..feaed463e3b 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/responses.utils.ts @@ -1,9 +1,9 @@ import { capitalize } from 'twenty-shared/utils'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const getFindManyResponse200 = ( - item: Pick, + item: Pick, fromMetadata = false, ) => { const schemaRef = `#/components/schemas/${capitalize( @@ -55,7 +55,7 @@ export const getFindManyResponse200 = ( }; export const getFindOneResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)}ForResponse`; @@ -82,7 +82,7 @@ export const getFindOneResponse200 = ( }; export const getRestoreOneResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)}ForResponse`; @@ -109,7 +109,7 @@ export const getRestoreOneResponse200 = ( }; export const getRestoreManyResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize( item.nameSingular, @@ -141,7 +141,7 @@ export const getRestoreManyResponse200 = ( }; export const getCreateOneResponse201 = ( - item: Pick, + item: Pick, fromMetadata = false, ) => { const one = fromMetadata ? 'One' : ''; @@ -173,7 +173,7 @@ export const getCreateOneResponse201 = ( }; export const getCreateManyResponse201 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize( item.nameSingular, @@ -205,7 +205,7 @@ export const getCreateManyResponse201 = ( }; export const getUpdateOneResponse200 = ( - item: Pick, + item: Pick, fromMetadata = false, ) => { const one = fromMetadata ? 'One' : ''; @@ -234,7 +234,7 @@ export const getUpdateOneResponse200 = ( }; export const getDeleteManyResponse200 = ( - item: Pick, + item: Pick, ) => { return { description: 'Successful operation', @@ -268,7 +268,7 @@ export const getDeleteManyResponse200 = ( }; export const getUpdateManyResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize(item.nameSingular)}ForResponse`; @@ -298,7 +298,7 @@ export const getUpdateManyResponse200 = ( }; export const getDeleteResponse200 = ( - item: Pick, + item: Pick, fromMetadata = false, ) => { const one = fromMetadata ? 'One' : ''; @@ -391,7 +391,7 @@ export const getJsonResponse = () => { }; export const getFindDuplicatesResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize( item.nameSingular, @@ -441,7 +441,7 @@ export const getFindDuplicatesResponse200 = ( }; export const getMergeManyResponse200 = ( - item: Pick, + item: Pick, ) => { const schemaRef = `#/components/schemas/${capitalize( item.nameSingular, diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts index e4a00c6c643..93ce3b01a81 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts @@ -1,8 +1,8 @@ import { Injectable, Logger } from '@nestjs/common'; -import { isDefined } from 'class-validator'; -import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow'; import { FieldActorSource } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow'; import { RecordCrudException, @@ -13,11 +13,11 @@ import { getSelectedColumnsFromRestrictedFields } from 'src/engine/core-modules/ import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class CreateRecordService { private readonly logger = new Logger(CreateRecordService.name); @@ -25,7 +25,6 @@ export class CreateRecordService { private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly recordPositionService: RecordPositionService, private readonly recordInputTransformerService: RecordInputTransformerService, - private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: CreateRecordParams): Promise { @@ -48,16 +47,30 @@ export class CreateRecordService { rolePermissionConfig, ); - const { objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - objectName, - workspaceId, + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; + + const objectId = objectIdByNameSingular[objectName]; + + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, ); + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); if ( !canObjectBeManagedByWorkflow({ - nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, - isSystem: objectMetadataItemWithFieldsMaps.isSystem, + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, }) ) { throw new RecordCrudException( @@ -68,34 +81,39 @@ export class CreateRecordService { const position = await this.recordPositionService.buildRecordPosition({ value: 'first', - objectMetadata: objectMetadataItemWithFieldsMaps, + objectMetadata: flatObjectMetadata, workspaceId, }); + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const validObjectRecord = Object.fromEntries( Object.entries(objectRecord).filter( ([key]) => - isDefined(objectMetadataItemWithFieldsMaps.fieldIdByName[key]) || - isDefined( - objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName[key], - ), + isDefined(fieldIdByName[key]) || + isDefined(fieldIdByJoinColumnName[key]), ), ); const transformedObjectRecord = await this.recordInputTransformerService.process({ recordInput: validObjectRecord, - objectMetadataMapItem: objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); const restrictedFields = - repository.objectRecordsPermissions?.[ - objectMetadataItemWithFieldsMaps.id - ]?.restrictedFields; + repository.objectRecordsPermissions?.[flatObjectMetadata.id] + ?.restrictedFields; const selectedColumns = getSelectedColumnsFromRestrictedFields( restrictedFields, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const insertResult = await repository.insert( diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts index a7a1102ab2d..417350b3468 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts @@ -9,17 +9,15 @@ import { } from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception'; import { type DeleteRecordParams } from 'src/engine/core-modules/record-crud/types/delete-record-params.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class DeleteRecordService { private readonly logger = new Logger(DeleteRecordService.name); constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: DeleteRecordParams): Promise { @@ -55,16 +53,27 @@ export class DeleteRecordService { rolePermissionConfig, ); - const { objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - objectName, - workspaceId, + const { flatObjectMetadataMaps, objectIdByNameSingular } = + repository.internalContext; + + const objectId = objectIdByNameSingular[objectName]; + + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, ); + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); if ( !canObjectBeManagedByWorkflow({ - nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, - isSystem: objectMetadataItemWithFieldsMaps.isSystem, + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, }) ) { throw new RecordCrudException( diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts index b112990860f..a174cfde684 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts @@ -3,6 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import isEmpty from 'lodash.isempty'; import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; import { OrderByDirection } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { type ObjectLiteral } from 'typeorm'; import { @@ -12,23 +13,27 @@ import { import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; +import { + RecordCrudException, + RecordCrudExceptionCode, +} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception'; import { type FindRecordsParams } from 'src/engine/core-modules/record-crud/types/find-records-params.type'; import { FindRecordsResult } from 'src/engine/core-modules/record-crud/types/find-records-result.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class FindRecordsService { private readonly logger = new Logger(FindRecordsService.name); constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute( @@ -60,15 +65,30 @@ export class FindRecordsService { rolePermissionConfig, ); - const { objectMetadataItemWithFieldsMaps, objectMetadataMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - objectName, - workspaceId, + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; + + const objectId = objectIdByNameSingular[objectName]; + + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, ); + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadataItemWithFieldsMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); const records = await this.getObjectRecords({ @@ -79,7 +99,8 @@ export class FindRecordsService { offset, repository, graphqlQueryParser, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); const totalCount = await this.getTotalCount({ @@ -87,7 +108,8 @@ export class FindRecordsService { filter, repository, graphqlQueryParser, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); this.logger.log(`Found ${records.length} records in ${objectName}`); @@ -115,10 +137,11 @@ export class FindRecordsService { private applyRestrictedFieldsToQueryBuilder( queryBuilder: WorkspaceSelectQueryBuilder, repository: WorkspaceRepository, - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): WorkspaceSelectQueryBuilder { const restrictedFields = - repository.objectRecordsPermissions?.[objectMetadataItemWithFieldsMaps.id] + repository.objectRecordsPermissions?.[flatObjectMetadata.id] ?.restrictedFields; if (!restrictedFields || isEmpty(restrictedFields)) { @@ -128,7 +151,8 @@ export class FindRecordsService { const selectableFields = getAllSelectableColumnNames({ restrictedFields, objectMetadata: { - objectMetadataMapItem: objectMetadataItemWithFieldsMaps, + objectMetadataMapItem: flatObjectMetadata, + flatFieldMetadataMaps, }, }); @@ -146,7 +170,8 @@ export class FindRecordsService { offset, repository, graphqlQueryParser, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }: { objectName: string; filter: @@ -158,7 +183,8 @@ export class FindRecordsService { offset: number; repository: WorkspaceRepository; graphqlQueryParser: GraphqlQueryParser; - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise { const queryBuilder = repository.createQueryBuilder(objectName); @@ -183,7 +209,8 @@ export class FindRecordsService { const queryBuilderWithSelect = this.applyRestrictedFieldsToQueryBuilder( withOrderByQueryBuilder, repository, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); return queryBuilderWithSelect @@ -197,7 +224,8 @@ export class FindRecordsService { filter, repository, graphqlQueryParser, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }: { objectName: string; filter: @@ -206,7 +234,8 @@ export class FindRecordsService { | undefined; repository: WorkspaceRepository; graphqlQueryParser: GraphqlQueryParser; - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise { const countQueryBuilder = repository.createQueryBuilder(objectName); @@ -225,7 +254,8 @@ export class FindRecordsService { const queryBuilderWithSelect = this.applyRestrictedFieldsToQueryBuilder( withDeletedCountQueryBuilder, repository, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); return queryBuilderWithSelect.getCount(); diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts index 8d974af88ef..4699e2598ed 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts @@ -12,18 +12,16 @@ import { type UpdateRecordParams } from 'src/engine/core-modules/record-crud/typ import { getSelectedColumnsFromRestrictedFields } from 'src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util'; import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class UpdateRecordService { private readonly logger = new Logger(UpdateRecordService.name); constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly recordInputTransformerService: RecordInputTransformerService, - private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: UpdateRecordParams): Promise { @@ -60,20 +58,34 @@ export class UpdateRecordService { rolePermissionConfig, ); - const { objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - objectName, - workspaceId, + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; + + const objectId = objectIdByNameSingular[objectName]; + + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, ); + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); const restrictedFields = - repository.objectRecordsPermissions?.[ - objectMetadataItemWithFieldsMaps.id - ]?.restrictedFields; + repository.objectRecordsPermissions?.[flatObjectMetadata.id] + ?.restrictedFields; const selectedColumns = getSelectedColumnsFromRestrictedFields( restrictedFields, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const previousObjectRecord = await repository.findOne({ @@ -102,8 +114,8 @@ export class UpdateRecordService { if ( !canObjectBeManagedByWorkflow({ - nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, - isSystem: objectMetadataItemWithFieldsMaps.isSystem, + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, }) ) { throw new RecordCrudException( @@ -129,7 +141,8 @@ export class UpdateRecordService { const transformedObjectRecord = await this.recordInputTransformerService.process({ recordInput: objectRecordWithFilteredFields, - objectMetadataMapItem: objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); const updatedObjectRecord = { diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts index 022504f56a6..972f37830d6 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts @@ -14,19 +14,18 @@ import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output. import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/get-composite-type-or-throw.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class UpsertRecordService { private readonly logger = new Logger(UpsertRecordService.name); constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly recordInputTransformerService: RecordInputTransformerService, - private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: UpsertRecordParams): Promise { @@ -53,16 +52,30 @@ export class UpsertRecordService { isDefined(objectRecord[field]), ); - const { objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - objectName, - workspaceId, + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = repository.internalContext; + + const objectId = objectIdByNameSingular[objectName]; + + if (!isDefined(objectId)) { + throw new RecordCrudException( + `Object ${objectName} not found`, + RecordCrudExceptionCode.INVALID_REQUEST, ); + } + + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: objectId, + }); if ( !canObjectBeManagedByWorkflow({ - nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, - isSystem: objectMetadataItemWithFieldsMaps.isSystem, + nameSingular: flatObjectMetadata.nameSingular, + isSystem: flatObjectMetadata.isSystem, }) ) { throw new RecordCrudException( @@ -88,17 +101,24 @@ export class UpsertRecordService { const transformedObjectRecord = await this.recordInputTransformerService.process({ recordInput: objectRecordWithFilteredFields, - objectMetadataMapItem: objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, }); + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const uniqueFieldsToUpdate = fieldsToUpdateArray .map( - (field) => - objectMetadataItemWithFieldsMaps.fieldIdByName[field] || - objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName[field], + (fieldName) => + fieldIdByName[fieldName] || fieldIdByJoinColumnName[fieldName], ) - .map((fieldId) => objectMetadataItemWithFieldsMaps.fieldsById[fieldId]) - .filter((field) => field && (field.isUnique || field.name === 'id')); + .map((fieldId) => flatFieldMetadataMaps.byId[fieldId]) + .filter(isDefined) + .filter((field) => field.isUnique || field.name === 'id'); const conflictPathsUniqueFieldsToUpdate = uniqueFieldsToUpdate.flatMap( (field) => { @@ -140,13 +160,13 @@ export class UpsertRecordService { .filter(isDefined); const restrictedFields = - repository.objectRecordsPermissions?.[ - objectMetadataItemWithFieldsMaps.id - ]?.restrictedFields; + repository.objectRecordsPermissions?.[flatObjectMetadata.id] + ?.restrictedFields; const selectedColumns = getSelectedColumnsFromRestrictedFields( restrictedFields, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const upsertResult = await repository.upsert( diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type.ts b/packages/twenty-server/src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type.ts new file mode 100644 index 00000000000..43dcc9c2a94 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type.ts @@ -0,0 +1,7 @@ +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +// Object metadata type enriched with flat fields for tool schema generation +export type ObjectMetadataForToolSchema = FlatObjectMetadata & { + fields: FlatFieldMetadata[]; +}; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts index 3e9e828b5a1..2e388585647 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-create-record-input-schema.util.ts @@ -1,11 +1,11 @@ import { type RestrictedFieldsPermissions } from 'twenty-shared/types'; import { z } from 'zod'; +import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type'; import { generateRecordPropertiesZodSchema } from 'src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; export const generateCreateRecordInputSchema = ( - objectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataForToolSchema, restrictedFields?: RestrictedFieldsPermissions, ) => { const recordPropertiesSchema = generateRecordPropertiesZodSchema( diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts index 65e014deab3..cd997484001 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/utils/generate-update-record-input-schema.util.ts @@ -1,11 +1,11 @@ import { type RestrictedFieldsPermissions } from 'twenty-shared/types'; import { z } from 'zod'; +import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type'; import { generateRecordPropertiesZodSchema } from 'src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; export const generateUpdateRecordInputSchema = ( - objectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataForToolSchema, restrictedFields?: RestrictedFieldsPermissions, ) => { const recordPropertiesSchema = generateRecordPropertiesZodSchema( diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util.ts index aa5ffd8a79b..0c3ac93abbc 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util.ts @@ -2,11 +2,14 @@ import isEmpty from 'lodash.isempty'; import { type RestrictedFieldsPermissions } from 'twenty-shared/types'; import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export const getSelectedColumnsFromRestrictedFields = ( restrictedFields: RestrictedFieldsPermissions | undefined, - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): string[] | undefined => { if (!restrictedFields || isEmpty(restrictedFields)) { return undefined; @@ -15,7 +18,8 @@ export const getSelectedColumnsFromRestrictedFields = ( const selectableFields = getAllSelectableColumnNames({ restrictedFields, objectMetadata: { - objectMetadataMapItem: objectMetadataItemWithFieldsMaps, + objectMetadataMapItem: flatObjectMetadata, + flatFieldMetadataMaps, }, }); diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts index b93c5623dd9..472bb6def96 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema.ts @@ -1,15 +1,16 @@ -import { z } from 'zod'; import { FieldMetadataType } from 'twenty-shared/types'; +import { z } from 'zod'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; const NullCheckEnum = z.enum(['NULL', 'NOT_NULL']); export const generateFieldFilterZodSchema = ( - field: FieldMetadataEntity, + field: FieldMetadataEntity | FlatFieldMetadata, ): z.ZodTypeAny | null => { switch (field.type) { case FieldMetadataType.UUID: diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts index 66bfacdf34c..ae554682d9f 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema.ts @@ -5,14 +5,14 @@ import { } from 'twenty-shared/types'; import { z } from 'zod'; +import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type'; import { generateFieldFilterZodSchema } from 'src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema'; import { ObjectRecordOrderBySchema } from 'src/engine/core-modules/record-crud/zod-schemas/order-by.zod-schema'; import { shouldExcludeFieldFromAgentToolSchema } from 'src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export const generateFindToolInputSchema = ( - objectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataForToolSchema, restrictedFields?: RestrictedFieldsPermissions, ) => { const filterShape: Record = {}; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema.ts b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema.ts index 41bb9d6134b..e24d29357ae 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema.ts @@ -9,11 +9,11 @@ import { z } from 'zod'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; -const isFieldAvailable = (field: FieldMetadataEntity, forResponse: boolean) => { +const isFieldAvailable = (field: FlatFieldMetadata, forResponse: boolean) => { if (forResponse) { return true; } @@ -28,7 +28,7 @@ const isFieldAvailable = (field: FieldMetadataEntity, forResponse: boolean) => { } }; -const getFieldZodType = (field: FieldMetadataEntity): z.ZodTypeAny => { +const getFieldZodType = (field: FlatFieldMetadata): z.ZodTypeAny => { switch (field.type) { case FieldMetadataType.UUID: return z.string().uuidv4(); @@ -73,7 +73,7 @@ const getFieldZodType = (field: FieldMetadataEntity): z.ZodTypeAny => { }; export const generateRecordPropertiesZodSchema = ( - objectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataForToolSchema, forResponse = false, restrictedFields?: RestrictedFieldsPermissions, ): z.ZodObject> => { diff --git a/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts b/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts index eb867ae12eb..75d5f222024 100644 --- a/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-transformer/services/record-input-transformer.service.ts @@ -11,22 +11,34 @@ import { transformEmailsValue } from 'src/engine/core-modules/record-transformer import { transformLinksValue } from 'src/engine/core-modules/record-transformer/utils/transform-links-value.util'; import { transformPhonesValue } from 'src/engine/core-modules/record-transformer/utils/transform-phones-value.util'; import { transformRichTextV2Value } from 'src/engine/core-modules/record-transformer/utils/transform-rich-text-v2.util'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; @Injectable() export class RecordInputTransformerService { async process({ recordInput, - objectMetadataMapItem, + flatObjectMetadata, + flatFieldMetadataMaps, }: { recordInput: Partial; - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise> { let transformedEntries = {}; + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + for (const [key, value] of Object.entries(recordInput)) { - const fieldMetadataId = objectMetadataMapItem.fieldIdByName[key]; - const fieldMetadata = objectMetadataMapItem.fieldsById[fieldMetadataId]; + const fieldMetadataId = fieldIdByName[key]; + const fieldMetadata = fieldMetadataId + ? flatFieldMetadataMaps.byId[fieldMetadataId] + : undefined; if (!fieldMetadata) { transformedEntries = { ...transformedEntries, [key]: value }; diff --git a/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts b/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts index 2bfa4583658..6a984ed3174 100644 --- a/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/search/__tests__/search.service.spec.ts @@ -1,7 +1,10 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { encodeCursorData } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { + mockFlatFieldMetadataMaps, + mockFlatObjectMetadatas, +} from 'src/engine/core-modules/__mocks__/mockFlatObjectMetadatas'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { SearchService } from 'src/engine/core-modules/search/services/search.service'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; @@ -30,53 +33,53 @@ describe('SearchService', () => { describe('filterObjectMetadataItems', () => { it('should return searchable object metadata items', () => { const objectMetadataItems = service.filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps: mockObjectMetadataItemsWithFieldMaps, + flatObjectMetadatas: mockFlatObjectMetadatas, includedObjectNameSingulars: [], excludedObjectNameSingulars: [], }); expect(objectMetadataItems).toEqual([ - mockObjectMetadataItemsWithFieldMaps[0], - mockObjectMetadataItemsWithFieldMaps[1], - mockObjectMetadataItemsWithFieldMaps[2], + mockFlatObjectMetadatas[0], + mockFlatObjectMetadatas[1], + mockFlatObjectMetadatas[2], ]); }); it('should return searchable object metadata items without excluded ones', () => { const objectMetadataItems = service.filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps: mockObjectMetadataItemsWithFieldMaps, + flatObjectMetadatas: mockFlatObjectMetadatas, includedObjectNameSingulars: [], excludedObjectNameSingulars: ['company'], }); expect(objectMetadataItems).toEqual([ - mockObjectMetadataItemsWithFieldMaps[0], - mockObjectMetadataItemsWithFieldMaps[2], + mockFlatObjectMetadatas[0], + mockFlatObjectMetadatas[2], ]); }); it('should return searchable object metadata items with included ones only', () => { const objectMetadataItems = service.filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps: mockObjectMetadataItemsWithFieldMaps, + flatObjectMetadatas: mockFlatObjectMetadatas, includedObjectNameSingulars: ['company'], excludedObjectNameSingulars: [], }); - expect(objectMetadataItems).toEqual([ - mockObjectMetadataItemsWithFieldMaps[1], - ]); + expect(objectMetadataItems).toEqual([mockFlatObjectMetadatas[1]]); }); }); describe('getLabelIdentifierColumns', () => { it('should return the two label identifier columns for a person object metadata item', () => { const labelIdentifierColumns = service.getLabelIdentifierColumns( - mockObjectMetadataItemsWithFieldMaps[0], + mockFlatObjectMetadatas[0], + mockFlatFieldMetadataMaps, ); expect(labelIdentifierColumns).toEqual(['nameFirstName', 'nameLastName']); }); it('should return the label identifier column for a regular object metadata item', () => { const labelIdentifierColumns = service.getLabelIdentifierColumns( - mockObjectMetadataItemsWithFieldMaps[1], + mockFlatObjectMetadatas[1], + mockFlatFieldMetadataMaps, ); expect(labelIdentifierColumns).toEqual(['name']); @@ -86,14 +89,16 @@ describe('SearchService', () => { describe('getImageIdentifierColumn', () => { it('should return null if the object metadata item does not have an image identifier', () => { const imageIdentifierColumn = service.getImageIdentifierColumn( - mockObjectMetadataItemsWithFieldMaps[0], + mockFlatObjectMetadatas[0], + mockFlatFieldMetadataMaps, ); expect(imageIdentifierColumn).toBeNull(); }); it('should return `domainNamePrimaryLinkUrl` column for a company object metadata item', () => { const imageIdentifierColumn = service.getImageIdentifierColumn( - mockObjectMetadataItemsWithFieldMaps[1], + mockFlatObjectMetadatas[1], + mockFlatFieldMetadataMaps, ); expect(imageIdentifierColumn).toEqual('domainNamePrimaryLinkUrl'); @@ -101,7 +106,8 @@ describe('SearchService', () => { it('should return the image identifier column', () => { const imageIdentifierColumn = service.getImageIdentifierColumn( - mockObjectMetadataItemsWithFieldMaps[2], + mockFlatObjectMetadatas[2], + mockFlatFieldMetadataMaps, ); expect(imageIdentifierColumn).toEqual('imageIdentifierFieldName'); diff --git a/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts b/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts index 78b84504f7e..66fcd12a76d 100644 --- a/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/search/exceptions/search.exception.ts @@ -4,4 +4,5 @@ export class SearchException extends CustomException {} export enum SearchExceptionCode { LABEL_IDENTIFIER_FIELD_NOT_FOUND = 'LABEL_IDENTIFIER_FIELD_NOT_FOUND', + OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', } diff --git a/packages/twenty-server/src/engine/core-modules/search/filters/search-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/search/filters/search-api-exception.filter.ts index bcd24349c4a..61cda97fec7 100644 --- a/packages/twenty-server/src/engine/core-modules/search/filters/search-api-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/search/filters/search-api-exception.filter.ts @@ -14,6 +14,7 @@ export class SearchApiExceptionFilter implements ExceptionFilter { catch(exception: SearchException) { switch (exception.code) { case SearchExceptionCode.LABEL_IDENTIFIER_FIELD_NOT_FOUND: + case SearchExceptionCode.OBJECT_METADATA_NOT_FOUND: throw exception; default: { assertUnreachable(exception.code); diff --git a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts index 2ec10e1211f..caf1b4f27fc 100644 --- a/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/search/search.resolver.ts @@ -14,9 +14,6 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @Resolver() @UseFilters(SearchApiExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter) @@ -41,48 +38,29 @@ export class SearchResolver { after, }: SearchArgs, ) { - const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } = + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId: workspace.id, - flatMapsKeys: [ - 'flatObjectMetadataMaps', - 'flatFieldMetadataMaps', - 'flatIndexMaps', - ], + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], }, ); - const { idByNameSingular } = buildObjectIdByNameMaps( - flatObjectMetadataMaps, - ); - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, - }; - - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } + const flatObjectMetadatas = Object.values( + flatObjectMetadataMaps.byId, + ).filter(isDefined); const filteredObjectMetadataItems = this.searchService.filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps: Object.values( - objectMetadataMaps.byId, - ).filter(isDefined), + flatObjectMetadatas, includedObjectNameSingulars: includedObjectNameSingulars ?? [], excludedObjectNameSingulars: excludedObjectNameSingulars ?? [], }); const allRecordsWithObjectMetadataItems = await this.searchService.getAllRecordsWithObjectMetadataItems({ - objectMetadataItemWithFieldMaps: filteredObjectMetadataItems, + flatObjectMetadatas: filteredObjectMetadataItems, + flatFieldMetadataMaps, searchInput, limit, filter, @@ -93,6 +71,7 @@ export class SearchResolver { return this.searchService.computeSearchObjectResults({ recordsWithObjectMetadataItems: allRecordsWithObjectMetadataItems, + flatFieldMetadataMaps, workspaceId: workspace.id, limit, after, diff --git a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts index 9823fd69f6d..c7045d8bc40 100644 --- a/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts +++ b/packages/twenty-server/src/engine/core-modules/search/services/search.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { isNonEmptyString } from '@sniptt/guards'; import chunk from 'lodash.chunk'; import { FieldMetadataType, ObjectRecord } from 'twenty-shared/types'; -import { getLogoUrlFromDomainName } from 'twenty-shared/utils'; +import { getLogoUrlFromDomainName, isDefined } from 'twenty-shared/utils'; import { Brackets, type ObjectLiteral } from 'typeorm'; import { type ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; @@ -26,9 +26,10 @@ import { } from 'src/engine/core-modules/search/exceptions/search.exception'; import { type RecordsWithObjectMetadataItem } from 'src/engine/core-modules/search/types/records-with-object-metadata-item'; import { formatSearchTerms } from 'src/engine/core-modules/search/utils/format-search-terms'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; @@ -49,7 +50,8 @@ export class SearchService { ) {} async getAllRecordsWithObjectMetadataItems({ - objectMetadataItemWithFieldMaps, + flatObjectMetadatas, + flatFieldMetadataMaps, includedObjectNameSingulars, excludedObjectNameSingulars, searchInput, @@ -57,10 +59,11 @@ export class SearchService { filter, after, }: { - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps[]; + flatObjectMetadatas: FlatObjectMetadata[]; + flatFieldMetadataMaps: FlatEntityMaps; } & SearchArgs) { const filteredObjectMetadataItems = this.filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps, + flatObjectMetadatas, includedObjectNameSingulars: includedObjectNameSingulars ?? [], excludedObjectNameSingulars: excludedObjectNameSingulars ?? [], }); @@ -75,17 +78,18 @@ export class SearchService { for (const objectMetadataItemChunk of filteredObjectMetadataItemsChunks) { const recordsWithObjectMetadataItems = await Promise.all( - objectMetadataItemChunk.map(async (objectMetadataItem) => { + objectMetadataItemChunk.map(async (flatObjectMetadata) => { const repository = await this.twentyORMManager.getRepository( - objectMetadataItem.nameSingular, + flatObjectMetadata.nameSingular, ); return { - objectMetadataItem, + objectMetadataItem: flatObjectMetadata, records: await this.buildSearchQueryAndGetRecords({ entityManager: repository, - objectMetadataItem, + flatObjectMetadata, + flatFieldMetadataMaps, searchTerms: formatSearchTerms(searchInput, 'and'), searchTermsOr: formatSearchTerms(searchInput, 'or'), limit: limit as number, @@ -103,34 +107,33 @@ export class SearchService { } filterObjectMetadataItems({ - objectMetadataItemWithFieldMaps, + flatObjectMetadatas, includedObjectNameSingulars, excludedObjectNameSingulars, }: { - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps[]; + flatObjectMetadatas: FlatObjectMetadata[]; includedObjectNameSingulars: string[]; excludedObjectNameSingulars: string[]; }) { - return objectMetadataItemWithFieldMaps.filter( - ({ nameSingular, isSearchable }) => { - if (!isSearchable) { - return false; - } - if (excludedObjectNameSingulars.includes(nameSingular)) { - return false; - } - if (includedObjectNameSingulars.length > 0) { - return includedObjectNameSingulars.includes(nameSingular); - } + return flatObjectMetadatas.filter(({ nameSingular, isSearchable }) => { + if (!isSearchable) { + return false; + } + if (excludedObjectNameSingulars.includes(nameSingular)) { + return false; + } + if (includedObjectNameSingulars.length > 0) { + return includedObjectNameSingulars.includes(nameSingular); + } - return true; - }, - ); + return true; + }); } async buildSearchQueryAndGetRecords({ entityManager, - objectMetadataItem, + flatObjectMetadata, + flatFieldMetadataMaps, searchTerms, searchTermsOr, limit, @@ -138,7 +141,8 @@ export class SearchService { after, }: { entityManager: WorkspaceRepository; - objectMetadataItem: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; searchTerms: string; searchTermsOr: string; limit: number; @@ -147,30 +151,33 @@ export class SearchService { }) { const queryBuilder = entityManager.createQueryBuilder(); + const { flatObjectMetadataMaps } = entityManager.internalContext; + const queryParser = new GraphqlQueryParser( - objectMetadataItem, - generateObjectMetadataMaps([ - { - ...objectMetadataItem, - fields: Object.values(objectMetadataItem.fieldsById), - }, - ]), + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); queryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItem.nameSingular, + flatObjectMetadata.nameSingular, filter, ); queryParser.applyDeletedAtToBuilder(queryBuilder, filter); - const imageIdentifierField = - this.getImageIdentifierColumn(objectMetadataItem); + const imageIdentifierField = this.getImageIdentifierColumn( + flatObjectMetadata, + flatFieldMetadataMaps, + ); const fieldsToSelect = [ 'id', - ...this.getLabelIdentifierColumns(objectMetadataItem), + ...this.getLabelIdentifierColumns( + flatObjectMetadata, + flatFieldMetadataMaps, + ), ...(imageIdentifierField ? [imageIdentifierField] : []), ].map((field) => `"${field}"`); @@ -180,7 +187,7 @@ export class SearchService { const cursorWhereCondition = this.computeCursorWhereCondition({ after, - objectMetadataNameSingular: objectMetadataItem.nameSingular, + objectMetadataNameSingular: flatObjectMetadata.nameSingular, tsRankExpr, tsRankCDExpr, }); @@ -273,9 +280,10 @@ export class SearchService { } getLabelIdentifierColumns( - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { - if (!objectMetadataItem.labelIdentifierFieldMetadataId) { + if (!flatObjectMetadata.labelIdentifierFieldMetadataId) { throw new SearchException( 'Label identifier field not found', SearchExceptionCode.LABEL_IDENTIFIER_FIELD_NOT_FOUND, @@ -283,10 +291,17 @@ export class SearchService { } const labelIdentifierField = - objectMetadataItem.fieldsById[ - objectMetadataItem.labelIdentifierFieldMetadataId + flatFieldMetadataMaps.byId[ + flatObjectMetadata.labelIdentifierFieldMetadataId ]; + if (!isDefined(labelIdentifierField)) { + throw new SearchException( + 'Label identifier field not found', + SearchExceptionCode.LABEL_IDENTIFIER_FIELD_NOT_FOUND, + ); + } + if (labelIdentifierField.type === FieldMetadataType.FULL_NAME) { return [ `${labelIdentifierField.name}FirstName`, @@ -294,37 +309,44 @@ export class SearchService { ]; } - return [ - objectMetadataItem.fieldsById[ - objectMetadataItem.labelIdentifierFieldMetadataId - ].name, - ]; + return [labelIdentifierField.name]; } getLabelIdentifierValue( record: ObjectRecord, - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): string { - const labelIdentifierFields = - this.getLabelIdentifierColumns(objectMetadataItem); + const labelIdentifierFields = this.getLabelIdentifierColumns( + flatObjectMetadata, + flatFieldMetadataMaps, + ); return labelIdentifierFields.map((field) => record[field]).join(' '); } getImageIdentifierColumn( - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { - if (objectMetadataItem.nameSingular === 'company') { + if (flatObjectMetadata.nameSingular === 'company') { return 'domainNamePrimaryLinkUrl'; } - if (!objectMetadataItem.imageIdentifierFieldMetadataId) { + if (!flatObjectMetadata.imageIdentifierFieldMetadataId) { return null; } - return objectMetadataItem.fieldsById[ - objectMetadataItem.imageIdentifierFieldMetadataId - ].name; + const imageIdentifierField = + flatFieldMetadataMaps.byId[ + flatObjectMetadata.imageIdentifierFieldMetadataId + ]; + + if (!isDefined(imageIdentifierField)) { + return null; + } + + return imageIdentifierField.name; } private getImageUrlWithToken(avatarUrl: string, workspaceId: string): string { @@ -336,13 +358,16 @@ export class SearchService { getImageIdentifierValue( record: ObjectRecord, - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, workspaceId: string, ): string { - const imageIdentifierField = - this.getImageIdentifierColumn(objectMetadataItem); + const imageIdentifierField = this.getImageIdentifierColumn( + flatObjectMetadata, + flatFieldMetadataMaps, + ); - if (objectMetadataItem.nameSingular === 'company') { + if (flatObjectMetadata.nameSingular === 'company') { return getLogoUrlFromDomainName(record.domainNamePrimaryLinkUrl) || ''; } @@ -391,11 +416,13 @@ export class SearchService { computeSearchObjectResults({ recordsWithObjectMetadataItems, + flatFieldMetadataMaps, workspaceId, limit, after, }: { recordsWithObjectMetadataItems: RecordsWithObjectMetadataItem[]; + flatFieldMetadataMaps: FlatEntityMaps; workspaceId: string; limit: number; after?: string; @@ -406,10 +433,15 @@ export class SearchService { return { recordId: record.id, objectNameSingular: objectMetadataItem.nameSingular, - label: this.getLabelIdentifierValue(record, objectMetadataItem), + label: this.getLabelIdentifierValue( + record, + objectMetadataItem, + flatFieldMetadataMaps, + ), imageUrl: this.getImageIdentifierValue( record, objectMetadataItem, + flatFieldMetadataMaps, workspaceId, ), tsRankCD: record.tsRankCD, diff --git a/packages/twenty-server/src/engine/core-modules/search/types/records-with-object-metadata-item.ts b/packages/twenty-server/src/engine/core-modules/search/types/records-with-object-metadata-item.ts index f9beee15ea1..030c52398a6 100644 --- a/packages/twenty-server/src/engine/core-modules/search/types/records-with-object-metadata-item.ts +++ b/packages/twenty-server/src/engine/core-modules/search/types/records-with-object-metadata-item.ts @@ -1,8 +1,8 @@ import { type ObjectRecord } from 'twenty-shared/types'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export type RecordsWithObjectMetadataItem = { - objectMetadataItem: ObjectMetadataItemWithFieldMaps; + objectMetadataItem: FlatObjectMetadata; records: ObjectRecord[]; }; diff --git a/packages/twenty-server/src/engine/core-modules/webhook/utils/__tests__/transform-event-batch-to-webhook-events.spec.ts b/packages/twenty-server/src/engine/core-modules/webhook/utils/__tests__/transform-event-batch-to-webhook-events.spec.ts index 1a28b2ae444..ea8f40ed810 100644 --- a/packages/twenty-server/src/engine/core-modules/webhook/utils/__tests__/transform-event-batch-to-webhook-events.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/webhook/utils/__tests__/transform-event-batch-to-webhook-events.spec.ts @@ -2,18 +2,31 @@ import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/typ import type { ObjectRecordEvent } from 'src/engine/core-modules/event-emitter/types/object-record-event.event'; import type { WebhookEntity } from 'src/engine/core-modules/webhook/webhook.entity'; import { transformEventBatchToWebhookEvents } from 'src/engine/core-modules/webhook/utils/transform-event-batch-to-webhook-events'; -import { getMockObjectMetadataEntity } from 'src/utils/__test__/get-object-metadata-entity.mock'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -const mockObjectMetadata = getMockObjectMetadataEntity({ +const mockObjectMetadata: FlatObjectMetadata = { id: 'id', nameSingular: 'nameSingular', namePlural: 'namePlural', workspaceId: 'workspaceId', -}); + labelSingular: 'Label Singular', + labelPlural: 'Label Plural', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'id', + fieldMetadataIds: [], + indexMetadataIds: [], + viewIds: [], + applicationId: null, +} as unknown as FlatObjectMetadata; describe('transformEventBatchToWebhookEvents', () => { it('should transform properly', () => { - const workspaceEventBatch = { + const workspaceEventBatch: WorkspaceEventBatch = { workspaceId: 'workspaceId', objectMetadata: mockObjectMetadata, name: 'objectNameSingular.created', @@ -48,7 +61,7 @@ describe('transformEventBatchToWebhookEvents', () => { }, }, ], - } as WorkspaceEventBatch; + }; const webhooks = [ { @@ -174,7 +187,7 @@ describe('transformEventBatchToWebhookEvents', () => { }); it('should sanitize records properly', () => { - const workspaceEventBatch = { + const workspaceEventBatch: WorkspaceEventBatch = { workspaceId: 'workspaceId', objectMetadata: mockObjectMetadata, name: 'webhook.created', @@ -190,7 +203,7 @@ describe('transformEventBatchToWebhookEvents', () => { }, }, ], - } as WorkspaceEventBatch; + }; const webhooks = [ { diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/services/agent-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/services/agent-execution.service.ts index 0c692c4ba20..0174324f242 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/services/agent-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/services/agent-execution.service.ts @@ -28,13 +28,12 @@ import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/ag import { AgentActorContextService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-actor-context.service'; import { AgentModelConfigService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-model-config.service'; import { AgentToolGeneratorService } from 'src/engine/metadata-modules/ai/ai-agent/services/agent-tool-generator.service'; -import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type'; +import { RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/ai/ai-agent/types/recordIdsByObjectMetadataNameSingular.type'; import { repairToolCall } from 'src/engine/metadata-modules/ai/ai-agent/utils/repair-tool-call.util'; import { AIBillingService } from 'src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service'; -import { type ToolHints } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface'; +import { ToolHints } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface'; import { AI_TELEMETRY_CONFIG } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-telemetry.const'; import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -188,8 +187,12 @@ export class AgentExecutionService { workspaceId: workspace.id, }); - const objectMetadataMaps = - workspaceDataSource.internalContext.objectMetadataMaps; + const flatObjectMetadataMaps = + workspaceDataSource.internalContext.flatObjectMetadataMaps; + const flatFieldMetadataMaps = + workspaceDataSource.internalContext.flatFieldMetadataMaps; + const objectIdByNameSingular = + workspaceDataSource.internalContext.objectIdByNameSingular; const objectMetadataPermissions = workspaceDataSource.permissionsPerRoleId; const contextObject = ( @@ -200,11 +203,13 @@ export class AgentExecutionService { return []; } - const objectMetadataMapItem = - getObjectMetadataMapItemByNameSingular( - objectMetadataMaps, - recordsWithObjectMetadataNameSingular.objectMetadataNameSingular, - ); + const objectMetadataId = + objectIdByNameSingular[ + recordsWithObjectMetadataNameSingular.objectMetadataNameSingular + ]; + const objectMetadataMapItem = objectMetadataId + ? flatObjectMetadataMaps.byId[objectMetadataId] + : undefined; if (!objectMetadataMapItem) { this.logger.warn( @@ -230,7 +235,10 @@ export class AgentExecutionService { const selectOptions = hasRestrictedFields ? getAllSelectableColumnNames({ restrictedFields, - objectMetadata: { objectMetadataMapItem }, + objectMetadata: { + objectMetadataMapItem, + flatFieldMetadataMaps, + }, }) : undefined; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util.ts index 9cdc361f41b..703f06e7f60 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util.ts @@ -1,7 +1,5 @@ import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; - // All workflow-related standard object IDs that should be filtered out from agent access const WORKFLOW_STANDARD_OBJECT_IDS = [ STANDARD_OBJECT_IDS.workflow, @@ -10,9 +8,9 @@ const WORKFLOW_STANDARD_OBJECT_IDS = [ STANDARD_OBJECT_IDS.workflowAutomatedTrigger, ] as const; -export const isWorkflowRelatedObject = ( - objectMetadata: ObjectMetadataEntity, -): boolean => { +export const isWorkflowRelatedObject = (objectMetadata: { + standardId: string | null; +}): boolean => { return ( objectMetadata.standardId !== null && WORKFLOW_STANDARD_OBJECT_IDS.includes( diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts index 9601c4075a3..429f846c97c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts @@ -10,7 +10,7 @@ import { ToolModule } from 'src/engine/core-modules/tool/tool.module'; import { SearchArticlesTool } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool'; import { ToolAdapterService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool-adapter.service'; import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @@ -27,7 +27,7 @@ import { MessagingModule } from 'src/modules/messaging/messaging.module'; TokenModule, FeatureFlagModule, RecordCrudModule, - ObjectMetadataModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, WorkspacePermissionsCacheModule, WorkspaceCacheStorageModule, UserRoleModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts index 9f3914a2535..8aa0ebe589b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts @@ -6,7 +6,7 @@ import { FindRecordsService } from 'src/engine/core-modules/record-crud/services import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service'; import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service'; -import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @@ -57,9 +57,21 @@ describe('ToolService', () => { }, }, { - provide: ObjectMetadataServiceV2, + provide: WorkspaceManyOrAllFlatEntityMapsCacheService, useValue: { - findManyWithinWorkspace: jest.fn().mockResolvedValue([testObject]), + getOrRecomputeManyOrAllFlatEntityMaps: jest.fn().mockResolvedValue({ + flatObjectMetadataMaps: { + byId: { + [testObject.id]: { + ...testObject, + fieldMetadataIds: [], + }, + }, + }, + flatFieldMetadataMaps: { + byId: {}, + }, + }), }, }, { diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts index 2de7758f15a..04c346af3ed 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts @@ -2,7 +2,9 @@ import { Injectable, Logger } from '@nestjs/common'; import { type ToolSet } from 'ai'; import { type ActorMetadata } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { CreateRecordService } from 'src/engine/core-modules/record-crud/services/create-record.service'; import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service'; import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service'; @@ -15,13 +17,13 @@ import { generateFindToolInputSchema } from 'src/engine/core-modules/record-crud import { SoftDeleteToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/soft-delete-tool.zod-schema'; import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util'; import { - type ToolHints, - type ToolOperation, + ToolHints, + ToolOperation, } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface'; -import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util'; @Injectable() @@ -30,8 +32,8 @@ export class ToolService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly objectMetadataService: ObjectMetadataServiceV2, protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly createRecordService: CreateRecordService, private readonly updateRecordService: UpdateRecordService, private readonly deleteRecordService: DeleteRecordService, @@ -78,14 +80,25 @@ export class ToolService { return tools; } - const allObjectMetadata = - await this.objectMetadataService.findManyWithinWorkspace(workspaceId, { - where: { - isActive: true, - isSystem: false, + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], }, - relations: ['fields'], - }); + ); + + const allFlatObjects = Object.values(flatObjectMetadataMaps.byId) + .filter(isDefined) + .filter((obj) => obj.isActive && !obj.isSystem); + + const allObjectMetadata = allFlatObjects.map((flatObject) => ({ + ...flatObject, + fields: getFlatFieldsFromFlatObjectMetadata( + flatObject, + flatFieldMetadataMaps, + ), + })); let filteredObjectMetadata = allObjectMetadata.filter( (objectMetadata) => !isWorkflowRelatedObject(objectMetadata), diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts index 45701c2d02f..e4a49683cc9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service.ts @@ -17,8 +17,9 @@ import { import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; @Injectable() @@ -30,15 +31,17 @@ export class FieldMetadataMorphRelationService { async createMorphRelationFieldMetadataItems({ fieldMetadataForCreate, morphRelationsCreationPayload, - objectMetadata, + flatObjectMetadata, fieldMetadataRepository, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { fieldMetadataForCreate: CreateFieldInput; morphRelationsCreationPayload: CreateFieldInput['morphRelationsCreationPayload']; - objectMetadata: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; fieldMetadataRepository: Repository; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }): Promise { if ( !isDefined(morphRelationsCreationPayload) || @@ -62,7 +65,9 @@ export class FieldMetadataMorphRelationService { for (const relationCreationPayload of morphRelationsCreationPayload) { const targetObjectMetadata = - objectMetadataMaps.byId[relationCreationPayload.targetObjectMetadataId]; + flatObjectMetadataMaps.byId[ + relationCreationPayload.targetObjectMetadataId + ]; if (!isDefined(targetObjectMetadata)) { throw new FieldMetadataException( @@ -95,8 +100,9 @@ export class FieldMetadataMorphRelationService { { fieldMetadataInput: relationFieldMetadataForCreate, fieldMetadataType: relationFieldMetadataForCreate.type, - objectMetadataMaps, - objectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, }, ); @@ -126,7 +132,7 @@ export class FieldMetadataMorphRelationService { { fieldMetadataInput: targetFieldMetadataToCreate, relationCreationPayload: { - targetObjectMetadataId: objectMetadata.id, + targetObjectMetadataId: flatObjectMetadata.id, targetFieldLabel: fieldMetadataForCreate.label, targetFieldIcon: fieldMetadataForCreate.icon ?? 'Icon123', type: diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts index 5f166734179..817ead6e5b0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts @@ -7,9 +7,9 @@ import { FieldMetadataType, RelationOnDeleteAction, } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { type Repository } from 'typeorm'; import { v4 } from 'uuid'; -import { isDefined } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; @@ -23,8 +23,9 @@ import { import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; import { validateRelationCreationPayloadOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/validate-relation-creation-payload-or-throw.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils'; import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; @@ -49,9 +50,10 @@ type ValidateFieldMetadataArgs = { fieldMetadataType: FieldMetadataType; fieldMetadataInput: T; - objectMetadata: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; existingFieldMetadata?: FieldMetadataEntity; - objectMetadataMaps: ObjectMetadataMaps; }; @Injectable() @@ -62,11 +64,11 @@ export class FieldMetadataRelationService { async createRelationFieldMetadataItems({ fieldMetadataInput, - objectMetadata, + flatObjectMetadata, fieldMetadataRepository, }: { fieldMetadataInput: CreateFieldInput; - objectMetadata: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; fieldMetadataRepository: Repository; }): Promise { const createdFieldMetadataItem = @@ -98,7 +100,7 @@ export class FieldMetadataRelationService { this.computeCustomRelationFieldMetadataForCreation({ fieldMetadataInput: targetFieldMetadataToCreate, relationCreationPayload: { - targetObjectMetadataId: objectMetadata.id, + targetObjectMetadataId: flatObjectMetadata.id, targetFieldLabel: fieldMetadataInput.label, targetFieldIcon: fieldMetadataInput.icon ?? 'Icon123', type: @@ -135,14 +137,16 @@ export class FieldMetadataRelationService { >({ fieldMetadataInput, fieldMetadataType, - objectMetadataMaps, - objectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatObjectMetadata, }: Pick< ValidateFieldMetadataArgs, | 'fieldMetadataInput' | 'fieldMetadataType' - | 'objectMetadataMaps' - | 'objectMetadata' + | 'flatObjectMetadataMaps' + | 'flatFieldMetadataMaps' + | 'flatObjectMetadata' >): Promise { // TODO: clean typings, we should try to validate both update and create inputs in the same function const isRelation = @@ -158,7 +162,8 @@ export class FieldMetadataRelationService { ) { validateFieldNameAvailabilityOrThrow({ name: `${fieldMetadataInput.name}Id`, - fieldMetadataMapById: objectMetadata.fieldsById, + flatObjectMetadata, + flatFieldMetadataMaps, }); const relationCreationPayload = ( @@ -173,12 +178,12 @@ export class FieldMetadataRelationService { validateMetadataNameOrThrow(computedMetadataNameFromLabel); - const objectMetadataTarget = - objectMetadataMaps.byId[ + const targetFlatObjectMetadata = + flatObjectMetadataMaps.byId[ relationCreationPayload.targetObjectMetadataId ]; - if (!isDefined(objectMetadataTarget)) { + if (!isDefined(targetFlatObjectMetadata)) { throw new FieldMetadataException( `Object metadata relation target not found for relation creation payload`, FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, @@ -187,17 +192,19 @@ export class FieldMetadataRelationService { validateFieldNameAvailabilityOrThrow({ name: computedMetadataNameFromLabel, - fieldMetadataMapById: objectMetadataTarget.fieldsById, + flatObjectMetadata: targetFlatObjectMetadata, + flatFieldMetadataMaps, }); validateFieldNameAvailabilityOrThrow({ name: `${computedMetadataNameFromLabel}Id`, - fieldMetadataMapById: objectMetadataTarget.fieldsById, + flatObjectMetadata: targetFlatObjectMetadata, + flatFieldMetadataMaps, }); if ( computedMetadataNameFromLabel === fieldMetadataInput.name && - objectMetadata.id === objectMetadataTarget.id + flatObjectMetadata.id === targetFlatObjectMetadata.id ) { throw new FieldMetadataException( `Name "${computedMetadataNameFromLabel}" cannot be the same on both side of the relation`, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts index 8b466787ca7..d279c101438 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service.ts @@ -30,7 +30,9 @@ import { } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { FieldMetadataEnumValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-enum-validation.service'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils'; @@ -38,7 +40,8 @@ import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/v type ValidateFieldMetadataArgs = { fieldMetadataType: FieldMetadataType; fieldMetadataInput: CreateFieldInput | UpdateFieldInput; - objectMetadata: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; existingFieldMetadata?: FieldMetadataEntity; }; @@ -164,7 +167,8 @@ export class FieldMetadataValidationService { async validateFieldMetadata({ fieldMetadataInput, fieldMetadataType, - objectMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, existingFieldMetadata, }: ValidateFieldMetadataArgs): Promise { if (fieldMetadataInput.name) { @@ -187,7 +191,8 @@ export class FieldMetadataValidationService { try { validateFieldNameAvailabilityOrThrow({ name: fieldMetadataInput.name, - fieldMetadataMapById: objectMetadata.fieldsById, + flatObjectMetadata, + flatFieldMetadataMaps, }); } catch (error) { if (error instanceof InvalidMetadataException) { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts index 599b17813ae..b8a107f94be 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts @@ -3,7 +3,6 @@ import { type CompositeProperty, } from 'twenty-shared/types'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataException, FieldMetadataExceptionCode, @@ -19,8 +18,8 @@ export type FieldTypeAndNameMetadata = { }; // TODO: If we need to implement custom name logic for columns, we can do it here -export function computeColumnName( - fieldMetadataOrFieldName: FieldMetadataEntity | string, +export function computeColumnName( + fieldMetadataOrFieldName: FieldTypeAndNameMetadata | string, options?: ComputeColumnNameOptions, ): string { const generateName = (name: string) => { @@ -40,11 +39,9 @@ export function computeColumnName( return generateName(fieldMetadataOrFieldName.name); } -export function computeCompositeColumnName( - fieldMetadataOrFieldName: - | FieldTypeAndNameMetadata - | FieldMetadataEntity - | string, + +export function computeCompositeColumnName( + fieldMetadataOrFieldName: FieldTypeAndNameMetadata | string, compositeProperty: CompositeProperty, ): string { const generateName = (name: string) => { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util.ts deleted file mode 100644 index a2e65ff266f..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { - WorkspaceMigrationColumnActionType, - type WorkspaceMigrationTableAction, - WorkspaceMigrationTableActionType, -} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { type WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; - -export const createMigrationActions = async ({ - createdFieldMetadataItems, - objectMetadataMap, - isRemoteCreation, - workspaceMigrationFactory, -}: { - createdFieldMetadataItems: FieldMetadataEntity[]; - objectMetadataMap: ObjectMetadataMaps['byId']; - isRemoteCreation: boolean; - workspaceMigrationFactory: WorkspaceMigrationFactory; -}): Promise => { - if (isRemoteCreation) { - return []; - } - - const migrationActions: WorkspaceMigrationTableAction[] = []; - - for (const createdFieldMetadata of createdFieldMetadataItems) { - if ( - isFieldMetadataEntityOfType( - createdFieldMetadata, - FieldMetadataType.RELATION, - ) || - isFieldMetadataEntityOfType( - createdFieldMetadata, - FieldMetadataType.MORPH_RELATION, - ) - ) { - const relationType = createdFieldMetadata.settings?.relationType; - - if (relationType === RelationType.ONE_TO_MANY) { - continue; - } - } - - const objectMetadata = - objectMetadataMap[createdFieldMetadata.objectMetadataId]; - - if (!isDefined(objectMetadata)) { - throw new FieldMetadataException( - 'Object metadata does not exist', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - migrationActions.push({ - name: computeObjectTargetTable(objectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: workspaceMigrationFactory.createColumnActions( - WorkspaceMigrationColumnActionType.CREATE, - createdFieldMetadata, - ), - }); - } - - return migrationActions; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util.ts index 91cf05658ec..a6fe4a32970 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util.ts @@ -1,7 +1,8 @@ import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export const shouldExcludeFieldFromAgentToolSchema = ( - field: FieldMetadataEntity, + field: FieldMetadataEntity | FlatFieldMetadata, excludeId = true, ): boolean => { const excludedFieldNames = [ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-for-object.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util.ts similarity index 61% rename from packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-for-object.util.ts rename to packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util.ts index 9962cdde57e..a9458cbad50 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-for-object.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util.ts @@ -1,31 +1,28 @@ -import { isDefined } from 'twenty-shared/utils'; - +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export type FieldMapsForObject = { fieldIdByName: Record; fieldIdByJoinColumnName: Record; }; -export const buildFieldMapsForObject = ( +export const buildFieldMapsFromFlatObjectMetadata = ( flatFieldMetadataMaps: FlatEntityMaps, - objectMetadataId: string, + flatObjectMetadata: FlatObjectMetadata, ): FieldMapsForObject => { const fieldIdByName: Record = {}; const fieldIdByJoinColumnName: Record = {}; - for (const [fieldId, field] of Object.entries(flatFieldMetadataMaps.byId)) { - if (!isDefined(field)) { - continue; - } + const objectFields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, + ); - if (field.objectMetadataId !== objectMetadataId) { - continue; - } - - fieldIdByName[field.name] = fieldId; + for (const field of objectFields) { + fieldIdByName[field.name] = field.id; if (isMorphOrRelationFlatFieldMetadata(field)) { const joinColumnName = ( @@ -33,7 +30,7 @@ export const buildFieldMapsForObject = ( )?.joinColumnName; if (joinColumnName) { - fieldIdByJoinColumnName[joinColumnName] = fieldId; + fieldIdByJoinColumnName[joinColumnName] = field.id; } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util.ts deleted file mode 100644 index fc6dfd7ca92..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { isDefined } from 'twenty-shared/utils'; - -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -import { buildFieldMapsForObject } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-for-object.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'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export const buildObjectMetadataItemWithFieldMaps = ( - flatObjectMetadata: FlatObjectMetadata, - flatFieldMetadataMaps: FlatEntityMaps, - flatIndexMaps: FlatEntityMaps, -): ObjectMetadataItemWithFieldMaps => { - const fieldsById: Record = {}; - - for (const fieldId of flatObjectMetadata.fieldMetadataIds) { - const field = flatFieldMetadataMaps.byId[fieldId]; - - if (isDefined(field)) { - fieldsById[fieldId] = field; - } - } - - const { fieldIdByName, fieldIdByJoinColumnName } = buildFieldMapsForObject( - flatFieldMetadataMaps, - flatObjectMetadata.id, - ); - - const indexMetadatas = flatObjectMetadata.indexMetadataIds - .map((indexId) => flatIndexMaps.byId[indexId]) - .filter(isDefined) - .map((flatIndex) => ({ - ...flatIndex, - indexFieldMetadatas: flatIndex.flatIndexFieldMetadatas, - })); - - return { - ...flatObjectMetadata, - fieldsById, - fieldIdByName, - fieldIdByJoinColumnName, - indexMetadatas, - } as unknown as ObjectMetadataItemWithFieldMaps; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts index 805d3667980..8c3d3dc6009 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts @@ -599,13 +599,8 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService, - ) { - return this.objectMetadataRepository.find({ - relations: [ - 'fields.object', - 'fields', - 'fields.relationTargetObjectMetadata', - ], + ): Promise { + const objectMetadataEntities = await this.objectMetadataRepository.find({ ...options, where: { ...options?.where, @@ -614,6 +609,27 @@ export class ObjectMetadataServiceV2 extends TypeOrmQueryService objectMetadata.id, + ); + + const { flatObjectMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps'], + }, + ); + + const filteredOutAndSortedFlatObjectMetadataMaps = + findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityIds: objectMetadataIds, + }); + + return filteredOutAndSortedFlatObjectMetadataMaps; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts deleted file mode 100644 index 0ab78ae85f1..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { RelationOnDeleteAction } from 'twenty-shared/types'; - -import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { - WorkspaceMigrationColumnActionType, - type WorkspaceMigrationColumnCreate, - type WorkspaceMigrationTableAction, - WorkspaceMigrationTableActionType, -} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; - -export const buildMigrationsForCustomObjectRelations = ( - createdObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'nameSingular' | 'isCustom' - >, - relatedObjectMetadataCollection: Pick< - ObjectMetadataItemWithFieldMaps, - 'nameSingular' | 'isCustom' - >[], -): WorkspaceMigrationTableAction[] => { - const migrations: WorkspaceMigrationTableAction[] = []; - - for (const relatedObjectMetadata of relatedObjectMetadataCollection) { - migrations.push( - { - name: computeObjectTargetTable(relatedObjectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.CREATE, - // TODO: When we get rid of this and use the sync metadata, columnName must be based on the joinColumnName from the field metadata settings - columnName: computeColumnName(createdObjectMetadata.nameSingular, { - isForeignKey: true, - }), - columnType: 'uuid', - isNullable: true, - defaultValue: null, - } satisfies WorkspaceMigrationColumnCreate, - ], - }, - { - name: computeObjectTargetTable(relatedObjectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY, - // TODO: When we get rid of this and use the sync metadata, columnName must be based on the joinColumnName from the field metadata settings - columnName: computeColumnName(createdObjectMetadata.nameSingular, { - isForeignKey: true, - }), - referencedTableName: computeObjectTargetTable( - createdObjectMetadata, - ), - referencedTableColumnName: 'id', - onDelete: RelationOnDeleteAction.CASCADE, - }, - ], - }, - ); - } - - return migrations; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts index df02f811a3e..9165e191df2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts @@ -14,9 +14,10 @@ import { } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; -import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input'; import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; import { @@ -25,7 +26,6 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; @Injectable() @@ -70,18 +70,6 @@ export class FieldPermissionService { }, ); - const objectMetadataMapsById: ObjectMetadataMaps['byId'] = {}; - - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMapsById[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - createEmptyFlatEntityMaps(), - ); - } - } - const existingFieldPermissions = await this.fieldPermissionsRepository.find( { where: { @@ -97,7 +85,8 @@ export class FieldPermissionService { this.validateFieldPermission({ allFieldPermissions: input.fieldPermissions, fieldPermission, - objectMetadataMapsById, + flatObjectMetadataMaps, + flatFieldMetadataMaps, rolesPermissions, role, }); @@ -193,13 +182,15 @@ export class FieldPermissionService { private validateFieldPermission({ allFieldPermissions, fieldPermission, - objectMetadataMapsById, + flatObjectMetadataMaps, + flatFieldMetadataMaps, rolesPermissions, role, }: { allFieldPermissions: UpsertFieldPermissionsInput['fieldPermissions']; fieldPermission: UpsertFieldPermissionsInput['fieldPermissions'][0]; - objectMetadataMapsById: ObjectMetadataMaps['byId']; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; rolesPermissions: ObjectsPermissionsByRoleId; role: RoleEntity; }) { @@ -230,10 +221,10 @@ export class FieldPermissionService { ); } - const objectMetadataForFieldPermission = - objectMetadataMapsById[fieldPermission.objectMetadataId]; + const flatObjectMetadata = + flatObjectMetadataMaps.byId[fieldPermission.objectMetadataId]; - if (!isDefined(objectMetadataForFieldPermission)) { + if (!isDefined(flatObjectMetadata)) { throw new PermissionsException( PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND, PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND, @@ -243,7 +234,7 @@ export class FieldPermissionService { ); } - if (objectMetadataForFieldPermission.isSystem === true) { + if (flatObjectMetadata.isSystem === true) { throw new PermissionsException( PermissionsExceptionMessage.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT, PermissionsExceptionCode.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT, @@ -253,12 +244,10 @@ export class FieldPermissionService { ); } - const fieldMetadataForFieldPermission = - objectMetadataForFieldPermission.fieldsById[ - fieldPermission.fieldMetadataId - ]; + const flatFieldMetadata = + flatFieldMetadataMaps.byId[fieldPermission.fieldMetadataId]; - if (!isDefined(fieldMetadataForFieldPermission)) { + if (!isDefined(flatFieldMetadata)) { throw new PermissionsException( PermissionsExceptionMessage.FIELD_METADATA_NOT_FOUND, PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND, diff --git a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts deleted file mode 100644 index 366ddc2ef73..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-item-with-field-maps.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { type IndexMetadataInterface } from 'src/engine/metadata-modules/index-metadata/interfaces/index-metadata.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; - -export type ObjectMetadataItemWithFieldMaps = Omit< - ObjectMetadataEntity, - 'fields' -> & { - fieldsById: FieldMetadataMap; - fieldIdByJoinColumnName: Record; - fieldIdByName: Record; - indexMetadatas: IndexMetadataInterface[]; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts b/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts deleted file mode 100644 index da74069001d..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/types/object-metadata-maps.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export type ObjectMetadataMaps = { - byId: Partial>; - idByNameSingular: Partial>; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/get-object-metadata-map-item-by-name-singular.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/get-object-metadata-map-item-by-name-singular.util.spec.ts deleted file mode 100644 index 9eb51ef8324..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/get-object-metadata-map-item-by-name-singular.util.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; - -describe('getObjectMetadataMapItemByNameSingular', () => { - it('should return the correct metadata item when given a valid singular name', () => { - const mockMetadataItem = { - id: 'test-id', - nameSingular: 'company', - } as ObjectMetadataItemWithFieldMaps; - - const mockObjectMetadataMaps: ObjectMetadataMaps = { - byId: { - 'test-id': mockMetadataItem, - }, - idByNameSingular: { - company: 'test-id', - }, - }; - - const result = getObjectMetadataMapItemByNameSingular( - mockObjectMetadataMaps, - 'company', - ); - - expect(result).toBe(mockMetadataItem); - }); - - it('should return undefined when the singular name does not exist', () => { - const mockObjectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular: {}, - }; - - const result = getObjectMetadataMapItemByNameSingular( - mockObjectMetadataMaps, - 'nonexistent', - ); - - expect(result).toBeUndefined(); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts index 5cb9a490093..99f43d24c73 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/__tests__/validate-field-name-availability.spec.ts @@ -1,15 +1,113 @@ +import { FieldMetadataType } from 'twenty-shared/types'; import { type EachTestingContext } from 'twenty-shared/testing'; -import { - FIELD_ACTOR_MOCK_NAME, - FIELD_ADDRESS_MOCK_NAME, - FIELD_CURRENCY_MOCK_NAME, - FIELD_FULL_NAME_MOCK_NAME, - FIELD_LINKS_MOCK_NAME, - objectMetadataMapItemMock, -} from 'src/engine/api/__mocks__/object-metadata-item.mock'; +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; +import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; +const FIELD_LINKS_MOCK_NAME = 'fieldLinks'; +const FIELD_CURRENCY_MOCK_NAME = 'fieldCurrency'; +const FIELD_ADDRESS_MOCK_NAME = 'fieldAddress'; +const FIELD_ACTOR_MOCK_NAME = 'fieldActor'; +const FIELD_FULL_NAME_MOCK_NAME = 'fieldFullName'; + +const objectMetadataId = '20202020-0000-0000-0000-000000000001'; +const workspaceId = '20202020-0000-0000-0000-000000000000'; + +const createFlatFieldMetadata = ( + id: string, + name: string, + type: FieldMetadataType, +): FlatFieldMetadata => { + return getFlatFieldMetadataMock({ + id, + name, + type, + objectMetadataId, + universalIdentifier: id, + workspaceId, + }); +}; + +const fieldLinksMock = createFlatFieldMetadata( + 'fieldLinksId', + FIELD_LINKS_MOCK_NAME, + FieldMetadataType.LINKS, +); +const fieldCurrencyMock = createFlatFieldMetadata( + 'fieldCurrencyId', + FIELD_CURRENCY_MOCK_NAME, + FieldMetadataType.CURRENCY, +); +const fieldFullNameMock = createFlatFieldMetadata( + 'fieldFullNameId', + FIELD_FULL_NAME_MOCK_NAME, + FieldMetadataType.FULL_NAME, +); +const fieldActorMock = createFlatFieldMetadata( + 'fieldActorId', + FIELD_ACTOR_MOCK_NAME, + FieldMetadataType.ACTOR, +); +const fieldAddressMock = createFlatFieldMetadata( + 'fieldAddressId', + FIELD_ADDRESS_MOCK_NAME, + FieldMetadataType.ADDRESS, +); + +const FIELDS_MOCK = [ + fieldLinksMock, + fieldCurrencyMock, + fieldFullNameMock, + fieldActorMock, + fieldAddressMock, +]; + +const flatFieldMetadataMaps: FlatEntityMaps = + FIELDS_MOCK.reduce( + (acc, field) => ({ + ...acc, + byId: { + ...acc.byId, + [field.id]: field, + }, + }), + createEmptyFlatEntityMaps() as FlatEntityMaps, + ); + +const flatObjectMetadata = getFlatObjectMetadataMock({ + id: objectMetadataId, + workspaceId, + nameSingular: 'objectName', + namePlural: 'objectNames', + labelSingular: 'Object Name', + labelPlural: 'Object Names', + description: 'Object description', + icon: 'Icon123', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + fieldMetadataIds: FIELDS_MOCK.map((f) => f.id), + indexMetadataIds: [], + viewIds: [], + universalIdentifier: objectMetadataId, + applicationId: null, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + shortcut: null, + isLabelSyncedWithName: true, + standardId: null, + standardOverrides: null, + targetTableName: 'DEPRECATED', + duplicateCriteria: null, +}); + type ValidateFieldNameAvailabilityTestContext = EachTestingContext<{ input: string; shouldNotThrow?: true; @@ -64,14 +162,16 @@ describe('validateFieldNameAvailabilityOrThrow', () => { expect(() => validateFieldNameAvailabilityOrThrow({ name: input, - fieldMetadataMapById: objectMetadataMapItemMock.fieldsById, + flatObjectMetadata, + flatFieldMetadataMaps, }), ).not.toThrow(); } else { expect(() => validateFieldNameAvailabilityOrThrow({ name: input, - fieldMetadataMapById: objectMetadataMapItemMock.fieldsById, + flatObjectMetadata, + flatFieldMetadataMaps, }), ).toThrowErrorMatchingSnapshot(); } diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts deleted file mode 100644 index bae07ae768f..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts +++ /dev/null @@ -1,60 +0,0 @@ -import omit from 'lodash.omit'; -import { FieldMetadataType } from 'twenty-shared/types'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; - -export const generateObjectMetadataMaps = ( - objectMetadataCollection: ObjectMetadataEntity[], -): ObjectMetadataMaps => { - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular: {}, - }; - - for (const objectMetadata of objectMetadataCollection) { - const fieldIdByJoinColumnNameMap: Record = {}; - - for (const fieldMetadata of objectMetadata.fields) { - if ( - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.RELATION, - ) || - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.MORPH_RELATION, - ) - ) { - if (fieldMetadata.settings?.joinColumnName) { - fieldIdByJoinColumnNameMap[fieldMetadata.settings.joinColumnName] = - fieldMetadata.id; - } - } - } - - const fieldsByIdMap = objectMetadata.fields.reduce((acc, field) => { - acc[field.id] = field; - - return acc; - }, {} as FieldMetadataMap); - - const processedObjectMetadata: ObjectMetadataItemWithFieldMaps = { - ...omit(objectMetadata, 'fields'), - fieldsById: fieldsByIdMap, - fieldIdByName: Object.fromEntries( - Object.entries(fieldsByIdMap).map(([id, field]) => [field.name, id]), - ), - fieldIdByJoinColumnName: fieldIdByJoinColumnNameMap, - }; - - objectMetadataMaps.byId[objectMetadata.id] = processedObjectMetadata; - objectMetadataMaps.idByNameSingular[objectMetadata.nameSingular] = - objectMetadata.id; - } - - return objectMetadataMaps; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-entity-from-object-metadata-item-with-fields-map.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-entity-from-object-metadata-item-with-fields-map.util.ts deleted file mode 100644 index a739afe8592..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-entity-from-object-metadata-item-with-fields-map.util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import omit from 'lodash.omit'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export const getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap = ( - objectMetadataItem: ObjectMetadataItemWithFieldMaps, -): ObjectMetadataEntity => { - return { - ...omit(objectMetadataItem, [ - 'fieldsById', - 'fieldIdByName', - 'fieldIdByJoinColumnName', - ]), - fields: Object.values(objectMetadataItem.fieldsById), - }; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps.ts b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps.ts deleted file mode 100644 index 64df5d8bc8e..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-from-object-metadata-Item-with-field-maps.ts +++ /dev/null @@ -1,17 +0,0 @@ -import omit from 'lodash.omit'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; - -export const getObjectMetadataFromObjectMetadataItemWithFieldMaps = ( - objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, -): ObjectMetadataEntity => { - return { - ...omit(objectMetadataMapItem, [ - 'fieldsById', - 'fieldIdByName', - 'fieldIdByJoinColumnName', - ]), - fields: Object.values(objectMetadataMapItem.fieldsById), - }; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util.ts deleted file mode 100644 index 38bb9c88ce0..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { isDefined } from 'twenty-shared/utils'; - -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; - -export const getObjectMetadataMapItemByNamePlural = ( - objectMetadataMaps: ObjectMetadataMaps, - namePlural: string, -): ObjectMetadataItemWithFieldMaps | undefined => { - const objectMetadataItems = Object.values(objectMetadataMaps.byId).filter( - isDefined, - ); - - return objectMetadataItems.find( - (objectMetadata) => objectMetadata.namePlural === namePlural, - ); -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util.ts deleted file mode 100644 index 9915b8a4973..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { isNonEmptyString } from '@sniptt/guards'; - -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; - -export const getObjectMetadataMapItemByNameSingular = ( - objectMetadataMaps: ObjectMetadataMaps, - nameSingular: string, -): ObjectMetadataItemWithFieldMaps | undefined => { - const objectMetadataId = objectMetadataMaps.idByNameSingular[nameSingular]; - - if (!isNonEmptyString(objectMetadataId)) { - return undefined; - } - - return objectMetadataMaps.byId[objectMetadataId]; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts index 189df27df38..b56c7783881 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-field-name-availability.utils.ts @@ -6,18 +6,27 @@ import { import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { InvalidMetadataException, InvalidMetadataExceptionCode, } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; const getReservedCompositeFieldNames = ( - fieldMetadataMapById: FieldMetadataMap, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) => { const reservedCompositeFieldsNames: string[] = []; - for (const field of Object.values(fieldMetadataMapById)) { + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const field = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); + if (isCompositeFieldMetadataType(field.type)) { const base = field.name; const compositeType = compositeTypeDefinitions.get(field.type); @@ -35,30 +44,37 @@ const getReservedCompositeFieldNames = ( type ValidateFieldNameAvailabilityOrThrowArgs = { name: string; - fieldMetadataMapById: FieldMetadataMap; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; }; export const validateFieldNameAvailabilityOrThrow = ({ name, - fieldMetadataMapById, + flatObjectMetadata, + flatFieldMetadataMaps, }: ValidateFieldNameAvailabilityOrThrowArgs) => { - const reservedCompositeFieldsNames = - getReservedCompositeFieldNames(fieldMetadataMapById); + const reservedCompositeFieldsNames = getReservedCompositeFieldNames( + flatObjectMetadata, + flatFieldMetadataMaps, + ); - if ( - Object.values(fieldMetadataMapById).some( - (field) => - field.name === name || - (field.type === FieldMetadataType.RELATION && - `${field.name}Id` === name), - ) - ) { - throw new InvalidMetadataException( - `Name "${name}" is not available as it is already used by another field`, - InvalidMetadataExceptionCode.NOT_AVAILABLE, - { - userFriendlyMessage: msg`This name is not available as it is already used by another field.`, - }, - ); + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const field = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); + + if ( + field.name === name || + (field.type === FieldMetadataType.RELATION && `${field.name}Id` === name) + ) { + throw new InvalidMetadataException( + `Name "${name}" is not available as it is already used by another field`, + InvalidMetadataExceptionCode.NOT_AVAILABLE, + { + userFriendlyMessage: msg`This name is not available as it is already used by another field.`, + }, + ); + } } if (reservedCompositeFieldsNames.includes(name)) { diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts index 3182cd61601..b222d2f2911 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util.ts @@ -7,13 +7,12 @@ import { ObjectMetadataException, ObjectMetadataExceptionCode, } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; type ValidateNoOtherObjectWithSameNameExistsOrThrowsParams = { objectMetadataNameSingular: string; objectMetadataNamePlural: string; existingObjectMetadataId?: string; - objectMetadataMaps: ObjectMetadataMaps | FlatEntityMaps; + objectMetadataMaps: FlatEntityMaps; }; export const doesOtherObjectWithSameNameExists = ({ diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type.ts index c153660455e..c1ad1200bb3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/types/relation-connect-query-config.type.ts @@ -1,6 +1,4 @@ -import { type FieldMetadataType } from 'twenty-shared/types'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; export type UniqueFieldCondition = [field: string, value: string]; @@ -11,7 +9,7 @@ export type RelationConnectQueryConfig = { recordToConnectConditions: UniqueConstraintCondition[]; relationFieldName: string; connectFieldName: string; - uniqueConstraintFields: FieldMetadataEntity[]; + uniqueConstraintFields: FlatFieldMetadata[]; recordToConnectConditionByEntityIndex: { [entityIndex: number]: UniqueConstraintCondition; }; diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index 496bb18226c..c9a6f077e92 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -1,11 +1,16 @@ -import { type ObjectsPermissions } from 'twenty-shared/types'; +import { + type FieldMetadataType, + type ObjectsPermissions, +} from 'twenty-shared/types'; import { EntityManager } from 'typeorm'; import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor'; import { PlainObjectToDatabaseEntityTransformer } from 'typeorm/query-builder/transformer/PlainObjectToDatabaseEntityTransformer'; import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { validateOperationIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils'; @@ -79,47 +84,105 @@ describe('WorkspaceEntityManager', () => { }; beforeEach(() => { + const mockFlatObjectMetadata: FlatObjectMetadata = { + id: 'test-entity-id', + nameSingular: 'test-entity', + namePlural: 'test-entities', + labelSingular: 'Test Entity', + labelPlural: 'Test Entities', + workspaceId: 'test-workspace-id', + icon: 'test-icon', + isCustom: false, + isRemote: false, + isAuditLogged: false, + isSearchable: false, + isSystem: false, + isActive: true, + targetTableName: 'test_entity', + fieldMetadataIds: ['field-id'], + indexMetadataIds: [], + viewIds: [], + universalIdentifier: 'test-entity-id', + description: null, + imageIdentifierFieldMetadataId: null, + labelIdentifierFieldMetadataId: null, + shortcut: null, + standardId: null, + standardOverrides: null, + applicationId: null, + isLabelSyncedWithName: false, + isUIReadOnly: false, + duplicateCriteria: null, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const mockFlatFieldMetadata: FlatFieldMetadata = { + id: 'field-id', + type: 'TEXT' as FieldMetadataType, + name: 'fieldName', + label: 'Field Name', + objectMetadataId: 'test-entity-id', + isNullable: true, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'field-id', + defaultValue: null, + description: null, + icon: null, + isActive: true, + isCustom: false, + isSystem: false, + isUIReadOnly: false, + isUnique: false, + options: null, + settings: null, + standardId: null, + standardOverrides: null, + workspaceId: 'test-workspace-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + relationTargetFieldMetadataId: null, + relationTargetObjectMetadataId: null, + morphId: null, + applicationId: null, + }; + + const flatObjectMetadataMaps: FlatEntityMaps = { + byId: { + 'test-entity-id': mockFlatObjectMetadata, + }, + idByUniversalIdentifier: { + 'test-entity-id': 'test-entity-id', + }, + universalIdentifiersByApplicationId: {}, + }; + + const flatFieldMetadataMaps: FlatEntityMaps = { + byId: { + 'field-id': mockFlatFieldMetadata, + }, + idByUniversalIdentifier: { + 'field-id': 'field-id', + }, + universalIdentifiersByApplicationId: {}, + }; + mockInternalContext = { workspaceId: 'test-workspace-id', - objectMetadataMaps: { - byId: { - 'test-entity-id': { - id: 'test-entity-id', - nameSingular: 'test-entity', - namePlural: 'test-entities', - labelSingular: 'Test Entity', - labelPlural: 'Test Entities', - workspaceId: 'test-workspace-id', - icon: 'test-icon', - color: 'test-color', - isCustom: false, - isRemote: false, - isAuditLogged: false, - isSearchable: false, - isSystem: false, - isActive: true, - targetTableName: 'test_entity', - indexMetadatas: [], - fieldsById: { - 'field-id': { - id: 'field-id', - type: 'TEXT', - name: 'fieldName', - label: 'Field Name', - objectMetadataId: 'test-entity-id', - isNullable: true, - isLabelSyncedWithName: false, - createdAt: new Date(), - updatedAt: new Date(), - }, - }, - fieldIdByName: { fieldName: 'field-id' }, - fieldIdByJoinColumnName: {}, - } as unknown as ObjectMetadataItemWithFieldMaps, - }, - idByNameSingular: { - 'test-entity': 'test-entity-id', - }, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps: { + byId: {}, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }, + objectIdByNameSingular: { + 'test-entity': 'test-entity-id', }, featureFlagsMap: { IS_AIRTABLE_INTEGRATION_ENABLED: false, @@ -333,7 +396,9 @@ describe('WorkspaceEntityManager', () => { expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({ entityName: 'test-entity', operationType: 'update', - objectMetadataMaps: mockInternalContext.objectMetadataMaps, + flatObjectMetadataMaps: mockInternalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: mockInternalContext.flatFieldMetadataMaps, + objectIdByNameSingular: mockInternalContext.objectIdByNameSingular, objectsPermissions: mockPermissionOptions.objectRecordsPermissions, selectedColumns: [], allFieldsSelected: false, @@ -366,7 +431,9 @@ describe('WorkspaceEntityManager', () => { expect(validateOperationIsPermittedOrThrow).toHaveBeenCalledWith({ entityName: 'test-entity', operationType: 'delete', - objectMetadataMaps: mockInternalContext.objectMetadataMaps, + flatObjectMetadataMaps: mockInternalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: mockInternalContext.flatFieldMetadataMaps, + objectIdByNameSingular: mockInternalContext.objectIdByNameSingular, objectsPermissions: mockPermissionOptions.objectRecordsPermissions, selectedColumns: [], allFieldsSelected: false, diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts index 0f48536d6b3..36ccb5ccf46 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts @@ -39,11 +39,11 @@ import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/ import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { PermissionsException, PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { type DeepPartialWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/deep-partial-entity-with-nested-relation-fields.type'; import { type QueryDeepPartialEntityWithNestedRelationFields } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; @@ -442,7 +442,9 @@ export class WorkspaceEntityManager extends EntityManager { entityName, operationType, objectsPermissions: permissionOptions?.objectRecordsPermissions ?? {}, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, selectedColumns, allFieldsSelected: false, updatedColumns, @@ -956,7 +958,11 @@ export class WorkspaceEntityManager extends EntityManager { this.internalContext, ); - const formattedEntityLike = formatData(entityLike, objectMetadataItem); + const formattedEntityLike = formatData( + entityLike, + objectMetadataItem, + this.internalContext.flatFieldMetadataMaps, + ); const managerWithPermissionOptions = Object.assign( Object.create(Object.getPrototypeOf(this)), @@ -1190,6 +1196,7 @@ export class WorkspaceEntityManager extends EntityManager { const formattedEntityOrEntities = formatData( entityWithConnectedRelations, objectMetadataItem, + this.internalContext.flatFieldMetadataMaps, ); const updatedColumns = formattedEntityOrEntities @@ -1221,7 +1228,8 @@ export class WorkspaceEntityManager extends EntityManager { let formattedResult = formatResult( resultArray, objectMetadataItem, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); const updatedEntities = formattedResult.filter( @@ -1235,6 +1243,7 @@ export class WorkspaceEntityManager extends EntityManager { formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, objectMetadataItem, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: updatedEntities, beforeEntities: updatedEntities.map( @@ -1247,6 +1256,7 @@ export class WorkspaceEntityManager extends EntityManager { formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.CREATED, objectMetadataItem, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: createdEntities, }), @@ -1283,7 +1293,7 @@ export class WorkspaceEntityManager extends EntityManager { permissionOptionsFromArgs, }: { formattedResult: Entity[]; - objectMetadataItem: ObjectMetadataItemWithFieldMaps; + objectMetadataItem: FlatObjectMetadata; permissionOptionsFromArgs: PermissionOptions | undefined; }): Entity[] { if (permissionOptionsFromArgs?.shouldBypassPermissionChecks === true) { @@ -1303,15 +1313,12 @@ export class WorkspaceEntityManager extends EntityManager { return formattedResult; } - const objectMetadataItemWithFieldMaps = - this.internalContext.objectMetadataMaps.byId[objectMetadataItem.id]; - const restrictedFieldNames = new Set( Object.entries(restrictedFields) .filter(([_, fieldPermissions]) => fieldPermissions.canRead === false) .map(([fieldMetadataId]) => { const fieldMetadata = - objectMetadataItemWithFieldMaps?.fieldsById[fieldMetadataId]; + this.internalContext.flatFieldMetadataMaps.byId[fieldMetadataId]; if (!isDefined(fieldMetadata)) { throw new InternalServerError( @@ -1407,7 +1414,11 @@ export class WorkspaceEntityManager extends EntityManager { this.internalContext, ); - const formattedEntity = formatData(entity, objectMetadataItem); + const formattedEntity = formatData( + entity, + objectMetadataItem, + this.internalContext.flatFieldMetadataMaps, + ); const result = new EntityPersistExecutor( this.connection, @@ -1424,13 +1435,15 @@ export class WorkspaceEntityManager extends EntityManager { const formattedResult = formatResult( result, objectMetadataItem, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.DESTROYED, objectMetadataItem, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedResult, }), @@ -1522,7 +1535,11 @@ export class WorkspaceEntityManager extends EntityManager { this.internalContext, ); - const formattedEntity = formatData(entity, objectMetadataItem); + const formattedEntity = formatData( + entity, + objectMetadataItem, + this.internalContext.flatFieldMetadataMaps, + ); const result = new EntityPersistExecutor( this.connection, @@ -1539,13 +1556,15 @@ export class WorkspaceEntityManager extends EntityManager { const formattedResult = formatResult( result, objectMetadataItem, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.DELETED, objectMetadataItem, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedResult, }), @@ -1633,7 +1652,11 @@ export class WorkspaceEntityManager extends EntityManager { this.internalContext, ); - const formattedEntity = formatData(entity, objectMetadataItem); + const formattedEntity = formatData( + entity, + objectMetadataItem, + this.internalContext.flatFieldMetadataMaps, + ); const result = new EntityPersistExecutor( this.connection, @@ -1650,13 +1673,15 @@ export class WorkspaceEntityManager extends EntityManager { const formattedResult = formatResult( result, objectMetadataItem, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.RESTORED, objectMetadataItem, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedResult, }), diff --git a/packages/twenty-server/src/engine/twenty-orm/error-handling/compute-twenty-orm-exception.ts b/packages/twenty-server/src/engine/twenty-orm/error-handling/compute-twenty-orm-exception.ts index 02682bf91ca..71bc93c269d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/error-handling/compute-twenty-orm-exception.ts +++ b/packages/twenty-server/src/engine/twenty-orm/error-handling/compute-twenty-orm-exception.ts @@ -5,7 +5,7 @@ import { QueryFailedError } from 'typeorm'; import { POSTGRESQL_ERROR_CODES } from 'src/engine/api/graphql/workspace-query-runner/constants/postgres-error-codes.constants'; import { handleDuplicateKeyError } from 'src/engine/api/graphql/workspace-query-runner/utils/handle-duplicate-key-error.util'; import { PostgresException } from 'src/engine/api/graphql/workspace-query-runner/utils/postgres-exception'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMException, TwentyORMExceptionCode, @@ -17,7 +17,7 @@ interface QueryFailedErrorWithCode extends QueryFailedError { export const computeTwentyORMException = ( error: Error, - objectMetadata?: ObjectMetadataItemWithFieldMaps, + objectMetadata?: FlatObjectMetadata, ) => { if (error instanceof QueryFailedError) { if (error.message.includes('Query read timeout')) { diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts index d00ba2a7894..ce47e843da6 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts @@ -9,12 +9,14 @@ import { type ColumnType, type EntitySchemaColumnOptions } from 'typeorm'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { TwentyORMException, @@ -29,12 +31,14 @@ type EntitySchemaColumnMap = { @Injectable() export class EntitySchemaColumnFactory { create( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): EntitySchemaColumnMap { let entitySchemaColumnMap: EntitySchemaColumnMap = {}; - const fieldMetadataCollection = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, + const fieldMetadataCollection = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ); for (const fieldMetadata of fieldMetadataCollection) { @@ -117,7 +121,7 @@ export class EntitySchemaColumnFactory { } private createCompositeColumns( - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ): EntitySchemaColumnMap { const entitySchemaColumnMap: EntitySchemaColumnMap = {}; const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts index 194f6bb3b93..1cbd6e3b216 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { FieldMetadataType } from 'twenty-shared/types'; import { type EntitySchemaRelationOptions } from 'typeorm'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { determineSchemaRelationDetails } from 'src/engine/twenty-orm/utils/determine-schema-relation-details.util'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; type EntitySchemaRelationMap = { [key: string]: EntitySchemaRelationOptions; @@ -16,52 +17,47 @@ type EntitySchemaRelationMap = { export class EntitySchemaRelationFactory { constructor() {} - async create( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, - ): Promise { + create( + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + ): EntitySchemaRelationMap { const entitySchemaRelationMap: EntitySchemaRelationMap = {}; - const fieldMetadataCollection = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, + const flatFieldMetadatas = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ); - for (const fieldMetadata of fieldMetadataCollection) { - const isRelation = - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.RELATION, - ) || - isFieldMetadataEntityOfType( - fieldMetadata, - FieldMetadataType.MORPH_RELATION, - ); - - if (!isRelation) { + for (const flatFieldMetadata of flatFieldMetadatas) { + if (!isMorphOrRelationFlatFieldMetadata(flatFieldMetadata)) { continue; } - if (!fieldMetadata.settings) { + if (!flatFieldMetadata.settings) { throw new Error( - `Field metadata settings are missing for field ${fieldMetadata.name}`, + `Field metadata settings are missing for field ${flatFieldMetadata.name}`, ); } - const schemaRelationDetails = await determineSchemaRelationDetails( - fieldMetadata, - objectMetadataMaps, + const schemaRelationDetails = determineSchemaRelationDetails( + flatFieldMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + flatObjectMetadataMaps.byId[ + flatFieldMetadata.relationTargetObjectMetadataId + ]; if (!targetObjectMetadata) { throw new Error( - `Target object metadata not found for field ${fieldMetadata.name}`, + `Target object metadata not found for field ${flatFieldMetadata.name}`, ); } - entitySchemaRelationMap[fieldMetadata.name] = { + entitySchemaRelationMap[flatFieldMetadata.name] = { type: schemaRelationDetails.relationType, target: schemaRelationDetails.target, inverseSide: schemaRelationDetails.inverseSide, diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts index 540455377a6..064c41fabd1 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema.factory.ts @@ -2,8 +2,9 @@ import { Injectable } from '@nestjs/common'; import { EntitySchema } from 'typeorm'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { EntitySchemaColumnFactory } from 'src/engine/twenty-orm/factories/entity-schema-column.factory'; import { EntitySchemaRelationFactory } from 'src/engine/twenty-orm/factories/entity-schema-relation.factory'; import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspace-entities.storage'; @@ -17,25 +18,30 @@ export class EntitySchemaFactory { private readonly entitySchemaRelationFactory: EntitySchemaRelationFactory, ) {} - async create( + create( workspaceId: string, - objectMetadata: ObjectMetadataItemWithFieldMaps, - objectMetadataMaps: ObjectMetadataMaps, - ): Promise { - const columns = this.entitySchemaColumnFactory.create(objectMetadata); + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + ): EntitySchema { + const columns = this.entitySchemaColumnFactory.create( + flatObjectMetadata, + flatFieldMetadataMaps, + ); - const relations = await this.entitySchemaRelationFactory.create( - objectMetadata, - objectMetadataMaps, + const relations = this.entitySchemaRelationFactory.create( + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); const schemaName = getWorkspaceSchemaName(workspaceId); const entitySchema = new EntitySchema({ - name: objectMetadata.nameSingular, + name: flatObjectMetadata.nameSingular, tableName: computeTableName( - objectMetadata.nameSingular, - objectMetadata.isCustom, + flatObjectMetadata.nameSingular, + flatObjectMetadata.isCustom, ), columns, relations, @@ -44,7 +50,7 @@ export class EntitySchemaFactory { WorkspaceEntitiesStorage.setEntitySchema( workspaceId, - objectMetadata.nameSingular, + flatObjectMetadata.nameSingular, entitySchema, ); diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index b1231623bf7..2692ff1935f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -12,8 +12,6 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service'; import { @@ -144,26 +142,8 @@ export class WorkspaceDatasourceFactory { }, ); - const { idByNameSingular } = buildObjectIdByNameMaps( - flatObjectMetadataMaps, - ); - const cachedObjectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, - }; - - for (const [id, flatObj] of Object.entries( - flatObjectMetadataMaps.byId, - )) { - if (isDefined(flatObj)) { - cachedObjectMetadataMaps.byId[id] = - buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } + const { idByNameSingular: objectIdByNameSingular } = + buildObjectIdByNameMaps(flatObjectMetadataMaps); const metadataVersionForFinalUpToDateCheck = await this.workspaceCacheStorageService.getMetadataVersion( @@ -184,17 +164,16 @@ export class WorkspaceDatasourceFactory { (option) => new EntitySchema(option), ); } else { - const entitySchemas = await Promise.all( - Object.values(cachedObjectMetadataMaps.byId) - .filter(isDefined) - .map((objectMetadata) => - this.entitySchemaFactory.create( - workspaceId, - objectMetadata, - cachedObjectMetadataMaps, - ), + const entitySchemas = Object.values(flatObjectMetadataMaps.byId) + .filter(isDefined) + .map((flatObjectMetadata) => + this.entitySchemaFactory.create( + workspaceId, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ), - ); + ); await this.workspaceCacheStorageService.setORMEntitySchema( workspaceId, @@ -208,7 +187,10 @@ export class WorkspaceDatasourceFactory { const workspaceDataSource = new WorkspaceDataSource( { workspaceId, - objectMetadataMaps: cachedObjectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, + objectIdByNameSingular, featureFlagsMap: cachedFeatureFlagMap, eventEmitterService: this.workspaceEventEmitter, }, diff --git a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts index c6dd77c4d2f..b8854fea89b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts +++ b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.ts @@ -112,7 +112,10 @@ export class GlobalWorkspaceDataSource extends DataSource { return new WorkspaceEntityManager( { workspaceId: context.authContext.workspace.id, - objectMetadataMaps: context.objectMetadataMaps, + flatObjectMetadataMaps: context.flatObjectMetadataMaps, + flatFieldMetadataMaps: context.flatFieldMetadataMaps, + flatIndexMaps: context.flatIndexMaps, + objectIdByNameSingular: context.objectIdByNameSingular, featureFlagsMap: context.featureFlagsMap, eventEmitterService: this.eventEmitterService, }, diff --git a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts index cd4bbbde8c7..b684159a8da 100644 --- a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts @@ -6,9 +6,10 @@ import { EntitySchema, ObjectLiteral } from 'typeorm'; import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; @@ -99,23 +100,8 @@ export class GlobalWorkspaceOrmManager { }, ); - const { idByNameSingular } = buildObjectIdByNameMaps( - flatObjectMetadataMaps, - ); - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, - }; - - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } + const { idByNameSingular: objectIdByNameSingular } = + buildObjectIdByNameMaps(flatObjectMetadataMaps); const metadataVersion = await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); @@ -139,12 +125,16 @@ export class GlobalWorkspaceOrmManager { const entitySchemas = await this.buildEntitySchemas( workspaceId, metadataVersion, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); return { authContext, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, + objectIdByNameSingular, metadataVersion, featureFlagsMap, permissionsPerRoleId, @@ -155,7 +145,8 @@ export class GlobalWorkspaceOrmManager { private async buildEntitySchemas( workspaceId: string, dataSourceMetadataVersion: number, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ) { const cachedEntitySchemaOptions = await this.workspaceCacheStorageService.getORMEntitySchema( @@ -170,17 +161,16 @@ export class GlobalWorkspaceOrmManager { (option) => new EntitySchema(option), ); } else { - const entitySchemas = await Promise.all( - Object.values(objectMetadataMaps.byId) - .filter(isDefined) - .map((objectMetadata) => - this.entitySchemaFactory.create( - workspaceId, - objectMetadata, - objectMetadataMaps, - ), + const entitySchemas = Object.values(flatObjectMetadataMaps.byId) + .filter(isDefined) + .map((flatObjectMetadata) => + this.entitySchemaFactory.create( + workspaceId, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ), - ); + ); await this.workspaceCacheStorageService.setORMEntitySchema( workspaceId, diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts index 1f93da6149a..57a66bdf80f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-internal-context.interface.ts @@ -1,10 +1,16 @@ import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +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'; import { type WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; export interface WorkspaceInternalContext { workspaceId: string; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatIndexMaps: FlatEntityMaps; + objectIdByNameSingular: Record; featureFlagsMap: Record; eventEmitterService: WorkspaceEventEmitter; } diff --git a/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts index f98bb5441eb..da1ef88fdd2 100644 --- a/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts +++ b/packages/twenty-server/src/engine/twenty-orm/relation-nested-queries/relation-nested-queries.ts @@ -72,12 +72,12 @@ export class RelationNestedQueries { this.internalContext, ); - const objectMetadataMap = this.internalContext.objectMetadataMaps; - const relationConnectQueryConfigs = computeRelationConnectQueryConfigs( entities, objectMetadata, - objectMetadataMap, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, + this.internalContext.flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts index 8f4b601d60d..f577735d964 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/__tests__/workspace.repository.spec.ts @@ -1,4 +1,7 @@ -import { type ObjectsPermissions } from 'twenty-shared/types'; +import { + FieldMetadataType, + type ObjectsPermissions, +} from 'twenty-shared/types'; import { type DeepPartial, type FindManyOptions, @@ -12,6 +15,7 @@ import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interf import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; @@ -57,13 +61,64 @@ describe('WorkspaceRepository', () => { clear: jest.fn(), } as unknown as jest.Mocked; + const mockFieldMetadata: FlatFieldMetadata = { + id: 'test-field-id', + name: 'id', + type: FieldMetadataType.UUID, + objectMetadataId: 'test-metadata-id', + isActive: true, + isNullable: false, + isUnique: true, + isSystem: true, + isCustom: false, + isUIReadOnly: false, + isLabelSyncedWithName: false, + label: 'ID', + description: 'Record ID', + icon: 'IconKey', + workspaceId: 'test-workspace-id', + universalIdentifier: 'id-field-universal-id', + createdAt: new Date(), + updatedAt: new Date(), + defaultValue: null, + options: null, + settings: null, + morphId: null, + standardId: null, + standardOverrides: null, + applicationId: null, + relationTargetFieldMetadataId: null, + relationTargetObjectMetadataId: null, + calendarViewIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + viewFieldIds: [], + }; + mockInternalContext = { workspaceId: 'test-workspace-id', - objectMetadataMaps: { - idByNameSingular: {}, + flatObjectMetadataMaps: { + byId: {}, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, }, + flatFieldMetadataMaps: { + byId: { + 'test-field-id': mockFieldMetadata, + }, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }, + flatIndexMaps: { + byId: {}, + idByUniversalIdentifier: {}, + universalIdentifiersByApplicationId: {}, + }, + objectIdByNameSingular: {}, featureFlagsMap: {}, - } as WorkspaceInternalContext; + eventEmitterService: {} as unknown, + } as unknown as WorkspaceInternalContext; mockFeatureFlagMap = Object.values(FeatureFlagKey).reduce( (acc, key) => ({ ...acc, [key]: false }), @@ -97,6 +152,7 @@ describe('WorkspaceRepository', () => { id: 'test-metadata-id', nameSingular: 'test-entity', namePlural: 'test-entities', + fieldMetadataIds: ['test-field-id'], fieldIdByName: { id: 'test-field-id', }, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts index f0390284a50..cfc68558895 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.utils.ts @@ -9,12 +9,14 @@ import { type QueryExpressionMap } from 'typeorm/query-builder/QueryExpressionMa import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { PermissionsException, PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getColumnNameToFieldMetadataIdMap } from 'src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util'; const getTargetEntityAndOperationType = ( @@ -66,7 +68,9 @@ type ValidateOperationIsPermittedOrThrowArgs = { entityName: string; operationType: OperationType; objectsPermissions: ObjectsPermissions; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; selectedColumns: string[] | '*'; allFieldsSelected: boolean; updatedColumns: string[]; @@ -76,13 +80,14 @@ export const validateOperationIsPermittedOrThrow = ({ entityName, operationType, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, selectedColumns, allFieldsSelected, updatedColumns, }: ValidateOperationIsPermittedOrThrowArgs) => { - const objectMetadataIdForEntity = - objectMetadataMaps.idByNameSingular[entityName]; + const objectMetadataIdForEntity = objectIdByNameSingular[entityName]; if (!isNonEmptyString(objectMetadataIdForEntity)) { throw new PermissionsException( @@ -91,7 +96,7 @@ export const validateOperationIsPermittedOrThrow = ({ ); } - const objectMetadata = objectMetadataMaps.byId[objectMetadataIdForEntity]; + const objectMetadata = flatObjectMetadataMaps.byId[objectMetadataIdForEntity]; if (!isDefined(objectMetadata)) { throw new PermissionsException( @@ -106,8 +111,10 @@ export const validateOperationIsPermittedOrThrow = ({ return; } - const columnNameToFieldMetadataIdMap = - getColumnNameToFieldMetadataIdMap(objectMetadata); + const columnNameToFieldMetadataIdMap = getColumnNameToFieldMetadataIdMap( + objectMetadata, + flatFieldMetadataMaps, + ); const permissionsForEntity = objectsPermissions[objectMetadataIdForEntity]; @@ -194,14 +201,18 @@ export const validateOperationIsPermittedOrThrow = ({ type ValidateQueryIsPermittedOrThrowArgs = { expressionMap: QueryExpressionMap; objectsPermissions: ObjectsPermissions; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; shouldBypassPermissionChecks: boolean; }; export const validateQueryIsPermittedOrThrow = ({ expressionMap, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, shouldBypassPermissionChecks, }: ValidateQueryIsPermittedOrThrowArgs) => { if (shouldBypassPermissionChecks) { @@ -222,7 +233,9 @@ export const validateQueryIsPermittedOrThrow = ({ validatePermissionsForJoinsAndReturnSelectsWithoutJoins({ expressionMap, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }); expressionMapSelectsOnMainEntity = selectsWithoutJoinedAliases; @@ -266,7 +279,9 @@ export const validateQueryIsPermittedOrThrow = ({ entityName: mainEntity, operationType: operationType as OperationType, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, selectedColumns, allFieldsSelected, updatedColumns, @@ -276,11 +291,15 @@ export const validateQueryIsPermittedOrThrow = ({ const validatePermissionsForJoinsAndReturnSelectsWithoutJoins = ({ expressionMap, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }: { expressionMap: QueryExpressionMap; objectsPermissions: ObjectsPermissions; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; }) => { const joinAttributesAliases = new Set( expressionMap.joinAttributes.map((join) => join.alias.name), @@ -313,7 +332,9 @@ const validatePermissionsForJoinsAndReturnSelectsWithoutJoins = ({ entityName: entity.name, operationType: 'select' as OperationType, objectsPermissions, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, selectedColumns, allFieldsSelected: false, updatedColumns: [], diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts index 54774d07c03..50545135569 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts @@ -72,7 +72,9 @@ export class WorkspaceDeleteQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); @@ -110,19 +112,22 @@ export class WorkspaceDeleteQueryBuilder< const formattedResult = formatResult( result.raw, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); const formattedBefore = formatResult( before, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.DESTROYED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedBefore, authContext: this.authContext, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts index 2e48cfe8b2a..8281ce618ee 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-insert-query-builder.ts @@ -95,7 +95,11 @@ export class WorkspaceInsertQueryBuilder< this.internalContext, ); - const formattedValues = formatData(values, objectMetadata); + const formattedValues = formatData( + values, + objectMetadata, + this.internalContext.flatFieldMetadataMaps, + ); return super.values(formattedValues); } @@ -105,7 +109,9 @@ export class WorkspaceInsertQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); @@ -159,13 +165,15 @@ export class WorkspaceInsertQueryBuilder< const formattedResultForEvent = formatResult( afterResult, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.CREATED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedResultForEvent, authContext: this.authContext, @@ -176,6 +184,7 @@ export class WorkspaceInsertQueryBuilder< formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPSERTED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedResultForEvent, authContext: this.authContext, @@ -202,7 +211,8 @@ export class WorkspaceInsertQueryBuilder< const formattedResult = formatResult( resultWithoutInsertionExtraColumns, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return { diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts index 975fe0c2264..643bcada01d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts @@ -85,7 +85,8 @@ export class WorkspaceSelectQueryBuilder< const formattedResult = formatResult( result, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return { @@ -114,7 +115,8 @@ export class WorkspaceSelectQueryBuilder< const formattedResult = formatResult( result, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return formattedResult; @@ -163,7 +165,8 @@ export class WorkspaceSelectQueryBuilder< const formattedResult = formatResult( result, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return formattedResult; @@ -188,7 +191,8 @@ export class WorkspaceSelectQueryBuilder< const formattedResult = formatResult( result, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return formattedResult[0]; @@ -230,7 +234,8 @@ export class WorkspaceSelectQueryBuilder< const formattedResult = formatResult( result, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return [formattedResult, count]; @@ -325,7 +330,9 @@ export class WorkspaceSelectQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); } diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts index 73ffe7c4393..28e911ca304 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-soft-delete-query-builder.ts @@ -72,7 +72,9 @@ export class WorkspaceSoftDeleteQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); @@ -110,19 +112,22 @@ export class WorkspaceSoftDeleteQueryBuilder< const formattedAfter = formatResult( after.raw, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); const formattedBefore = formatResult( before, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.DELETED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedBefore, authContext: this.authContext, diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts index e87e1fd1f1a..e4a83908df9 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts @@ -95,7 +95,9 @@ export class WorkspaceUpdateQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: this.expressionMap, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); @@ -164,7 +166,8 @@ export class WorkspaceUpdateQueryBuilder< const formattedBefore = formatResult( before, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); const result = await super.execute(); @@ -174,13 +177,15 @@ export class WorkspaceUpdateQueryBuilder< const formattedAfter = formatResult( after, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedAfter, beforeEntities: formattedBefore, @@ -192,6 +197,7 @@ export class WorkspaceUpdateQueryBuilder< formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPSERTED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedAfter, beforeEntities: formattedBefore, @@ -202,7 +208,8 @@ export class WorkspaceUpdateQueryBuilder< const formattedResult = formatResult( result.raw, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return { @@ -235,7 +242,9 @@ export class WorkspaceUpdateQueryBuilder< validateQueryIsPermittedOrThrow({ expressionMap: fakeExpressionMapToValidatePermissions, objectsPermissions: this.objectRecordsPermissions, - objectMetadataMaps: this.internalContext.objectMetadataMaps, + flatObjectMetadataMaps: this.internalContext.flatObjectMetadataMaps, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, + objectIdByNameSingular: this.internalContext.objectIdByNameSingular, shouldBypassPermissionChecks: this.shouldBypassPermissionChecks, }); } @@ -265,7 +274,8 @@ export class WorkspaceUpdateQueryBuilder< const formattedBefore = formatResult( beforeRecords, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); const results: UpdateResult[] = []; @@ -317,13 +327,15 @@ export class WorkspaceUpdateQueryBuilder< const formattedAfter = formatResult( afterRecords, objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); this.internalContext.eventEmitterService.emitDatabaseBatchEvent( formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedAfter, beforeEntities: formattedBefore, @@ -335,6 +347,7 @@ export class WorkspaceUpdateQueryBuilder< formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPSERTED, objectMetadataItem: objectMetadata, + flatFieldMetadataMaps: this.internalContext.flatFieldMetadataMaps, workspaceId: this.internalContext.workspaceId, entities: formattedAfter, beforeEntities: formattedBefore, @@ -345,7 +358,8 @@ export class WorkspaceUpdateQueryBuilder< const formattedResults = formatResult( results.flatMap((result) => result.raw), objectMetadata, - this.internalContext.objectMetadataMaps, + this.internalContext.flatObjectMetadataMaps, + this.internalContext.flatFieldMetadataMaps, ); return { @@ -395,7 +409,11 @@ export class WorkspaceUpdateQueryBuilder< mainAliasTarget, ); - const formattedUpdateSet = formatData(_values, objectMetadata); + const formattedUpdateSet = formatData( + _values, + objectMetadata, + this.internalContext.flatFieldMetadataMaps, + ); return super.set(formattedUpdateSet as QueryDeepPartialEntity); } @@ -456,7 +474,11 @@ export class WorkspaceUpdateQueryBuilder< this.manyInputs = inputs.map((input) => ({ criteria: input.criteria, - partialEntity: formatData(input.partialEntity, objectMetadata), + partialEntity: formatData( + input.partialEntity, + objectMetadata, + this.internalContext.flatFieldMetadataMaps, + ), })); return this; diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index 25cc5064b68..508fe747daa 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -37,13 +37,17 @@ import { getObjectMetadataFromEntityTarget } from 'src/engine/twenty-orm/utils/g export class WorkspaceRepository< T extends ObjectLiteral, > extends Repository { - private readonly internalContext: WorkspaceInternalContext; + private readonly _internalContext: WorkspaceInternalContext; private shouldBypassPermissionChecks: boolean; private featureFlagMap: FeatureFlagMap; public readonly objectRecordsPermissions?: ObjectsPermissions; private authContext?: AuthContext; declare manager: WorkspaceEntityManager; + get internalContext(): WorkspaceInternalContext { + return this._internalContext; + } + constructor( internalContext: WorkspaceInternalContext, target: EntityTarget, @@ -55,7 +59,7 @@ export class WorkspaceRepository< authContext?: AuthContext, ) { super(target, manager, queryRunner); - this.internalContext = internalContext; + this._internalContext = internalContext; this.featureFlagMap = featureFlagMap; this.objectRecordsPermissions = objectRecordsPermissions; this.shouldBypassPermissionChecks = shouldBypassPermissionChecks; @@ -79,7 +83,7 @@ export class WorkspaceRepository< return new WorkspaceSelectQueryBuilder( queryBuilder, this.objectRecordsPermissions, - this.internalContext, + this._internalContext, this.shouldBypassPermissionChecks, this.authContext ?? {}, this.featureFlagMap, @@ -944,7 +948,10 @@ export class WorkspaceRepository< * PRIVATE METHODS */ private async getObjectMetadataFromTarget() { - return getObjectMetadataFromEntityTarget(this.target, this.internalContext); + return getObjectMetadataFromEntityTarget( + this.target, + this._internalContext, + ); } private async transformOptions< @@ -968,6 +975,10 @@ export class WorkspaceRepository< private async formatData(data: T): Promise { const objectMetadata = await this.getObjectMetadataFromTarget(); - return formatData(data, objectMetadata) as T; + return formatData( + data, + objectMetadata, + this._internalContext.flatFieldMetadataMaps, + ) as T; } } diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/workspace-context.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/workspace-context.storage.ts index 97ed0d55caf..fa71149d6f3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/storage/workspace-context.storage.ts +++ b/packages/twenty-server/src/engine/twenty-orm/storage/workspace-context.storage.ts @@ -6,11 +6,17 @@ import { type EntitySchema } from 'typeorm'; import { type WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface'; import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +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'; export type WorkspaceContext = { authContext: WorkspaceAuthContext; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + flatIndexMaps: FlatEntityMaps; + objectIdByNameSingular: Record; metadataVersion: number; featureFlagsMap: Record; permissionsPerRoleId: ObjectsPermissionsByRoleId; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts index 198a2bef9df..0de37cf278a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/compute-relation-connect-query-configs.util.spec.ts @@ -2,147 +2,359 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +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'; import { computeRelationConnectQueryConfigs } from 'src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util'; describe('computeRelationConnectQueryConfigs', () => { - const personMetadata = { + const personFields: FlatFieldMetadata[] = [ + { + id: 'person-id-field-id', + name: 'id', + type: FieldMetadataType.UUID, + label: 'id', + objectMetadataId: 'person-object-metadata-id', + isNullable: false, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'person-id-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'person-name-field-id', + name: 'name', + type: FieldMetadataType.FULL_NAME, + label: 'name', + objectMetadataId: 'person-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'person-name-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'person-company-1-field-id', + name: 'company-related-to-1', + type: FieldMetadataType.RELATION, + label: 'company-related-to-1', + objectMetadataId: 'person-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'person-company-1-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + relationTargetObjectMetadataId: 'company-object-metadata-id', + relationTargetFieldMetadataId: 'company-id-field-id', + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: 'company-related-to-1Id', + }, + } as unknown as FlatFieldMetadata, + { + id: 'person-company-2-field-id', + name: 'company-related-to-2', + type: FieldMetadataType.RELATION, + label: 'company-related-to-2', + objectMetadataId: 'person-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'person-company-2-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + relationTargetObjectMetadataId: 'company-object-metadata-id', + relationTargetFieldMetadataId: 'company-id-field-id', + settings: { + relationType: RelationType.MANY_TO_ONE, + joinColumnName: 'company-related-to-2Id', + }, + } as unknown as FlatFieldMetadata, + ]; + + const companyFields: FlatFieldMetadata[] = [ + { + id: 'company-id-field-id', + name: 'id', + type: FieldMetadataType.UUID, + label: 'id', + objectMetadataId: 'company-object-metadata-id', + isNullable: false, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'company-id-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'company-name-field-id', + name: 'name', + type: FieldMetadataType.TEXT, + label: 'name', + objectMetadataId: 'company-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'company-name-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'company-description-field-id', + name: 'description', + type: FieldMetadataType.TEXT, + label: 'description', + objectMetadataId: 'company-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'company-description-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'company-domain-name-field-id', + name: 'domainName', + type: FieldMetadataType.LINKS, + label: 'domainName', + objectMetadataId: 'company-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'company-domain-name-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + { + id: 'company-address-field-id', + name: 'address', + type: FieldMetadataType.TEXT, + label: 'address', + objectMetadataId: 'company-object-metadata-id', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: 'company-address-field-id', + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + } as unknown as FlatFieldMetadata, + ]; + + const allFields = [...personFields, ...companyFields]; + + const flatFieldMetadataMaps: FlatEntityMaps = { + byId: allFields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: allFields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, + }; + + const createFlatObjectMetadata = ( + partial: Partial & { + id: string; + nameSingular: string; + fieldMetadataIds: string[]; + indexMetadataIds: string[]; + }, + ): FlatObjectMetadata => + ({ + namePlural: `${partial.nameSingular}s`, + labelSingular: partial.nameSingular, + labelPlural: `${partial.nameSingular}s`, + icon: 'Icon', + targetTableName: partial.nameSingular, + workspaceId: 'workspace-id', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, + universalIdentifier: partial.id, + viewIds: [], + applicationId: null, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + shortcut: null, + description: null, + standardOverrides: null, + isUIReadOnly: false, + standardId: null, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + duplicateCriteria: null, + ...partial, + }) as FlatObjectMetadata; + + const personMetadata = createFlatObjectMetadata({ id: 'person-object-metadata-id', nameSingular: 'person', - fieldsById: { - 'person-id-field-id': { - id: 'person-id-field-id', - name: 'id', - type: FieldMetadataType.UUID, - label: 'id', - }, - 'person-name-field-id': { - id: 'person-name-field-id', - name: 'name', - type: FieldMetadataType.FULL_NAME, - label: 'name', - }, - 'person-company-1-field-id': { - id: 'person-company-1-field-id', - name: 'company-related-to-1', - type: FieldMetadataType.RELATION, - label: 'company-related-to-1', - relationTargetObjectMetadataId: 'company-object-metadata-id', - relationTargetFieldMetadataId: 'company-id-field-id', - settings: { - relationType: RelationType.MANY_TO_ONE, - }, - }, - 'person-company-2-field-id': { - id: 'person-company-2-field-id', - name: 'company-related-to-2', - type: FieldMetadataType.RELATION, - label: 'company-related-to-2', - relationTargetObjectMetadataId: 'company-object-metadata-id', - relationTargetFieldMetadataId: 'company-id-field-id', - settings: { - relationType: RelationType.MANY_TO_ONE, - }, - }, - }, - fieldIdByName: { - id: 'person-id-field-id', - name: 'person-name-field-id', - 'company-related-to-1': 'person-company-1-field-id', - 'company-related-to-2': 'person-company-2-field-id', - }, - } as unknown as ObjectMetadataItemWithFieldMaps; + indexMetadataIds: [], + fieldMetadataIds: personFields.map((f) => f.id), + }); - const companyMetadata = { + const companyMetadata = createFlatObjectMetadata({ id: 'company-object-metadata-id', nameSingular: 'company', - indexMetadatas: [ - { - id: 'company-id-index-metadata-id', - name: 'company-id-index-metadata-name', - indexFieldMetadatas: [ - { - fieldMetadataId: 'company-id-field-id', - }, - ], - isUnique: true, - }, - { - id: 'company-domain-index-metadata-id', - name: 'company-domain-index-metadata-name', - indexFieldMetadatas: [ - { - fieldMetadataId: 'company-domain-name-field-id', - }, - ], - isUnique: true, - }, - { - id: 'company-composite-index-metadata-id', - name: 'company-composite-index-metadata-name', - indexFieldMetadatas: [ - { - fieldMetadataId: 'company-name-field-id', - }, - { - fieldMetadataId: 'company-description-field-id', - }, - ], - isUnique: true, - }, + indexMetadataIds: [ + 'company-id-index-metadata-id', + 'company-domain-index-metadata-id', + 'company-composite-index-metadata-id', ], - fieldsById: { - 'company-id-field-id': { - id: 'company-id-field-id', - name: 'id', - type: FieldMetadataType.UUID, - label: 'id', - }, - 'company-name-field-id': { - id: 'company-name-field-id', - name: 'name', - type: FieldMetadataType.TEXT, - label: 'name', - }, - 'company-description-field-id': { - id: 'company-description-field-id', - name: 'description', - type: FieldMetadataType.TEXT, - label: 'description', - }, - 'company-domain-name-field-id': { - id: 'company-domain-name-field-id', - name: 'domainName', - type: FieldMetadataType.LINKS, - label: 'domainName', - }, - 'company-address-field-id': { - id: 'company-address-field-id', - name: 'address', - type: FieldMetadataType.TEXT, - label: 'address', - }, - }, - fieldIdByName: { - id: 'company-id-field-id', - name: 'company-name-field-id', - description: 'company-description-field-id', - domainName: 'company-domain-name-field-id', - address: 'company-address-field-id', - }, - } as unknown as ObjectMetadataItemWithFieldMaps; + fieldMetadataIds: companyFields.map((f) => f.id), + }); - const objectMetadataMaps = { + const flatObjectMetadataMaps: FlatEntityMaps = { byId: { 'person-object-metadata-id': personMetadata, 'company-object-metadata-id': companyMetadata, }, - idByNameSingular: { - person: 'person-object-metadata-id', - company: 'company-object-metadata-id', + idByUniversalIdentifier: { + 'person-object-metadata-id': 'person-object-metadata-id', + 'company-object-metadata-id': 'company-object-metadata-id', }, - } as ObjectMetadataMaps; + universalIdentifiersByApplicationId: {}, + }; + + const createFlatIndexFieldMetadata = ( + id: string, + fieldMetadataId: string, + indexMetadataId: string, + order: number, + ) => ({ + id, + fieldMetadataId, + indexMetadataId, + order, + createdAt: new Date(), + updatedAt: new Date(), + }); + + const flatIndexMaps: FlatEntityMaps = { + byId: { + 'company-id-index-metadata-id': { + id: 'company-id-index-metadata-id', + name: 'company-id-index-metadata-name', + isUnique: true, + objectMetadataId: 'company-object-metadata-id', + universalIdentifier: 'company-id-index-metadata-id', + flatIndexFieldMetadatas: [ + createFlatIndexFieldMetadata( + 'company-id-index-field-metadata-id', + 'company-id-field-id', + 'company-id-index-metadata-id', + 0, + ), + ], + } as unknown as FlatIndexMetadata, + 'company-domain-index-metadata-id': { + id: 'company-domain-index-metadata-id', + name: 'company-domain-index-metadata-name', + isUnique: true, + objectMetadataId: 'company-object-metadata-id', + universalIdentifier: 'company-domain-index-metadata-id', + flatIndexFieldMetadatas: [ + createFlatIndexFieldMetadata( + 'company-domain-index-field-metadata-id', + 'company-domain-name-field-id', + 'company-domain-index-metadata-id', + 0, + ), + ], + } as unknown as FlatIndexMetadata, + 'company-composite-index-metadata-id': { + id: 'company-composite-index-metadata-id', + name: 'company-composite-index-metadata-name', + isUnique: true, + objectMetadataId: 'company-object-metadata-id', + universalIdentifier: 'company-composite-index-metadata-id', + flatIndexFieldMetadatas: [ + createFlatIndexFieldMetadata( + 'company-name-index-field-metadata-id', + 'company-name-field-id', + 'company-composite-index-metadata-id', + 0, + ), + createFlatIndexFieldMetadata( + 'company-description-index-field-metadata-id', + 'company-description-field-id', + 'company-composite-index-metadata-id', + 1, + ), + ], + } as unknown as FlatIndexMetadata, + }, + idByUniversalIdentifier: { + 'company-id-index-metadata-id': 'company-id-index-metadata-id', + 'company-domain-index-metadata-id': 'company-domain-index-metadata-id', + 'company-composite-index-metadata-id': + 'company-composite-index-metadata-id', + }, + universalIdentifiersByApplicationId: {}, + }; it('should return an empty object if no connect fields are found', () => { const peopleEntityInputs = [ @@ -159,7 +371,9 @@ describe('computeRelationConnectQueryConfigs', () => { const result = computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, {}, ); @@ -187,7 +401,9 @@ describe('computeRelationConnectQueryConfigs', () => { computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); }).toThrow('Connect is not allowed for name on person'); @@ -213,7 +429,9 @@ describe('computeRelationConnectQueryConfigs', () => { computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); }).toThrow( @@ -255,7 +473,9 @@ describe('computeRelationConnectQueryConfigs', () => { computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); }).toThrow( @@ -304,7 +524,9 @@ describe('computeRelationConnectQueryConfigs', () => { computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); }).toThrow( @@ -374,7 +596,9 @@ describe('computeRelationConnectQueryConfigs', () => { const result = computeRelationConnectQueryConfigs( peopleEntityInputs, personMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, relationConnectQueryFieldsByEntityIndex, ); @@ -392,12 +616,11 @@ describe('computeRelationConnectQueryConfigs', () => { relationFieldName: 'company-related-to-1Id', targetObjectName: 'company', uniqueConstraintFields: [ - { + expect.objectContaining({ id: 'company-domain-name-field-id', - label: 'domainName', name: 'domainName', type: FieldMetadataType.LINKS, - }, + }), ], }, { @@ -410,12 +633,11 @@ describe('computeRelationConnectQueryConfigs', () => { relationFieldName: 'company-related-to-2Id', targetObjectName: 'company', uniqueConstraintFields: [ - { + expect.objectContaining({ id: 'company-id-field-id', - label: 'id', name: 'id', type: FieldMetadataType.UUID, - }, + }), ], }, ]); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts index 485b89cf19f..5d805624045 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/format-twenty-orm-event-to-database-batch-event.util.spec.ts @@ -2,43 +2,80 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMException, TwentyORMExceptionCode, } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; import { formatTwentyOrmEventToDatabaseBatchEvent } from 'src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; describe('formatTwentyOrmEventToDatabaseBatchEvent', () => { - const objectMetadataItemWithFieldMaps = - getMockObjectMetadataItemWithFieldsMaps({ - id: 'object-id', - workspaceId: 'workspace-id', - nameSingular: 'person', - namePlural: 'people', - indexMetadatas: [], - fieldIdByJoinColumnName: {}, - fieldIdByName: { - name: 'name-id', - age: 'age-id', - fullName: 'fullname-id', - }, - fieldsById: { - 'name-id': getMockFieldMetadataEntity({ - workspaceId: 'workspace-id', - objectMetadataId: 'object-id', - id: 'name-id', - type: FieldMetadataType.TEXT, - name: 'name', - label: 'Name', - isLabelSyncedWithName: true, - isNullable: true, - createdAt: new Date(), - updatedAt: new Date(), - }), - }, - }); + const workspaceId = 'workspace-id'; + const objectMetadataId = 'object-id'; + + const createMockField = ( + overrides: Partial & { + id: string; + name: string; + type: FieldMetadataType; + }, + ): FlatFieldMetadata => + ({ + workspaceId, + objectMetadataId, + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: overrides.id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + label: overrides.name, + ...overrides, + }) as FlatFieldMetadata; + + const nameField = createMockField({ + id: 'name-id', + type: FieldMetadataType.TEXT, + name: 'name', + label: 'Name', + }); + + const flatFieldMetadataMaps: FlatEntityMaps = { + byId: { + 'name-id': nameField, + }, + idByUniversalIdentifier: { + 'name-id': 'name-id', + }, + universalIdentifiersByApplicationId: {}, + }; + + const flatObjectMetadata: FlatObjectMetadata = { + id: objectMetadataId, + workspaceId, + nameSingular: 'person', + namePlural: 'people', + labelSingular: 'Person', + labelPlural: 'People', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: objectMetadataId, + fieldMetadataIds: ['name-id'], + indexMetadataIds: [], + viewIds: [], + applicationId: null, + } as unknown as FlatObjectMetadata; const mockWorkspaceId = 'workspace-id'; const mockAuthContext = { @@ -73,7 +110,8 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => { try { formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: flatObjectMetadata, + flatFieldMetadataMaps, workspaceId: mockWorkspaceId, authContext: mockAuthContext, entities: afterEntities, @@ -115,7 +153,8 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => { const result = formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: flatObjectMetadata, + flatFieldMetadataMaps, workspaceId: mockWorkspaceId, authContext: mockAuthContext, entities: afterEntities, @@ -155,7 +194,8 @@ describe('formatTwentyOrmEventToDatabaseBatchEvent', () => { const result = formatTwentyOrmEventToDatabaseBatchEvent({ action: DatabaseEventAction.UPDATED, - objectMetadataItem: objectMetadataItemWithFieldMaps, + objectMetadataItem: flatObjectMetadata, + flatFieldMetadataMaps, workspaceId: mockWorkspaceId, authContext: mockAuthContext, entities: afterEntity, diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts index f671f48632c..469f35f44cd 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-column-name-to-field-metadata-id.util.spec.ts @@ -1,19 +1,20 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getColumnNameToFieldMetadataIdMap } from 'src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util'; describe('getColumnNameToFieldMetadataIdMap', () => { - const createMockObjectMetadataItemWithFieldMaps = ( - fieldsById: Record, - ): ObjectMetadataItemWithFieldMaps => + const createMockFlatObjectMetadata = ( + fieldMetadataIds: string[], + ): FlatObjectMetadata => ({ id: 'test-object-id', nameSingular: 'test', namePlural: 'tests', labelSingular: 'Test', labelPlural: 'Tests', - description: 'Test object', icon: 'IconTest', targetTableName: 'test', isCustom: false, @@ -22,53 +23,87 @@ describe('getColumnNameToFieldMetadataIdMap', () => { isSystem: false, isAuditLogged: false, isSearchable: false, - labelIdentifierFieldMetadataId: '', - imageIdentifierFieldMetadataId: '', workspaceId: 'test-workspace-id', - indexMetadatas: [], - fieldsById, - fieldIdByName: {}, - fieldIdByJoinColumnName: {}, - }) as unknown as ObjectMetadataItemWithFieldMaps; + universalIdentifier: 'test-object-id', + indexMetadataIds: [], + fieldMetadataIds, + viewIds: [], + applicationId: null, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + shortcut: null, + description: null, + standardOverrides: null, + isUIReadOnly: false, + standardId: null, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + duplicateCriteria: null, + }) as FlatObjectMetadata; - const createMockFieldMetadata = ( + const createMockFlatFieldMetadata = ( id: string, name: string, type: FieldMetadataType, - ) => ({ - id, - name, - type, - label: name, - objectMetadataId: 'test-object-id', - isLabelSyncedWithName: true, - isNullable: true, - isUnique: false, - workspaceId: 'test-workspace-id', - createdAt: new Date(), - updatedAt: new Date(), + settings?: Record, + ): FlatFieldMetadata => + ({ + id, + name, + type, + label: name, + objectMetadataId: 'test-object-id', + isLabelSyncedWithName: true, + isNullable: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + ...(settings ? { settings } : {}), + }) as unknown as FlatFieldMetadata; + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, }); describe('with simple field types', () => { it('should return a map with column name to field metadata id for simple field types', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( - 'field-1', - 'name', - FieldMetadataType.TEXT, - ), - 'field-2': createMockFieldMetadata( - 'field-2', - 'age', - FieldMetadataType.NUMBER, - ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + const fields = [ + createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT), + createMockFlatFieldMetadata('field-2', 'age', FieldMetadataType.NUMBER), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['name']).toBe('field-1'); @@ -79,19 +114,21 @@ describe('getColumnNameToFieldMetadataIdMap', () => { describe('with composite field types', () => { it('should return column names to field metadata id for FULL_NAME composite type', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'fullName', FieldMetadataType.FULL_NAME, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['fullNameFirstName']).toBe('field-1'); @@ -100,19 +137,21 @@ describe('getColumnNameToFieldMetadataIdMap', () => { }); it('should return column names to field metadata id for CURRENCY composite type', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'price', FieldMetadataType.CURRENCY, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['priceAmountMicros']).toBe('field-1'); @@ -121,29 +160,27 @@ describe('getColumnNameToFieldMetadataIdMap', () => { }); it('should handle multiple composite fields', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'fullName', FieldMetadataType.FULL_NAME, ), - 'field-2': createMockFieldMetadata( + createMockFlatFieldMetadata( 'field-2', 'price', FieldMetadataType.CURRENCY, ), - 'field-3': createMockFieldMetadata( - 'field-3', - 'name', - FieldMetadataType.TEXT, - ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + createMockFlatFieldMetadata('field-3', 'name', FieldMetadataType.TEXT), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['fullNameFirstName']).toBe('field-1'); @@ -157,34 +194,28 @@ describe('getColumnNameToFieldMetadataIdMap', () => { describe('with mixed field types', () => { it('should handle both simple and composite field types', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( - 'field-1', - 'name', - FieldMetadataType.TEXT, - ), - 'field-2': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT), + createMockFlatFieldMetadata( 'field-2', 'fullName', FieldMetadataType.FULL_NAME, ), - 'field-3': createMockFieldMetadata( - 'field-3', - 'age', - FieldMetadataType.NUMBER, - ), - 'field-4': createMockFieldMetadata( + createMockFlatFieldMetadata('field-3', 'age', FieldMetadataType.NUMBER), + createMockFlatFieldMetadata( 'field-4', 'price', FieldMetadataType.CURRENCY, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['name']).toBe('field-1'); @@ -199,30 +230,26 @@ describe('getColumnNameToFieldMetadataIdMap', () => { describe('with relation field types', () => { it('should handle relation field types with join column name', () => { - const fieldsById = { - 'field-1': { - ...createMockFieldMetadata( - 'field-1', - 'company', - FieldMetadataType.RELATION, - ), - settings: { + const fields = [ + createMockFlatFieldMetadata( + 'field-1', + 'company', + FieldMetadataType.RELATION, + { relationType: 'ONE_TO_ONE', joinColumnName: 'companyId', }, - }, - 'field-2': createMockFieldMetadata( - 'field-2', - 'name', - FieldMetadataType.TEXT, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + createMockFlatFieldMetadata('field-2', 'name', FieldMetadataType.TEXT), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['companyId']).toBe('field-1'); @@ -232,30 +259,26 @@ describe('getColumnNameToFieldMetadataIdMap', () => { }); it('should skip ONE_TO_MANY relation field types', () => { - const fieldsById = { - 'field-1': { - ...createMockFieldMetadata( - 'field-1', - 'employees', - FieldMetadataType.RELATION, - ), - settings: { + const fields = [ + createMockFlatFieldMetadata( + 'field-1', + 'employees', + FieldMetadataType.RELATION, + { relationType: 'ONE_TO_MANY', joinColumnName: 'companyId', }, - }, - 'field-2': createMockFieldMetadata( - 'field-2', - 'name', - FieldMetadataType.TEXT, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + createMockFlatFieldMetadata('field-2', 'name', FieldMetadataType.TEXT), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result['name']).toBe('field-2'); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts index 9e7cc6e8519..6cc35623c97 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-field-metadata-id-to-column-names-map.util.spec.ts @@ -1,19 +1,20 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getFieldMetadataIdToColumnNamesMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util'; describe('getFieldMetadataIdToColumnNamesMap', () => { - const createMockObjectMetadataItemWithFieldMaps = ( - fieldsById: Record, - ): ObjectMetadataItemWithFieldMaps => + const createMockFlatObjectMetadata = ( + fieldMetadataIds: string[], + ): FlatObjectMetadata => ({ id: 'test-object-id', nameSingular: 'test', namePlural: 'tests', labelSingular: 'Test', labelPlural: 'Tests', - description: 'Test object', icon: 'IconTest', targetTableName: 'test', isCustom: false, @@ -22,53 +23,85 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { isSystem: false, isAuditLogged: false, isSearchable: false, - labelIdentifierFieldMetadataId: '', - imageIdentifierFieldMetadataId: '', workspaceId: 'test-workspace-id', - indexMetadatas: [], - fieldsById, - fieldIdByName: {}, - fieldIdByJoinColumnName: {}, - }) as unknown as ObjectMetadataItemWithFieldMaps; + universalIdentifier: 'test-object-id', + indexMetadataIds: [], + fieldMetadataIds, + viewIds: [], + applicationId: null, + isLabelSyncedWithName: false, + createdAt: new Date(), + updatedAt: new Date(), + shortcut: null, + description: null, + standardOverrides: null, + isUIReadOnly: false, + standardId: null, + labelIdentifierFieldMetadataId: null, + imageIdentifierFieldMetadataId: null, + duplicateCriteria: null, + }) as FlatObjectMetadata; - const createMockFieldMetadata = ( + const createMockFlatFieldMetadata = ( id: string, name: string, type: FieldMetadataType, - ) => ({ - id, - name, - type, - label: name, - objectMetadataId: 'test-object-id', - isLabelSyncedWithName: true, - isNullable: true, - isUnique: false, - workspaceId: 'test-workspace-id', - createdAt: new Date(), - updatedAt: new Date(), + ): FlatFieldMetadata => + ({ + id, + name, + type, + label: name, + objectMetadataId: 'test-object-id', + isLabelSyncedWithName: true, + isNullable: true, + createdAt: new Date(), + updatedAt: new Date(), + universalIdentifier: id, + viewFieldIds: [], + viewFilterIds: [], + viewGroupIds: [], + kanbanAggregateOperationViewIds: [], + calendarViewIds: [], + applicationId: null, + }) as unknown as FlatFieldMetadata; + + const buildFlatFieldMetadataMaps = ( + fields: FlatFieldMetadata[], + ): FlatEntityMaps => ({ + byId: fields.reduce( + (acc, field) => { + acc[field.id] = field; + + return acc; + }, + {} as Record, + ), + idByUniversalIdentifier: fields.reduce( + (acc, field) => { + acc[field.universalIdentifier] = field.id; + + return acc; + }, + {} as Record, + ), + universalIdentifiersByApplicationId: {}, }); describe('with simple field types', () => { it('should return a map with single column name for simple field types', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( - 'field-1', - 'name', - FieldMetadataType.TEXT, - ), - 'field-2': createMockFieldMetadata( - 'field-2', - 'age', - FieldMetadataType.NUMBER, - ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + const fields = [ + createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT), + createMockFlatFieldMetadata('field-2', 'age', FieldMetadataType.NUMBER), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result.get('field-1')).toEqual(['name']); @@ -79,19 +112,21 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { describe('with composite field types', () => { it('should return multiple column names for FULL_NAME composite type', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'fullName', FieldMetadataType.FULL_NAME, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result.get('field-1')).toEqual([ @@ -102,19 +137,21 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { }); it('should return multiple column names for CURRENCY composite type', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'price', FieldMetadataType.CURRENCY, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result.get('field-1')).toEqual([ @@ -125,29 +162,27 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { }); it('should handle multiple composite fields', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata( 'field-1', 'fullName', FieldMetadataType.FULL_NAME, ), - 'field-2': createMockFieldMetadata( + createMockFlatFieldMetadata( 'field-2', 'price', FieldMetadataType.CURRENCY, ), - 'field-3': createMockFieldMetadata( - 'field-3', - 'name', - FieldMetadataType.TEXT, - ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + createMockFlatFieldMetadata('field-3', 'name', FieldMetadataType.TEXT), + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result.get('field-1')).toEqual([ @@ -165,34 +200,28 @@ describe('getFieldMetadataIdToColumnNamesMap', () => { describe('with mixed field types', () => { it('should handle both simple and composite field types', () => { - const fieldsById = { - 'field-1': createMockFieldMetadata( - 'field-1', - 'name', - FieldMetadataType.TEXT, - ), - 'field-2': createMockFieldMetadata( + const fields = [ + createMockFlatFieldMetadata('field-1', 'name', FieldMetadataType.TEXT), + createMockFlatFieldMetadata( 'field-2', 'fullName', FieldMetadataType.FULL_NAME, ), - 'field-3': createMockFieldMetadata( - 'field-3', - 'age', - FieldMetadataType.NUMBER, - ), - 'field-4': createMockFieldMetadata( + createMockFlatFieldMetadata('field-3', 'age', FieldMetadataType.NUMBER), + createMockFlatFieldMetadata( 'field-4', 'price', FieldMetadataType.CURRENCY, ), - }; - - const objectMetadataItemWithFieldMaps = - createMockObjectMetadataItemWithFieldMaps(fieldsById); + ]; + const flatObjectMetadata = createMockFlatObjectMetadata( + fields.map((f) => f.id), + ); + const flatFieldMetadataMaps = buildFlatFieldMetadataMaps(fields); const result = getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); expect(result.get('field-1')).toEqual(['name']); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts index 1fc5390d0fa..908e27975e1 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/compute-relation-connect-query-configs.util.ts @@ -5,11 +5,13 @@ import { getUniqueConstraintsFields, isDefined } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.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'; import { type ConnectObject } from 'src/engine/twenty-orm/entity-manager/types/query-deep-partial-entity-with-nested-relation-fields.type'; import { type RelationConnectQueryConfig, @@ -26,8 +28,10 @@ import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata- export const computeRelationConnectQueryConfigs = ( entities: Record[], - objectMetadata: ObjectMetadataItemWithFieldMaps, - objectMetadataMap: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + flatIndexMaps: FlatEntityMaps, relationConnectQueryFieldsByEntityIndex: RelationConnectQueryFieldsByEntityIndex, ) => { const allConnectQueryConfigs: Record = {}; @@ -48,8 +52,10 @@ export const computeRelationConnectQueryConfigs = ( } = computeRecordToConnectCondition( connectFieldName, connectObject, - objectMetadata, - objectMetadataMap, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, entity, ); @@ -102,7 +108,7 @@ const updateConnectQueryConfigs = ( const createConnectQueryConfig = ( connectFieldName: string, recordToConnectCondition: UniqueConstraintCondition, - uniqueConstraintFields: FieldMetadataEntity[], + uniqueConstraintFields: FlatFieldMetadata[], targetObjectNameSingular: string, entityIndex: number, ) => { @@ -121,26 +127,33 @@ const createConnectQueryConfig = ( const computeRecordToConnectCondition = ( connectFieldName: string, connectObject: ConnectObject, - objectMetadata: ObjectMetadataItemWithFieldMaps, - objectMetadataMap: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, + flatIndexMaps: FlatEntityMaps, entity: Record, ): { recordToConnectCondition: UniqueConstraintCondition; - uniqueConstraintFields: FieldMetadataEntity[]; + uniqueConstraintFields: FlatFieldMetadata[]; targetObjectNameSingular: string; } => { - const field = - objectMetadata.fieldsById[objectMetadata.fieldIdByName[connectFieldName]]; + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + const field = flatFieldMetadataMaps.byId[fieldIdByName[connectFieldName]]; if ( + !isDefined(field) || (!isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) && !isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION)) || field.settings?.relationType !== RelationType.MANY_TO_ONE ) { - const objectMetadataNameSingular = objectMetadata.nameSingular; + const objectMetadataNameSingular = flatObjectMetadata.nameSingular; throw new TwentyORMException( - `Connect is not allowed for ${connectFieldName} on ${objectMetadata.nameSingular}`, + `Connect is not allowed for ${connectFieldName} on ${flatObjectMetadata.nameSingular}`, TwentyORMExceptionCode.CONNECT_NOT_ALLOWED, { userFriendlyMessage: msg`Connect is not allowed for ${connectFieldName} on ${objectMetadataNameSingular}`, @@ -150,7 +163,7 @@ const computeRecordToConnectCondition = ( checkNoRelationFieldConflictOrThrow(entity, connectFieldName); const targetObjectMetadata = - objectMetadataMap.byId[field.relationTargetObjectMetadataId || '']; + flatObjectMetadataMaps.byId[field.relationTargetObjectMetadataId || '']; if (!isDefined(targetObjectMetadata)) { throw new TwentyORMException( @@ -164,6 +177,8 @@ const computeRecordToConnectCondition = ( const uniqueConstraintFields = checkUniqueConstraintFullyPopulated( targetObjectMetadata, + flatFieldMetadataMaps, + flatIndexMaps, connectObject, connectFieldName, ); @@ -179,16 +194,41 @@ const computeRecordToConnectCondition = ( }; const checkUniqueConstraintFullyPopulated = ( - objectMetadata: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, + flatIndexMaps: FlatEntityMaps, connectObject: ConnectObject, connectFieldName: string, ) => { + const fields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, + ); + + const indexMetadatas = flatObjectMetadata.indexMetadataIds + .map((indexId) => flatIndexMaps.byId[indexId]) + .filter(isDefined) + .map((index) => ({ + id: index.id, + isUnique: index.isUnique, + indexFieldMetadatas: index.flatIndexFieldMetadatas.map( + (fieldMetadata) => ({ + fieldMetadataId: fieldMetadata.fieldMetadataId, + }), + ), + })); + const uniqueConstraintsFields = getUniqueConstraintsFields< - FieldMetadataEntity, - ObjectMetadataEntity + FlatFieldMetadata, + { + id: string; + indexMetadatas: typeof indexMetadatas; + fields: FlatFieldMetadata[]; + } >({ - ...objectMetadata, - fields: Object.values(objectMetadata.fieldsById), + id: flatObjectMetadata.id, + indexMetadatas, + fields, }); const hasUniqueConstraintFieldFullyPopulated = uniqueConstraintsFields.some( @@ -243,7 +283,7 @@ const checkNoRelationFieldConflictOrThrow = ( }; const computeUniqueConstraintCondition = ( - uniqueConstraintFields: FieldMetadataEntity[], + uniqueConstraintFields: FlatFieldMetadata[], connectObject: ConnectObject, ): UniqueConstraintCondition => { return uniqueConstraintFields.reduce((acc, uniqueConstraintField) => { @@ -271,7 +311,7 @@ const computeUniqueConstraintCondition = ( const checkUniqueConstraintsAreSameOrThrow = ( relationConnectQueryConfig: RelationConnectQueryConfig, - uniqueConstraintFields: FieldMetadataEntity[], + uniqueConstraintFields: FlatFieldMetadata[], ) => { if ( !deepEqual( diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts index 7025e6e45cb..8bba119c924 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts @@ -1,8 +1,9 @@ import { type FieldMetadataType } from 'twenty-shared/types'; import { type RelationType } from 'typeorm/metadata/types/RelationTypes'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { RelationException, RelationExceptionCode, @@ -16,12 +17,13 @@ interface RelationDetails { joinColumn: { name: string } | undefined; } -export async function determineSchemaRelationDetails( - fieldMetadata: FieldMetadataEntity< +export function determineSchemaRelationDetails( + fieldMetadata: FlatFieldMetadata< FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION >, - objectMetadataMaps: ObjectMetadataMaps, -): Promise { + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, +): RelationDetails { if (!fieldMetadata.settings) { throw new Error('Field metadata settings are missing'); } @@ -38,9 +40,9 @@ export async function determineSchemaRelationDetails( } const sourceObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.objectMetadataId]; + flatObjectMetadataMaps.byId[fieldMetadata.objectMetadataId]; const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + flatObjectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; if (!sourceObjectMetadata || !targetObjectMetadata) { throw new RelationException( @@ -57,9 +59,7 @@ export async function determineSchemaRelationDetails( } const targetFieldMetadata = - targetObjectMetadata.fieldsById[ - fieldMetadata.relationTargetFieldMetadataId - ]; + flatFieldMetadataMaps.byId[fieldMetadata.relationTargetFieldMetadataId]; if (!targetFieldMetadata) { throw new Error('Target field metadata not found'); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts index 3730cd42fb2..fe15bc24546 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts @@ -4,14 +4,17 @@ import { } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; export function formatData( data: T, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ): T { if (!data) { return data; @@ -19,24 +22,27 @@ export function formatData( if (Array.isArray(data)) { return data.map((item) => - formatData(item, objectMetadataItemWithFieldMaps), + formatData(item, flatObjectMetadata, flatFieldMetadataMaps), ) as T; } + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const newData: Record = {}; for (const [key, value] of Object.entries(data)) { - const fieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName[key] || - objectMetadataItemWithFieldMaps.fieldIdByJoinColumnName[key]; + const fieldMetadataId = fieldIdByName[key] || fieldIdByJoinColumnName[key]; - const fieldMetadata = - objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId]; + const fieldMetadata = flatFieldMetadataMaps.byId[fieldMetadataId]; if (!fieldMetadata) { throw new Error( - `Field metadata for field "${key}" is missing in object metadata ${objectMetadataItemWithFieldMaps.nameSingular}`, + `Field metadata for field "${key}" is missing in object metadata ${flatObjectMetadata.nameSingular}`, ); } @@ -58,7 +64,7 @@ export function formatData( export function formatCompositeField( // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Record { const compositeType = compositeTypeDefinitions.get( @@ -81,7 +87,7 @@ export function formatCompositeField( if (value && value[subFieldKey] !== undefined) { formattedCompositeField[fullFieldName] = formatFieldMetadataValue( value[subFieldKey], - property as unknown as FieldMetadataEntity, + property as unknown as FlatFieldMetadata, ); } } @@ -92,7 +98,7 @@ export function formatCompositeField( function formatFieldMetadataValue( // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ) { if ( fieldMetadata.type === FieldMetadataType.RAW_JSON && diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts index 70949f216e4..c04f9a15316 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts @@ -12,18 +12,21 @@ import { DEFAULT_COMPOSITE_FIELDS_NULL_EQUIVALENT_VALUE, DEFAULT_TEXT_FIELD_NULL_EQUIVALENT_VALUE, } from 'src/engine/api/common/common-args-processors/data-arg-processor/constants/null-equivalent-values.constant'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export function formatResult( // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any, - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps | undefined, - objectMetadataMaps: ObjectMetadataMaps, + flatObjectMetadata: FlatObjectMetadata | undefined, + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ): T { if (!isDefined(data)) { return data; @@ -31,7 +34,12 @@ export function formatResult( if (Array.isArray(data)) { return data.map((item) => - formatResult(item, objectMetadataItemWithFieldMaps, objectMetadataMaps), + formatResult( + item, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ), ) as T; } @@ -39,12 +47,18 @@ export function formatResult( return data; } - if (!objectMetadataItemWithFieldMaps) { + if (!flatObjectMetadata) { throw new Error('Object metadata is missing'); } + const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + const compositeFieldMetadataMap = getCompositeFieldMetadataMap( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); const newData: object = {}; @@ -53,14 +67,12 @@ export function formatResult( const compositePropertyArgs = compositeFieldMetadataMap.get(key); const fieldMetadataId = - objectMetadataItemWithFieldMaps.fieldIdByName[key] || - objectMetadataItemWithFieldMaps.fieldIdByName[ - compositePropertyArgs?.parentField ?? '' - ]; + fieldIdByName[key] || + fieldIdByName[compositePropertyArgs?.parentField ?? '']; - const fieldMetadata = objectMetadataItemWithFieldMaps.fieldsById[ - fieldMetadataId - ] as FieldMetadataEntity | undefined; + const fieldMetadata = flatFieldMetadataMaps.byId[fieldMetadataId] as + | FlatFieldMetadata + | undefined; const isRelation = fieldMetadata ? isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION) @@ -71,8 +83,9 @@ export function formatResult( // @ts-expect-error legacy noImplicitAny newData[key] = formatResult( value, - objectMetadataItemWithFieldMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); } else if (fieldMetadata) { // @ts-expect-error legacy noImplicitAny @@ -93,7 +106,9 @@ export function formatResult( } const targetObjectMetadata = - objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + flatObjectMetadataMaps.byId[ + fieldMetadata.relationTargetObjectMetadataId + ]; if (!targetObjectMetadata) { throw new Error( @@ -105,7 +120,8 @@ export function formatResult( newData[key] = formatResult( value, targetObjectMetadata, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); } @@ -131,8 +147,9 @@ export function formatResult( : value; } - const fieldMetadataItemsOfTypeDateOnly = Object.values( - objectMetadataItemWithFieldMaps.fieldsById, + const fieldMetadataItemsOfTypeDateOnly = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ).filter((field) => field.type === FieldMetadataType.DATE); for (const dateField of fieldMetadataItemsOfTypeDateOnly) { @@ -151,10 +168,12 @@ export function formatResult( } export function getCompositeFieldMetadataMap( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { const compositeFieldMetadataCollection = getCompositeFieldMetadataCollection( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, ); return new Map( @@ -211,7 +230,7 @@ function formatFieldMetadataValue( function transformCompositeFieldNullValue( value: unknown, compositePropertyName: string, - fieldMetadata: FieldMetadataEntity, + fieldMetadata: FlatFieldMetadata, ) { if (!isNull(value)) return value; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts index 76b75c05cf3..f9402a20657 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-twenty-orm-event-to-database-batch-event.util.ts @@ -12,7 +12,9 @@ import type { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/typ import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emitter/types/object-record-upsert.event'; import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values'; -import type { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import type { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMException, TwentyORMExceptionCode, @@ -24,13 +26,15 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < >({ action, objectMetadataItem, + flatFieldMetadataMaps, workspaceId, authContext, entities, beforeEntities, }: { action: DatabaseEventAction; - objectMetadataItem: ObjectMetadataItemWithFieldMaps; + objectMetadataItem: FlatObjectMetadata; + flatFieldMetadataMaps: FlatEntityMaps; workspaceId: string; authContext?: AuthContext; entities: T | T[]; @@ -41,7 +45,6 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < } const objectMetadataNameSingular = objectMetadataItem.nameSingular; - const fields = Object.values(objectMetadataItem.fieldsById ?? {}); const entityArray = isDefined(entities) ? Array.isArray(entities) ? entities @@ -89,6 +92,7 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < before, after, objectMetadataItem, + flatFieldMetadataMaps, ) as Partial>; const updatedFields = Object.keys(diff); @@ -158,6 +162,7 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < before ?? {}, after, objectMetadataItem, + flatFieldMetadataMaps, ) as Partial>; updatedFields = Object.keys(diff); @@ -183,7 +188,7 @@ export const formatTwentyOrmEventToDatabaseBatchEvent = < objectMetadataNameSingular, action, events, - objectMetadata: { ...objectMetadataItem, fields }, + objectMetadata: objectMetadataItem, workspaceId, }; }; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts index cc2c8269ce6..54f01222d8a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-column-name-to-field-metadata-id.util.ts @@ -1,16 +1,18 @@ -import { isDefined } from 'twenty-shared/utils'; import { type CompositeType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type ColumnNameProcessor, processFieldMetadataForColumnNameMapping, } from 'src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util'; export function getColumnNameToFieldMetadataIdMap( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { const columnNameToFieldMetadataIdMap: Record = {}; @@ -21,7 +23,7 @@ export function getColumnNameToFieldMetadataIdMap( compositeType, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; compositeType: CompositeType; }) => { compositeType.properties.forEach((compositeProperty) => { @@ -59,7 +61,8 @@ export function getColumnNameToFieldMetadataIdMap( }; processFieldMetadataForColumnNameMapping( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, processor, ); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts index 4822de779f8..6092699e344 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-composite-field-metadata-collection.ts @@ -1,11 +1,16 @@ +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export function getCompositeFieldMetadataCollection( - ObjectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { - const compositeFieldMetadataCollection = Object.values( - ObjectMetadataItemWithFieldMaps.fieldsById, + const compositeFieldMetadataCollection = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ).filter((fieldMetadataItem) => isCompositeFieldMetadataType(fieldMetadataItem.type), ); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts index 2b5b9893a09..1adf6bf2af3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util.ts @@ -1,15 +1,17 @@ import { type CompositeType } from 'twenty-shared/types'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ColumnNameProcessor, processFieldMetadataForColumnNameMapping, } from 'src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util'; export function getFieldMetadataIdToColumnNamesMap( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, ) { const fieldMetadataToColumnNamesMap = new Map(); @@ -20,7 +22,7 @@ export function getFieldMetadataIdToColumnNamesMap( compositeType, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; compositeType: CompositeType; }) => { compositeType.properties.forEach((compositeProperty) => { @@ -52,7 +54,7 @@ export function getFieldMetadataIdToColumnNamesMap( columnName, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; columnName: string; }) => { fieldMetadataToColumnNamesMap.set(fieldMetadataId, [columnName]); @@ -60,7 +62,8 @@ export function getFieldMetadataIdToColumnNamesMap( }; processFieldMetadataForColumnNameMapping( - objectMetadataItemWithFieldMaps, + flatObjectMetadata, + flatFieldMetadataMaps, processor, ); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util.ts index b58c1a4231a..bd08b43526d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-object-metadata-from-entity-target.util.ts @@ -6,8 +6,7 @@ import { import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMException, TwentyORMExceptionCode, @@ -17,7 +16,7 @@ import { WorkspaceEntitiesStorage } from 'src/engine/twenty-orm/storage/workspac export const getObjectMetadataFromEntityTarget = ( entityTarget: EntityTarget, internalContext: WorkspaceInternalContext, -): ObjectMetadataItemWithFieldMaps => { +): FlatObjectMetadata => { const objectMetadataName = typeof entityTarget === 'string' ? entityTarget @@ -33,22 +32,29 @@ export const getObjectMetadataFromEntityTarget = ( ); } - const objectMetadata = getObjectMetadataMapItemByNameSingular( - internalContext.objectMetadataMaps, - objectMetadataName, - ); + const objectMetadataId = + internalContext.objectIdByNameSingular[objectMetadataName]; - if (!objectMetadata) { + if (!objectMetadataId) { throw new TwentyORMException( `Object metadata for object "${objectMetadataName}" is missing ` + `in workspace "${internalContext.workspaceId}" ` + `with object metadata collection length: ${ - Object.keys(internalContext.objectMetadataMaps.idByNameSingular) - .length + Object.keys(internalContext.objectIdByNameSingular).length }`, TwentyORMExceptionCode.MALFORMED_METADATA, ); } + const objectMetadata = + internalContext.flatObjectMetadataMaps.byId[objectMetadataId]; + + if (!objectMetadata) { + throw new TwentyORMException( + `Object metadata for object "${objectMetadataName}" (id: ${objectMetadataId}) is missing`, + TwentyORMExceptionCode.MALFORMED_METADATA, + ); + } + return objectMetadata; }; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts index dc0c103d1f5..bb815ad5972 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/process-field-metadata-for-column-name-mapping.util.ts @@ -1,5 +1,4 @@ import { - type FieldMetadataRelationSettings, type CompositeType, compositeTypeDefinitions, } from 'twenty-shared/types'; @@ -7,15 +6,17 @@ import { import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { extractGraphQLRelationFieldNames } from 'src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util'; -import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { PermissionsException, PermissionsExceptionCode, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; export type ColumnNameProcessor = { processCompositeField: ({ @@ -24,7 +25,7 @@ export type ColumnNameProcessor = { compositeType, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; compositeType: CompositeType; }) => void; processRelationField: ({ @@ -34,7 +35,7 @@ export type ColumnNameProcessor = { connectFieldName, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; joinColumnName: string; connectFieldName?: string; }) => void; @@ -44,18 +45,22 @@ export type ColumnNameProcessor = { columnName, }: { fieldMetadataId: string; - fieldMetadata: FieldMetadataEntity; + fieldMetadata: FlatFieldMetadata; columnName: string; }) => void; }; export function processFieldMetadataForColumnNameMapping( - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, processor: ColumnNameProcessor, ) { - for (const [fieldMetadataId, fieldMetadata] of Object.entries( - objectMetadataItemWithFieldMaps.fieldsById, - )) { + for (const fieldMetadataId of flatObjectMetadata.fieldMetadataIds) { + const fieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldMetadataId, + }); + if (isCompositeFieldMetadataType(fieldMetadata.type)) { const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); @@ -72,9 +77,8 @@ export function processFieldMetadataForColumnNameMapping( compositeType, }); } else { - if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) { - const fieldMetadataSettings = - fieldMetadata.settings as FieldMetadataRelationSettings; + if (isMorphOrRelationFlatFieldMetadata(fieldMetadata)) { + const fieldMetadataSettings = fieldMetadata.settings; if (fieldMetadataSettings?.relationType === RelationType.ONE_TO_MANY) { continue; diff --git a/packages/twenty-server/src/engine/workspace-cache/types/workspace-context-data.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/workspace-context-data.type.ts deleted file mode 100644 index 9d552bb8fbf..00000000000 --- a/packages/twenty-server/src/engine/workspace-cache/types/workspace-context-data.type.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; - -import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; - -export type WorkspaceContextData = { - objectMetadataMaps: ObjectMetadataMaps; - metadataVersion: number; - featureFlagsMap: Record; - permissionsPerRoleId: ObjectsPermissionsByRoleId; -}; diff --git a/packages/twenty-server/src/engine/workspace-event-emitter/types/workspace-event-batch.type.ts b/packages/twenty-server/src/engine/workspace-event-emitter/types/workspace-event-batch.type.ts index 9d313629cf3..5ab971cfcbb 100644 --- a/packages/twenty-server/src/engine/workspace-event-emitter/types/workspace-event-batch.type.ts +++ b/packages/twenty-server/src/engine/workspace-event-emitter/types/workspace-event-batch.type.ts @@ -1,8 +1,8 @@ -import type { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import type { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; export type WorkspaceEventBatch = { name: string; workspaceId: string; - objectMetadata: Omit; + objectMetadata: FlatObjectMetadata; events: WorkspaceEvent[]; }; diff --git a/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts b/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts index a6fca9ba13d..6beda6e6d62 100644 --- a/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts +++ b/packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts @@ -10,7 +10,7 @@ import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/ import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; import { ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emitter/types/object-record-upsert.event'; -import type { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import type { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type CustomEventName } from 'src/engine/workspace-event-emitter/types/custom-event-name.type'; import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; @@ -29,7 +29,7 @@ export type DatabaseBatchEventInput> = { objectMetadataNameSingular: string; action: A; events: ActionEventMap[A][]; - objectMetadata: Omit; + objectMetadata: FlatObjectMetadata; workspaceId: string; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts index a4d03ea1446..406ed5fa505 100644 --- a/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts @@ -11,6 +11,7 @@ import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.serv import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; @@ -145,6 +146,12 @@ describe('WorkspaceManagerService', () => { }), }, }, + { + provide: WorkspaceManyOrAllFlatEntityMapsCacheService, + useValue: { + invalidateEntireCache: jest.fn(), + }, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts index bdceb99b252..edd78a63c1e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts @@ -8,8 +8,9 @@ import { DataSource } from 'typeorm'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; import { @@ -268,6 +269,7 @@ export class DevSeederDataService { private readonly objectMetadataService: ObjectMetadataServiceV2, private readonly timelineActivitySeederService: TimelineActivitySeederService, private readonly fileStorageService: FileStorageService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} public async seed({ @@ -282,6 +284,14 @@ export class DevSeederDataService { const objectMetadataItems = await this.objectMetadataService.findManyWithinWorkspace(workspaceId); + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], + }, + ); + await this.coreDataSource.transaction( async (entityManager: WorkspaceEntityManager) => { await this.seedRecordsInBatches({ @@ -300,7 +310,12 @@ export class DevSeederDataService { await this.seedAttachmentFiles(workspaceId); - await prefillWorkflows(entityManager, schemaName, objectMetadataItems); + await prefillWorkflows( + entityManager, + schemaName, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); }, ); } @@ -316,7 +331,7 @@ export class DevSeederDataService { schemaName: string; workspaceId: string; featureFlags?: Record; - objectMetadataItems: ObjectMetadataEntity[]; + objectMetadataItems: FlatObjectMetadata[]; }) { const batches = getRecordSeedsBatches(workspaceId, featureFlags); diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts index 383e0b9f103..ef79e12307d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts @@ -193,13 +193,19 @@ export class DevSeederMetadataService { featureFlags?: Record; twentyStandardFlatApplication: FlatApplication; }): Promise { - const createdObjectMetadata = - await this.objectMetadataServiceV2.findManyWithinWorkspace(workspaceId); + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], + }, + ); await prefillCoreViews({ coreDataSource: this.coreDataSource, workspaceId, - objectMetadataItems: createdObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceSchemaName: dataSourceMetadata.schema, featureFlags, twentyStandardFlatApplication, diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views.ts index 8abac5fb77f..dbc93d65827 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views.ts @@ -1,7 +1,11 @@ import { isString } from '@sniptt/guards'; import { type DataSource, type QueryRunner } from 'typeorm'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; @@ -37,19 +41,46 @@ import { workspaceMembersAllView } from 'src/engine/workspace-manager/standard-o type PrefillCoreViewsArgs = { coreDataSource: DataSource; workspaceId: string; - objectMetadataItems: ObjectMetadataEntity[]; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; featureFlags?: Record; workspaceSchemaName: string; twentyStandardFlatApplication: FlatApplication; }; +// This is a temporary function to build the object metadata items from the flat maps. +// We should use the maps in the seeders instead. +const buildObjectMetadataItemsFromFlatMaps = ( + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, +): ObjectMetadataEntity[] => { + return Object.values(flatObjectMetadataMaps.byId) + .filter((flatObjectMetadata) => flatObjectMetadata !== undefined) + .map((flatObjectMetadata) => { + const fields = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, + ); + + return { + ...flatObjectMetadata, + fields, + } as unknown as ObjectMetadataEntity; + }); +}; + export const prefillCoreViews = async ({ coreDataSource, workspaceId, - objectMetadataItems, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceSchemaName, twentyStandardFlatApplication, }: PrefillCoreViewsArgs): Promise => { + const objectMetadataItems = buildObjectMetadataItemsFromFlatMaps( + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); const views = [ companiesAllView, peopleAllView, diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts index 10f76c69e6a..a00ba6455b1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows.ts @@ -1,9 +1,11 @@ +import { FieldActorSource } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type EntityManager } from 'typeorm'; -import { FieldActorSource } from 'twenty-shared/types'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d'; @@ -12,13 +14,15 @@ const QUICK_LEAD_WORKFLOW_VERSION_ID = 'ac67974f-c524-4288-9d88-af8515400b68'; export const prefillWorkflows = async ( entityManager: EntityManager, schemaName: string, - objectMetadataItems: ObjectMetadataEntity[], + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ) => { - const objectMetadataMaps = generateObjectMetadataMaps(objectMetadataItems); - const companyObjectMetadataId = - objectMetadataMaps.idByNameSingular['company']; + const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps( + flatObjectMetadataMaps, + ); - const personObjectMetadataId = objectMetadataMaps.idByNameSingular['person']; + const companyObjectMetadataId = objectIdByNameSingular['company']; + const personObjectMetadataId = objectIdByNameSingular['person']; if ( !isDefined(companyObjectMetadataId) || @@ -28,9 +32,10 @@ export const prefillWorkflows = async ( } const companyObjectMetadata = - objectMetadataMaps.byId[companyObjectMetadataId]; + flatObjectMetadataMaps.byId[companyObjectMetadataId]; - const personObjectMetadata = objectMetadataMaps.byId[personObjectMetadataId]; + const personObjectMetadata = + flatObjectMetadataMaps.byId[personObjectMetadataId]; if (!isDefined(companyObjectMetadata) || !isDefined(personObjectMetadata)) { throw new Error('Company or person object metadata not found'); @@ -220,8 +225,9 @@ export const prefillWorkflows = async ( _outputSchemaType: 'RECORD', fields: generateObjectRecordFields({ objectMetadataInfo: { - objectMetadataItemWithFieldsMaps: companyObjectMetadata, - objectMetadataMaps, + flatObjectMetadata: companyObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }, depth: 0, }), @@ -262,8 +268,9 @@ export const prefillWorkflows = async ( outputSchema: { fields: generateObjectRecordFields({ objectMetadataInfo: { - objectMetadataItemWithFieldsMaps: personObjectMetadata, - objectMetadataMaps, + flatObjectMetadata: personObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }, depth: 0, }), diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts index 038bfc18084..8a6af0955d5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts @@ -1,6 +1,8 @@ import { type DataSource, type EntityManager } from 'typeorm'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { prefillCompanies } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-companies'; import { prefillPeople } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-people'; import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-workflows'; @@ -8,13 +10,19 @@ import { prefillWorkflows } from 'src/engine/workspace-manager/standard-objects- export const standardObjectsPrefillData = async ( dataSource: DataSource, schemaName: string, - objectMetadataItems: ObjectMetadataEntity[], + flatObjectMetadataMaps: FlatEntityMaps, + flatFieldMetadataMaps: FlatEntityMaps, ) => { dataSource.transaction(async (entityManager: EntityManager) => { await prefillCompanies(entityManager, schemaName); await prefillPeople(entityManager, schemaName); - await prefillWorkflows(entityManager, schemaName, objectMetadataItems); + await prefillWorkflows( + entityManager, + schemaName, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + ); }); }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module.ts deleted file mode 100644 index 06bb4cb45b5..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { WorkspaceHealthCommand } from 'src/engine/workspace-manager/workspace-health/commands/workspace-health.command'; -import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module'; - -@Module({ - imports: [WorkspaceHealthModule], - providers: [WorkspaceHealthCommand], -}) -export class WorkspaceHealthCommandModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health.command.ts deleted file mode 100644 index c67a2632272..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/commands/workspace-health.command.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { Logger } from '@nestjs/common'; - -import chalk from 'chalk'; -import { Command, CommandRunner, Option } from 'nest-commander'; - -import { WorkspaceHealthFixKind } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface'; -import { WorkspaceHealthMode } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface'; - -import { CommandLogger } from 'src/command/command-logger'; -import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service'; - -interface WorkspaceHealthCommandOptions { - workspaceId: string; - mode?: WorkspaceHealthMode; - fix?: WorkspaceHealthFixKind; - dryRun?: boolean; -} - -@Command({ - name: 'workspace:health', - description: 'Check health of the given workspace.', -}) -export class WorkspaceHealthCommand extends CommandRunner { - private readonly logger = new Logger(WorkspaceHealthCommand.name); - private readonly commandLogger = new CommandLogger( - WorkspaceHealthCommand.name, - ); - - constructor(private readonly workspaceHealthService: WorkspaceHealthService) { - super(); - } - - async run( - _passedParam: string[], - options: WorkspaceHealthCommandOptions, - ): Promise { - const issues = await this.workspaceHealthService.healthCheck( - options.workspaceId, - { - mode: options.mode ?? WorkspaceHealthMode.All, - }, - ); - - if (issues.length === 0) { - this.logger.log(chalk.green('Workspace is healthy')); - } else { - this.logger.log( - chalk.red(`Workspace is not healthy, found ${issues.length} issues`), - ); - - for (let issueIndex = 0; issueIndex < issues.length; issueIndex++) { - this.logger.log( - chalk.red(`Issue #${issueIndex + 1} : ${issues[issueIndex].message}`), - ); - } - - const logFilePath = await this.commandLogger.writeLog( - `workspace-health-issues-${options.workspaceId}`, - issues, - ); - - this.logger.log( - chalk.yellow(`Issues written to log at : ${logFilePath}`), - ); - } - - if (options.fix) { - this.logger.log(chalk.yellow('Fixing issues')); - - const { workspaceMigrations, metadataEntities } = - await this.workspaceHealthService.fixIssues( - options.workspaceId, - issues, - { - type: options.fix, - applyChanges: !options.dryRun, - }, - ); - const totalCount = workspaceMigrations.length + metadataEntities.length; - - if (options.dryRun) { - await this.commandLogger.writeLog( - `workspace-health-${options.fix}-migrations`, - workspaceMigrations, - ); - - await this.commandLogger.writeLog( - `workspace-health-${options.fix}-metadata-entities`, - metadataEntities, - ); - } else { - this.logger.log( - chalk.green(`Fixed ${totalCount}/${issues.length} issues`), - ); - } - } - } - - @Option({ - flags: '-w, --workspace-id [workspace_id]', - description: 'workspace id', - required: true, - }) - parseWorkspaceId(value: string): string { - return value; - } - - @Option({ - flags: '-f, --fix [kind]', - description: 'fix issues', - required: false, - }) - fix(value: string): WorkspaceHealthFixKind { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!Object.values(WorkspaceHealthFixKind).includes(value as any)) { - throw new Error(`Invalid fix kind ${value}`); - } - - return value as WorkspaceHealthFixKind; - } - - @Option({ - flags: '-m, --mode [mode]', - description: 'Mode of the health check [structure, metadata, all]', - required: false, - defaultValue: WorkspaceHealthMode.All, - }) - parseMode(value: string): WorkspaceHealthMode { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!Object.values(WorkspaceHealthMode).includes(value as any)) { - throw new Error(`Invalid mode ${value}`); - } - - return value as WorkspaceHealthMode; - } - - @Option({ - flags: '-d, --dry-run', - description: 'Dry run without applying changes', - required: false, - }) - dryRun(): boolean { - return true; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/abstract-workspace.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/abstract-workspace.fixer.ts deleted file mode 100644 index 65a42045248..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/abstract-workspace.fixer.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { type EntityManager } from 'typeorm'; - -import { - type WorkspaceHealthIssue, - type WorkspaceHealthIssueType, - type WorkspaceIssueTypeToInterface, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; - -export class CompareEntity { - current: T | null; - altered: T | null; -} - -export abstract class AbstractWorkspaceFixer< - IssueTypes extends WorkspaceHealthIssueType, - UpdateRecordEntities = unknown, -> { - private issueTypes: IssueTypes[]; - - protected constructor(...issueTypes: IssueTypes[]) { - this.issueTypes = issueTypes; - } - - filterIssues( - issues: WorkspaceHealthIssue[], - ): WorkspaceIssueTypeToInterface[] { - return issues.filter( - (issue): issue is WorkspaceIssueTypeToInterface => - this.issueTypes.includes(issue.type as IssueTypes), - ); - } - - protected splitIssuesByType( - issues: WorkspaceIssueTypeToInterface[], - ): Record[]> { - return issues.reduce( - ( - acc: Record[]>, - issue, - ) => { - const type = issue.type as IssueTypes; - - if (!acc[type]) { - acc[type] = []; - } - acc[type].push(issue); - - return acc; - }, - {} as Record[]>, - ); - } - - async createWorkspaceMigrations?( - manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceIssueTypeToInterface[], - ): Promise[]>; - - async createMetadataUpdates?( - manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceIssueTypeToInterface[], - ): Promise[]>; -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/index.ts deleted file mode 100644 index ad32c60fc3d..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { WorkspaceMissingColumnFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer'; - -import { WorkspaceNullableFixer } from './workspace-nullable.fixer'; -import { WorkspaceDefaultValueFixer } from './workspace-default-value.fixer'; -import { WorkspaceTypeFixer } from './workspace-type.fixer'; - -export const workspaceFixers = [ - WorkspaceNullableFixer, - WorkspaceDefaultValueFixer, - WorkspaceTypeFixer, - WorkspaceMissingColumnFixer, -]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-default-value.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-default-value.fixer.ts deleted file mode 100644 index 3e3514a2dbe..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-default-value.fixer.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type EntityManager } from 'typeorm'; - -import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; -import { - type WorkspaceHealthColumnIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; - -import { - type FieldMetadataDefaultValueFunctionNames, - fieldMetadataDefaultValueFunctionName, -} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { WorkspaceMigrationFieldFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; - -import { - AbstractWorkspaceFixer, - type CompareEntity, -} from './abstract-workspace.fixer'; - -type WorkspaceDefaultValueFixerType = - | WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT - | WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID; - -@Injectable() -export class WorkspaceDefaultValueFixer extends AbstractWorkspaceFixer { - constructor( - private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, - ) { - super( - WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT, - WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - ); - } - - async createWorkspaceMigrations( - _manager: EntityManager, - _objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - const splittedIssues = this.splitIssuesByType(issues); - const issueNeedingMigration = - splittedIssues[WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT] ?? - []; - - return this.fixColumnDefaultValueConflictIssues( - _objectMetadataCollection, - issueNeedingMigration as WorkspaceHealthColumnIssue[], - ); - } - - async createMetadataUpdates( - _manager: EntityManager, - _objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - - const splittedIssues = this.splitIssuesByType(issues); - const issueNeedingMetadataUpdate = - splittedIssues[WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID] ?? - []; - - return this.fixColumnDefaultValueNotValidIssues( - _manager, - issueNeedingMetadataUpdate as WorkspaceHealthColumnIssue[], - ); - } - - private async fixColumnDefaultValueConflictIssues( - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const fieldMetadataUpdateCollection = issues.map((issue) => { - const oldDefaultValue = - this.computeFieldMetadataDefaultValueFromColumnDefault( - issue.columnStructure?.columnDefault, - ); - - return { - current: { - ...issue.fieldMetadata, - defaultValue: oldDefaultValue, - }, - altered: issue.fieldMetadata, - }; - }); - - return this.workspaceMigrationFieldFactory.create( - objectMetadataCollection, - fieldMetadataUpdateCollection, - WorkspaceMigrationBuilderAction.UPDATE, - ); - } - - private async fixColumnDefaultValueNotValidIssues( - manager: EntityManager, - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const fieldMetadataRepository = manager.getRepository(FieldMetadataEntity); - const updatedEntities: CompareEntity[] = []; - - for (const issue of issues) { - const currentDefaultValue: - | FieldMetadataDefaultValue - // Old format for default values - // TODO: Remove this after all workspaces are migrated - | { type: FieldMetadataDefaultValueFunctionNames } - | null = issue.fieldMetadata.defaultValue; - let alteredDefaultValue: FieldMetadataDefaultValue | null = null; - - // Check if it's an old function default value - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - if (currentDefaultValue && 'type' in currentDefaultValue) { - alteredDefaultValue = - currentDefaultValue.type as FieldMetadataDefaultValueFunctionNames; - } - - // Check if it's an old string default value - if (currentDefaultValue) { - for (const key of Object.keys(currentDefaultValue)) { - if (key === 'type') { - continue; - } - - // @ts-expect-error legacy noImplicitAny - const value = currentDefaultValue[key]; - - const newValue = - typeof value === 'string' && - !value.startsWith("'") && - !Object.values(fieldMetadataDefaultValueFunctionName).includes( - value as FieldMetadataDefaultValueFunctionNames, - ) - ? `'${value}'` - : value; - - alteredDefaultValue = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...(currentDefaultValue as any), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...(alteredDefaultValue as any), - [key]: newValue, - }; - } - } - - // Old formart default values - if ( - alteredDefaultValue && - typeof alteredDefaultValue === 'object' && - 'value' in alteredDefaultValue - ) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - alteredDefaultValue = alteredDefaultValue.value; - } - - if (alteredDefaultValue === null) { - continue; - } - - await fieldMetadataRepository.update(issue.fieldMetadata.id, { - defaultValue: alteredDefaultValue, - }); - const alteredEntity = await fieldMetadataRepository.findOne({ - where: { - id: issue.fieldMetadata.id, - }, - }); - - updatedEntities.push({ - current: issue.fieldMetadata, - altered: alteredEntity as FieldMetadataEntity | null, - }); - } - - return updatedEntities; - } - - private computeFieldMetadataDefaultValueFromColumnDefault( - columnDefault: string | undefined, - ): FieldMetadataDefaultValue { - if ( - columnDefault === undefined || - columnDefault === null || - columnDefault === 'NULL' - ) { - return null; - } - - if (!isNaN(Number(columnDefault))) { - return +columnDefault; - } - - if (columnDefault === 'true') { - return true; - } - - if (columnDefault === 'false') { - return false; - } - - if (columnDefault === '') { - return "''"; - } - - if (columnDefault === 'now()') { - return 'now'; - } - - if (columnDefault.startsWith('public.uuid_generate_v4')) { - return 'uuid'; - } - - return columnDefault; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer.ts deleted file mode 100644 index 15d8783ad7b..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type EntityManager } from 'typeorm'; - -import { - type WorkspaceHealthColumnIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { - type FieldMetadataUpdate, - WorkspaceMigrationFieldFactory, -} from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; - -import { AbstractWorkspaceFixer } from './abstract-workspace.fixer'; - -@Injectable() -export class WorkspaceMissingColumnFixer extends AbstractWorkspaceFixer { - constructor( - private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, - ) { - super(WorkspaceHealthIssueType.MISSING_COLUMN); - } - - async createWorkspaceMigrations( - _manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - - return this.fixMissingColumnIssues(objectMetadataCollection, issues); - } - - private async fixMissingColumnIssues( - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const fieldMetadataUpdateCollection: FieldMetadataUpdate[] = []; - - for (const issue of issues) { - if (!issue.columnStructures) { - continue; - } - - /** - * Check if the column is prefixed with an underscore as it was the old convention - */ - const oldColumnName = `_${issue.fieldMetadata.name}`; - const oldColumnStructure = issue.columnStructures.find( - (columnStructure) => columnStructure.columnName === oldColumnName, - ); - - if (!oldColumnStructure) { - continue; - } - - fieldMetadataUpdateCollection.push({ - current: { - ...issue.fieldMetadata, - name: oldColumnName, - }, - altered: issue.fieldMetadata, - }); - } - - if (fieldMetadataUpdateCollection.length <= 0) { - return []; - } - - return this.workspaceMigrationFieldFactory.create( - objectMetadataCollection, - fieldMetadataUpdateCollection, - WorkspaceMigrationBuilderAction.UPDATE, - ); - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-nullable.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-nullable.fixer.ts deleted file mode 100644 index e5bbc514f19..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-nullable.fixer.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type EntityManager } from 'typeorm'; - -import { - type WorkspaceHealthColumnIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { WorkspaceMigrationFieldFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; - -import { AbstractWorkspaceFixer } from './abstract-workspace.fixer'; - -@Injectable() -export class WorkspaceNullableFixer extends AbstractWorkspaceFixer { - constructor( - private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, - ) { - super(WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT); - } - - async createWorkspaceMigrations( - _manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - - return this.fixColumnNullabilityIssues(objectMetadataCollection, issues); - } - - private async fixColumnNullabilityIssues( - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const fieldMetadataUpdateCollection = issues.map((issue) => { - return { - current: { - ...issue.fieldMetadata, - isNullable: issue.columnStructure?.isNullable ?? false, - }, - altered: issue.fieldMetadata, - }; - }); - - return this.workspaceMigrationFieldFactory.create( - objectMetadataCollection, - fieldMetadataUpdateCollection, - WorkspaceMigrationBuilderAction.UPDATE, - ); - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-type.fixer.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-type.fixer.ts deleted file mode 100644 index 927747a6abf..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/fixer/workspace-type.fixer.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { type EntityManager } from 'typeorm'; - -import { - type WorkspaceHealthColumnIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; -import { - type FieldMetadataUpdate, - WorkspaceMigrationFieldFactory, -} from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory'; - -import { AbstractWorkspaceFixer } from './abstract-workspace.fixer'; - -const oldDataTypes = ['integer']; - -@Injectable() -export class WorkspaceTypeFixer extends AbstractWorkspaceFixer { - private readonly logger = new Logger(WorkspaceTypeFixer.name); - - constructor( - private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, - private readonly databaseStructureService: DatabaseStructureService, - ) { - super(WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT); - } - - async createWorkspaceMigrations( - _manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - if (issues.length <= 0) { - return []; - } - - return this.fixColumnTypeIssues(objectMetadataCollection, issues); - } - - private async fixColumnTypeIssues( - objectMetadataCollection: ObjectMetadataEntity[], - issues: WorkspaceHealthColumnIssue[], - ): Promise[]> { - const fieldMetadataUpdateCollection: FieldMetadataUpdate[] = []; - - for (const issue of issues) { - const dataType = issue.columnStructure?.dataType; - - if (!dataType) { - throw new Error('Column structure data type is missing'); - } - - const type = - this.databaseStructureService.getFieldMetadataTypeFromPostgresDataType( - dataType, - ); - - if (oldDataTypes.includes(dataType)) { - this.logger.warn( - `Old data type detected for column ${issue.columnStructure?.columnName} with data type ${dataType}. Please update the column data type manually.`, - ); - continue; - } - - if (!type) { - throw new Error("Can't find field metadata type from column structure"); - } - - fieldMetadataUpdateCollection.push({ - current: { - ...issue.fieldMetadata, - type, - }, - altered: issue.fieldMetadata, - }); - } - - return this.workspaceMigrationFieldFactory.create( - objectMetadataCollection, - fieldMetadataUpdateCollection, - WorkspaceMigrationBuilderAction.UPDATE, - ); - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface.ts deleted file mode 100644 index 907c02f2eed..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum WorkspaceHealthFixKind { - Nullable = 'nullable', - Type = 'type', - DefaultValue = 'default-value', - MissingColumn = 'missing-column', -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface.ts deleted file mode 100644 index 77b50d59c8d..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { type WorkspaceTableStructure } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; - -export enum WorkspaceHealthIssueType { - MISSING_TABLE = 'MISSING_TABLE', - TABLE_NAME_SHOULD_BE_CUSTOM = 'TABLE_NAME_SHOULD_BE_CUSTOM', - TABLE_TARGET_TABLE_NAME_NOT_VALID = 'TABLE_TARGET_TABLE_NAME_NOT_VALID', - TABLE_DATA_SOURCE_ID_NOT_VALID = 'TABLE_DATA_SOURCE_ID_NOT_VALID', - TABLE_NAME_NOT_VALID = 'TABLE_NAME_NOT_VALID', - MISSING_COLUMN = 'MISSING_COLUMN', - MISSING_INDEX = 'MISSING_INDEX', - MISSING_FOREIGN_KEY = 'MISSING_FOREIGN_KEY', - MISSING_COMPOSITE_TYPE = 'MISSING_COMPOSITE_TYPE', - COLUMN_NAME_SHOULD_NOT_BE_PREFIXED = 'COLUMN_NAME_SHOULD_NOT_BE_PREFIXED', - COLUMN_NAME_SHOULD_NOT_BE_CUSTOM = 'COLUMN_NAME_SHOULD_NOT_BE_CUSTOM', - COLUMN_OBJECT_REFERENCE_INVALID = 'COLUMN_OBJECT_REFERENCE_INVALID', - COLUMN_NAME_NOT_VALID = 'COLUMN_NAME_NOT_VALID', - COLUMN_TYPE_NOT_VALID = 'COLUMN_TYPE_NOT_VALID', - COLUMN_DATA_TYPE_CONFLICT = 'COLUMN_DATA_TYPE_CONFLICT', - COLUMN_NULLABILITY_CONFLICT = 'COLUMN_NULLABILITY_CONFLICT', - COLUMN_DEFAULT_VALUE_CONFLICT = 'COLUMN_DEFAULT_VALUE_CONFLICT', - COLUMN_DEFAULT_VALUE_NOT_VALID = 'COLUMN_DEFAULT_VALUE_NOT_VALID', - COLUMN_OPTIONS_NOT_VALID = 'COLUMN_OPTIONS_NOT_VALID', - RELATION_METADATA_NOT_VALID = 'RELATION_METADATA_NOT_VALID', - RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID = 'RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID', - RELATION_FOREIGN_KEY_NOT_VALID = 'RELATION_FOREIGN_KEY_NOT_VALID', - RELATION_FOREIGN_KEY_CONFLICT = 'RELATION_FOREIGN_KEY_CONFLICT', - RELATION_FOREIGN_KEY_ON_DELETE_ACTION_CONFLICT = 'RELATION_FOREIGN_KEY_ON_DELETE_ACTION_CONFLICT', - RELATION_TYPE_NOT_VALID = 'RELATION_TYPE_NOT_VALID', -} - -/** - * Table issues - */ -export type WorkspaceTableIssueTypes = - | WorkspaceHealthIssueType.MISSING_TABLE - | WorkspaceHealthIssueType.TABLE_NAME_SHOULD_BE_CUSTOM - | WorkspaceHealthIssueType.TABLE_TARGET_TABLE_NAME_NOT_VALID - | WorkspaceHealthIssueType.TABLE_DATA_SOURCE_ID_NOT_VALID - | WorkspaceHealthIssueType.TABLE_NAME_NOT_VALID; - -export interface WorkspaceHealthTableIssue { - type: T; - objectMetadata: ObjectMetadataEntity; - message: string; -} - -/** - * Column issues - */ -export type WorkspaceColumnIssueTypes = - | WorkspaceHealthIssueType.MISSING_COLUMN - | WorkspaceHealthIssueType.MISSING_INDEX - | WorkspaceHealthIssueType.MISSING_FOREIGN_KEY - | WorkspaceHealthIssueType.MISSING_COMPOSITE_TYPE - | WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_PREFIXED - | WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_CUSTOM - | WorkspaceHealthIssueType.COLUMN_OBJECT_REFERENCE_INVALID - | WorkspaceHealthIssueType.COLUMN_NAME_NOT_VALID - | WorkspaceHealthIssueType.COLUMN_TYPE_NOT_VALID - | WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT - | WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT - | WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT - | WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID - | WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID; - -export interface WorkspaceHealthColumnIssue< - T extends WorkspaceColumnIssueTypes, -> { - type: T; - fieldMetadata: FieldMetadataEntity; - columnStructure?: WorkspaceTableStructure; - columnStructures?: WorkspaceTableStructure[]; - message: string; -} - -/** - * Relation issues - */ -export type WorkspaceRelationIssueTypes = - | WorkspaceHealthIssueType.RELATION_METADATA_NOT_VALID - | WorkspaceHealthIssueType.RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID - | WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_NOT_VALID - | WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_CONFLICT - | WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_ON_DELETE_ACTION_CONFLICT - | WorkspaceHealthIssueType.RELATION_TYPE_NOT_VALID; - -/** - * Get the interface for the issue type - */ -export type WorkspaceIssueTypeToInterface = - T extends WorkspaceTableIssueTypes - ? WorkspaceHealthTableIssue - : T extends WorkspaceColumnIssueTypes - ? WorkspaceHealthColumnIssue - : never; - -/** - * Union of all issues - */ -export type WorkspaceHealthIssue = - WorkspaceIssueTypeToInterface; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface.ts deleted file mode 100644 index afe21015676..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum WorkspaceHealthMode { - Structure = 'structure', - Metadata = 'metadata', - All = 'all', -} - -export interface WorkspaceHealthOptions { - mode: WorkspaceHealthMode; -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface.ts deleted file mode 100644 index 543db747ff4..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface WorkspaceTableStructure { - tableSchema: string; - tableName: string; - columnName: string; - dataType: string; - columnDefault: string; - isNullable: boolean; - isUnique: boolean; - isPrimaryKey: boolean; - isForeignKey: boolean; - isArray: boolean; - onUpdateAction: string; - onDeleteAction: string; -} - -export type WorkspaceTableStructureResult = { - [P in keyof WorkspaceTableStructure]: string; -}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts deleted file mode 100644 index 26e330b68cb..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/database-structure.service.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; - -import { - FieldMetadataType, - compositeTypeDefinitions, -} from 'twenty-shared/types'; -import { DataSource, type ColumnType } from 'typeorm'; -import { type ColumnMetadata } from 'typeorm/metadata/ColumnMetadata'; - -import { - type FieldMetadataDefaultValue, - type FieldMetadataFunctionDefaultValue, -} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; -import { - type WorkspaceTableStructure, - type WorkspaceTableStructureResult, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface'; - -import { type FieldMetadataDefaultValueFunctionNames } from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { isFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util'; -import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util'; -import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; - -@Injectable() -export class DatabaseStructureService { - constructor( - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) {} - - async getWorkspaceTableColumns( - schemaName: string, - tableName: string, - ): Promise { - const results = await this.coreDataSource.query< - WorkspaceTableStructureResult[] - >(` - WITH foreign_keys AS ( - SELECT - kcu.table_schema AS schema_name, - kcu.table_name AS table_name, - kcu.column_name AS column_name, - tc.constraint_name AS constraint_name - FROM - information_schema.key_column_usage AS kcu - JOIN - information_schema.table_constraints AS tc - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - WHERE - tc.constraint_type = 'FOREIGN KEY' - AND tc.table_schema = '${schemaName}' - AND tc.table_name = '${tableName}' - ), - unique_constraints AS ( - SELECT - tc.table_schema AS schema_name, - tc.table_name AS table_name, - kcu.column_name AS column_name - FROM - information_schema.key_column_usage AS kcu - JOIN - information_schema.table_constraints AS tc - ON tc.constraint_name = kcu.constraint_name - AND tc.table_schema = kcu.table_schema - WHERE - tc.constraint_type = 'UNIQUE' - AND tc.table_schema = '${schemaName}' - AND tc.table_name = '${tableName}' - ) - SELECT - c.table_schema AS "tableSchema", - c.table_name AS "tableName", - c.column_name AS "columnName", - CASE - WHEN c.data_type = 'ARRAY' THEN - (SELECT typname FROM pg_type WHERE oid = t.typelem) - WHEN c.data_type = 'USER-DEFINED' THEN - c.udt_name - ELSE - c.data_type - END AS "dataType", - c.is_nullable AS "isNullable", - c.column_default AS "columnDefault", - CASE - WHEN pk.constraint_type = 'PRIMARY KEY' THEN 'TRUE' - ELSE 'FALSE' - END AS "isPrimaryKey", - CASE - WHEN fk.constraint_name IS NOT NULL THEN 'TRUE' - ELSE 'FALSE' - END AS "isForeignKey", - CASE - WHEN uc.column_name IS NOT NULL THEN 'TRUE' - ELSE 'FALSE' - END AS "isUnique", - CASE - WHEN c.data_type = 'ARRAY' THEN 'TRUE' - ELSE 'FALSE' - END AS "isArray", - rc.update_rule AS "onUpdateAction", - rc.delete_rule AS "onDeleteAction" - FROM - information_schema.columns AS c - LEFT JOIN pg_type t ON t.typname = c.udt_name - LEFT JOIN - information_schema.constraint_column_usage AS ccu - ON c.column_name = ccu.column_name - AND c.table_name = ccu.table_name - AND c.table_schema = ccu.table_schema - LEFT JOIN - information_schema.table_constraints AS pk - ON pk.constraint_name = ccu.constraint_name - AND pk.constraint_type = 'PRIMARY KEY' - AND pk.table_name = c.table_name - AND pk.table_schema = c.table_schema - LEFT JOIN - foreign_keys AS fk - ON c.table_schema = fk.schema_name - AND c.table_name = fk.table_name - AND c.column_name = fk.column_name - LEFT JOIN - unique_constraints AS uc - ON c.table_schema = uc.schema_name - AND c.table_name = uc.table_name - AND c.column_name = uc.column_name - LEFT JOIN - information_schema.referential_constraints AS rc - ON rc.constraint_name = fk.constraint_name - AND rc.constraint_schema = '${schemaName}' - WHERE - c.table_schema = '${schemaName}' - AND c.table_name = '${tableName}'; - `); - - if (!results || results.length === 0) { - return []; - } - - return results.map((item) => ({ - ...item, - dataType: item.isArray === 'TRUE' ? `${item.dataType}[]` : item.dataType, - isNullable: item.isNullable === 'YES', - isPrimaryKey: item.isPrimaryKey === 'TRUE', - isForeignKey: item.isForeignKey === 'TRUE', - isUnique: item.isUnique === 'TRUE', - isArray: item.isArray === 'TRUE', - })); - } - - getPostgresDataTypes(fieldMetadata: FieldMetadataEntity): string[] { - const normalizer = ( - type: FieldMetadataType, - isArray: boolean | undefined, - columnName: string, - ) => { - const typeORMType = fieldMetadataTypeToColumnType(type); - - // Compute enum name to compare data type properly - if (typeORMType === 'enum') { - const objectName = computeObjectTargetTable(fieldMetadata.object); - - return `${objectName}_${columnName}_enum${isArray ? '[]' : ''}`; - } - - return this.coreDataSource.driver.normalizeType({ - type: typeORMType, - }); - }; - - if (isCompositeFieldMetadataType(fieldMetadata.type)) { - const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); - - if (!compositeType) { - throw new Error( - `Composite type definition not found for ${fieldMetadata.type}`, - ); - } - - return compositeType.properties.map((compositeProperty) => - normalizer( - compositeProperty.type, - compositeProperty.isArray, - compositeProperty.name, - ), - ); - } - - return [ - normalizer( - fieldMetadata.type, - fieldMetadata.type === FieldMetadataType.MULTI_SELECT, - fieldMetadata.name, - ), - ]; - } - - getFieldMetadataTypeFromPostgresDataType( - postgresDataType: string, - ): FieldMetadataType | null { - const types = Object.values(FieldMetadataType).filter((type) => { - // We're skipping composite and relation types, as they're not directly mapped to a column type - if (isCompositeFieldMetadataType(type)) { - return false; - } - - if (isMorphOrRelationFieldMetadataType(type)) { - return false; - } - - return true; - }); - - for (const type of types) { - const typeORMType = fieldMetadataTypeToColumnType( - FieldMetadataType[type], - ) as ColumnType; - const dataType = this.coreDataSource.driver.normalizeType({ - type: typeORMType, - }); - - if (postgresDataType === dataType) { - return FieldMetadataType[type]; - } - } - - return null; - } - - getPostgresDefaults( - fieldMetadataType: FieldMetadataType, - initialDefaultValue: - | FieldMetadataDefaultValue - // Old format for default values - // TODO: Should be removed once all default values are migrated - | { type: FieldMetadataDefaultValueFunctionNames } - | null, - ): (string | null | undefined)[] { - const normalizer = ( - type: FieldMetadataType, - defaultValue: - | FieldMetadataDefaultValue - | { type: FieldMetadataDefaultValueFunctionNames } - | null, - ) => { - const typeORMType = fieldMetadataTypeToColumnType(type) as ColumnType; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let value: any = - // Old formart default values - defaultValue && - typeof defaultValue === 'object' && - 'value' in defaultValue - ? defaultValue.value - : defaultValue; - - // Old format for default values - // TODO: Should be removed once all default values are migrated - if ( - defaultValue && - typeof defaultValue === 'object' && - 'type' in defaultValue - ) { - return this.computeFunctionDefaultValue(defaultValue.type); - } - - if (isFunctionDefaultValue(value)) { - return this.computeFunctionDefaultValue(value); - } - - if (typeof value === 'number') { - return value.toString(); - } - - // Remove leading and trailing single quotes for string default values as it's already handled by TypeORM - if (typeof value === 'string' && value.match(/^'.*'$/)) { - value = value.replace(/^'/, '').replace(/'$/, ''); - } - - return this.coreDataSource.driver.normalizeDefault({ - type: typeORMType, - default: value, - isArray: false, - // Workaround to use normalizeDefault without a complete ColumnMetadata object - } as ColumnMetadata); - }; - - if (isCompositeFieldMetadataType(fieldMetadataType)) { - const compositeType = compositeTypeDefinitions.get(fieldMetadataType); - - if (!compositeType) { - throw new Error( - `Composite type definition not found for ${fieldMetadataType}`, - ); - } - - return compositeType.properties.map((compositeProperty) => - normalizer( - compositeProperty.type, - typeof initialDefaultValue === 'object' - ? // @ts-expect-error legacy noImplicitAny - initialDefaultValue?.[compositeProperty.name] - : null, - ), - ); - } - - return [normalizer(fieldMetadataType, initialDefaultValue)]; - } - - private computeFunctionDefaultValue( - value: FieldMetadataFunctionDefaultValue, - ) { - const serializedDefaultValue = serializeFunctionDefaultValue(value); - - // Special case for uuid_generate_v4() default value - if (serializedDefaultValue === 'public.uuid_generate_v4()') { - return 'uuid_generate_v4()'; - } - - return serializedDefaultValue; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts deleted file mode 100644 index 816d99bae38..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { - FieldMetadataType, - compositeTypeDefinitions, -} from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { - type WorkspaceHealthIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { type WorkspaceHealthOptions } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface'; -import { type WorkspaceTableStructure } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-table-definition.interface'; - -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { - computeColumnName, - computeCompositeColumnName, -} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; -import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; -import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util'; -import { validateOptionsForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-options-for-type.util'; -import { customNamePrefix } from 'src/engine/utils/compute-table-name.util'; -import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util'; -import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; -import { validName } from 'src/engine/workspace-manager/workspace-health/utils/valid-name.util'; - -@Injectable() -export class FieldMetadataHealthService { - constructor( - private readonly databaseStructureService: DatabaseStructureService, - ) {} - - async healthCheck( - tableName: string, - workspaceTableColumns: WorkspaceTableStructure[], - fieldMetadataCollection: FieldMetadataEntity[], - options: WorkspaceHealthOptions, - ): Promise { - const issues: WorkspaceHealthIssue[] = []; - - for (const fieldMetadata of fieldMetadataCollection) { - // Relation metadata are checked in another service - if (isMorphOrRelationFieldMetadataType(fieldMetadata.type)) { - continue; - } - - const fieldIssues = await this.healthCheckField( - tableName, - workspaceTableColumns, - fieldMetadata, - options, - ); - - issues.push(...fieldIssues); - } - - return issues; - } - - private async healthCheckField( - tableName: string, - workspaceTableColumns: WorkspaceTableStructure[], - fieldMetadata: FieldMetadataEntity, - options: WorkspaceHealthOptions, - ): Promise { - const issues: WorkspaceHealthIssue[] = []; - - if (options.mode === 'structure' || options.mode === 'all') { - const structureIssues = this.structureFieldCheck( - tableName, - workspaceTableColumns, - fieldMetadata, - ); - - issues.push(...structureIssues); - } - - if (options.mode === 'metadata' || options.mode === 'all') { - const metadataIssues = this.metadataFieldCheck(tableName, fieldMetadata); - - issues.push(...metadataIssues); - } - - return issues; - } - - private structureFieldCheck( - tableName: string, - workspaceTableColumns: WorkspaceTableStructure[], - fieldMetadata: FieldMetadataEntity, - ): WorkspaceHealthIssue[] { - const dataTypes = - this.databaseStructureService.getPostgresDataTypes(fieldMetadata); - const issues: WorkspaceHealthIssue[] = []; - let columnNames: string[] = []; - - if (isCompositeFieldMetadataType(fieldMetadata.type)) { - const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); - - if (!compositeType) { - throw new Error(`Composite type ${fieldMetadata.type} is not defined`); - } - - columnNames = compositeType.properties.map((compositeProperty) => - computeCompositeColumnName(fieldMetadata, compositeProperty), - ); - } else { - columnNames = [computeColumnName(fieldMetadata)]; - } - - const defaultValues = this.databaseStructureService.getPostgresDefaults( - fieldMetadata.type, - fieldMetadata.defaultValue, - ); - - // Check if column exist in database - const columnStructureMap = workspaceTableColumns.reduce( - (acc, workspaceTableColumn) => { - const columnName = workspaceTableColumn.columnName; - - if (columnNames.includes(columnName)) { - acc[columnName] = workspaceTableColumn; - } - - return acc; - }, - {} as { [key: string]: WorkspaceTableStructure }, - ); - - for (const [index, columnName] of columnNames.entries()) { - const columnStructure = columnStructureMap[columnName]; - - if (!columnStructure) { - issues.push({ - type: WorkspaceHealthIssueType.MISSING_COLUMN, - fieldMetadata, - columnStructures: workspaceTableColumns, - message: `Column ${columnName} not found in table ${tableName}`, - }); - - continue; - } - - const columnDefaultValue = - columnStructure.columnDefault?.split('::')?.[0]; - - // Check if column data type is the same - if (!dataTypes[index] || columnStructure.dataType !== dataTypes[index]) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT, - fieldMetadata, - columnStructure, - message: `Column ${columnName} type is not the same as the field metadata type "${columnStructure.dataType}" !== "${dataTypes[index]}"`, - }); - } - - if (columnStructure.isNullable !== fieldMetadata.isNullable) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT, - fieldMetadata, - columnStructure, - message: `Column ${columnName} is expected to be ${ - fieldMetadata.isNullable ? 'nullable' : 'not nullable' - } but is ${columnStructure.isNullable ? 'nullable' : 'not nullable'}`, - }); - } - - if (columnDefaultValue && isEnumFieldMetadataType(fieldMetadata.type)) { - const enumValues = fieldMetadata.options?.map((option) => - serializeDefaultValue(`'${option.value}'`), - ); - - if (!isDefined(enumValues)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID, - fieldMetadata, - columnStructure, - message: `Column options of ${fieldMetadata.name} are not defined`, - }); - } - - if (isDefined(enumValues) && !enumValues.includes(columnDefaultValue)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - fieldMetadata, - columnStructure, - message: `Column ${columnName} default value is not in the enum values "${columnDefaultValue}" NOT IN "${enumValues}"`, - }); - } - } - - if (columnDefaultValue !== defaultValues[index]) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT, - fieldMetadata, - columnStructure, - message: `Column ${columnName} default value is not the same as the field metadata default value "${columnStructure.columnDefault}" !== "${defaultValues[index]}"`, - }); - } - } - - return issues; - } - - private metadataFieldCheck( - tableName: string, - fieldMetadata: FieldMetadataEntity, - ): WorkspaceHealthIssue[] { - const issues: WorkspaceHealthIssue[] = []; - const columnName = fieldMetadata.name; - const defaultValueIssues = this.defaultValueHealthCheck(fieldMetadata); - - if (fieldMetadata.name.startsWith(customNamePrefix)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_PREFIXED, - fieldMetadata, - message: `Column ${columnName} should not be prefixed with "${customNamePrefix}"`, - }); - } - - if (fieldMetadata.isCustom && columnName?.startsWith(customNamePrefix)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_NAME_SHOULD_NOT_BE_CUSTOM, - fieldMetadata, - message: `Column ${columnName} is marked as custom in table ${tableName} but and start with "_", this behavior has been removed. Please remove the prefix.`, - }); - } - - if (!fieldMetadata.objectMetadataId) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_OBJECT_REFERENCE_INVALID, - fieldMetadata, - message: `Column ${columnName} doesn't have a valid object metadata id`, - }); - } - - if (!Object.values(FieldMetadataType).includes(fieldMetadata.type)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_TYPE_NOT_VALID, - fieldMetadata, - message: `Column ${columnName} doesn't have a valid field metadata type`, - }); - } - - if ( - !fieldMetadata.name || - !validName(fieldMetadata.name) || - !fieldMetadata.label - ) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_NAME_NOT_VALID, - fieldMetadata, - message: `Column ${columnName} doesn't have a valid name or label`, - }); - } - - if (!isDefined(fieldMetadata.options)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID, - fieldMetadata, - message: `Column options of ${fieldMetadata.name} are not defined`, - }); - } - - if ( - isEnumFieldMetadataType(fieldMetadata.type) && - isDefined(fieldMetadata.options) && - !validateOptionsForType(fieldMetadata.type, fieldMetadata.options) - ) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID, - fieldMetadata, - message: `Column options of ${fieldMetadata.name} is not valid`, - }); - } - - issues.push(...defaultValueIssues); - - return issues; - } - - private defaultValueHealthCheck( - fieldMetadata: FieldMetadataEntity, - ): WorkspaceHealthIssue[] { - const issues: WorkspaceHealthIssue[] = []; - - if ( - !validateDefaultValueForType( - fieldMetadata.type, - fieldMetadata.defaultValue, - ).isValid - ) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - fieldMetadata, - message: `Column default value for composite type ${fieldMetadata.type} is not well structured`, - }); - } - - if ( - isEnumFieldMetadataType(fieldMetadata.type) && - fieldMetadata.defaultValue - ) { - const enumValues = fieldMetadata.options?.map((option) => - serializeDefaultValue(`'${option.value}'`), - ); - const metadataDefaultValue = fieldMetadata.defaultValue; - - if (isDefined(metadataDefaultValue)) { - if (fieldMetadata.type === FieldMetadataType.MULTI_SELECT) { - if (!Array.isArray(metadataDefaultValue)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - fieldMetadata, - message: `Column default value for multi-select must be an array, got "${metadataDefaultValue}"`, - }); - } else { - metadataDefaultValue.forEach((value) => { - if (isDefined(enumValues) && !enumValues.includes(value)) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - fieldMetadata, - message: `Column default value "${value}" is not in the enum values "${enumValues}"`, - }); - } - }); - } - } else if ( - isDefined(enumValues) && - !enumValues.includes(metadataDefaultValue as string) - ) { - issues.push({ - type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, - fieldMetadata, - message: `Column default value is not in the enum values "${metadataDefaultValue}" NOT IN "${enumValues}"`, - }); - } - } - } - - return issues; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/object-metadata-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/object-metadata-health.service.ts deleted file mode 100644 index db52bba3d1f..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/object-metadata-health.service.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; - -import { DataSource } from 'typeorm'; - -import { - type WorkspaceHealthIssue, - WorkspaceHealthIssueType, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { type WorkspaceHealthOptions } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface'; - -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { validName } from 'src/engine/workspace-manager/workspace-health/utils/valid-name.util'; - -@Injectable() -export class ObjectMetadataHealthService { - constructor( - @InjectDataSource() - private readonly coreDataSource: DataSource, - ) {} - - async healthCheck( - schemaName: string, - objectMetadata: ObjectMetadataEntity, - options: WorkspaceHealthOptions, - ): Promise { - const issues: WorkspaceHealthIssue[] = []; - - if (options.mode === 'structure' || options.mode === 'all') { - const structureIssues = await this.structureObjectCheck( - schemaName, - objectMetadata, - ); - - issues.push(...structureIssues); - } - - if (options.mode === 'metadata' || options.mode === 'all') { - const metadataIssues = this.metadataObjectCheck(objectMetadata); - - issues.push(...metadataIssues); - } - - return issues; - } - - /** - * Check the structure health of the table based on metadata - * @param schemaName - * @param objectMetadata - * @returns WorkspaceHealthIssue[] - */ - private async structureObjectCheck( - schemaName: string, - objectMetadata: ObjectMetadataEntity, - ): Promise { - const issues: WorkspaceHealthIssue[] = []; - - // Check if the table exist in database - const tableExist = await this.coreDataSource.query( - `SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = '${schemaName}' - AND table_name = '${computeObjectTargetTable(objectMetadata)}')`, - ); - - if (!tableExist) { - issues.push({ - type: WorkspaceHealthIssueType.MISSING_TABLE, - objectMetadata, - message: `Table ${computeObjectTargetTable( - objectMetadata, - )} not found in schema ${schemaName}`, - }); - - return issues; - } - - return issues; - } - - /** - * Check ObjectMetadata health - * @param objectMetadata - * @returns WorkspaceHealthIssue[] - */ - private metadataObjectCheck( - objectMetadata: ObjectMetadataEntity, - ): WorkspaceHealthIssue[] { - const issues: WorkspaceHealthIssue[] = []; - - if (!objectMetadata.dataSourceId) { - issues.push({ - type: WorkspaceHealthIssueType.TABLE_DATA_SOURCE_ID_NOT_VALID, - objectMetadata, - message: `Table ${computeObjectTargetTable( - objectMetadata, - )} doesn't have a data source`, - }); - } - - if ( - !objectMetadata.nameSingular || - !objectMetadata.namePlural || - !validName(objectMetadata.nameSingular) || - !validName(objectMetadata.namePlural) || - !objectMetadata.labelSingular || - !objectMetadata.labelPlural - ) { - issues.push({ - type: WorkspaceHealthIssueType.TABLE_NAME_NOT_VALID, - objectMetadata, - message: `Table ${computeObjectTargetTable( - objectMetadata, - )} doesn't have a valid name or label`, - }); - } - - return issues; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/workspace-fix.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/workspace-fix.service.ts deleted file mode 100644 index c0a5f21b5be..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/workspace-fix.service.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type EntityManager } from 'typeorm'; - -import { WorkspaceHealthFixKind } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface'; -import { type WorkspaceHealthIssue } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; - -import { type WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { WorkspaceNullableFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-nullable.fixer'; -import { WorkspaceDefaultValueFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-default-value.fixer'; -import { WorkspaceTypeFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-type.fixer'; -import { type CompareEntity } from 'src/engine/workspace-manager/workspace-health/fixer/abstract-workspace.fixer'; -import { WorkspaceMissingColumnFixer } from 'src/engine/workspace-manager/workspace-health/fixer/workspace-missing-column.fixer'; - -@Injectable() -export class WorkspaceFixService { - constructor( - private readonly workspaceNullableFixer: WorkspaceNullableFixer, - private readonly workspaceDefaultValueFixer: WorkspaceDefaultValueFixer, - private readonly workspaceTypeFixer: WorkspaceTypeFixer, - private readonly workspaceMissingColumnFixer: WorkspaceMissingColumnFixer, - ) {} - - async createWorkspaceMigrations( - manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - type: WorkspaceHealthFixKind, - issues: WorkspaceHealthIssue[], - ): Promise[]> { - switch (type) { - case WorkspaceHealthFixKind.Nullable: { - const filteredIssues = this.workspaceNullableFixer.filterIssues(issues); - - return this.workspaceNullableFixer.createWorkspaceMigrations( - manager, - objectMetadataCollection, - filteredIssues, - ); - } - case WorkspaceHealthFixKind.DefaultValue: { - const filteredIssues = - this.workspaceDefaultValueFixer.filterIssues(issues); - - return this.workspaceDefaultValueFixer.createWorkspaceMigrations( - manager, - objectMetadataCollection, - filteredIssues, - ); - } - case WorkspaceHealthFixKind.Type: { - const filteredIssues = this.workspaceTypeFixer.filterIssues(issues); - - return this.workspaceTypeFixer.createWorkspaceMigrations( - manager, - objectMetadataCollection, - filteredIssues, - ); - } - case WorkspaceHealthFixKind.MissingColumn: { - const filteredIssues = - this.workspaceMissingColumnFixer.filterIssues(issues); - - return this.workspaceMissingColumnFixer.createWorkspaceMigrations( - manager, - objectMetadataCollection, - filteredIssues, - ); - } - default: { - return []; - } - } - } - - async createMetadataUpdates( - manager: EntityManager, - objectMetadataCollection: ObjectMetadataEntity[], - type: WorkspaceHealthFixKind, - - issues: WorkspaceHealthIssue[], - ): Promise[]> { - switch (type) { - case WorkspaceHealthFixKind.DefaultValue: { - const filteredIssues = - this.workspaceDefaultValueFixer.filterIssues(issues); - - return this.workspaceDefaultValueFixer.createMetadataUpdates( - manager, - objectMetadataCollection, - filteredIssues, - ); - } - default: { - return []; - } - } - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/valid-name.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/valid-name.util.ts deleted file mode 100644 index e53fce5d4ea..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/utils/valid-name.util.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const validName = (name: string): boolean => { - return /^[a-zA-Z0-9_]+$/.test(name); -}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.module.ts deleted file mode 100644 index 4cb4db7d2c6..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.module.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; -import { FieldMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/field-metadata-health.service'; -import { ObjectMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/object-metadata-health.service'; -import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service'; -import { WorkspaceMigrationBuilderModule } from 'src/engine/workspace-manager/workspace-migration-builder/workspace-migration-builder.module'; -import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; -import { WorkspaceFixService } from 'src/engine/workspace-manager/workspace-health/services/workspace-fix.service'; - -import { workspaceFixers } from './fixer'; - -@Module({ - imports: [ - DataSourceModule, - TypeORMModule, - ObjectMetadataModule, - WorkspaceDataSourceModule, - WorkspaceMigrationRunnerModule, - WorkspaceMigrationBuilderModule, - ], - providers: [ - ...workspaceFixers, - WorkspaceHealthService, - DatabaseStructureService, - ObjectMetadataHealthService, - FieldMetadataHealthService, - WorkspaceFixService, - ], - exports: [WorkspaceHealthService, DatabaseStructureService], -}) -export class WorkspaceHealthModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts deleted file mode 100644 index b84eeecce17..00000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; - -import { DataSource } from 'typeorm'; - -import { type WorkspaceHealthFixKind } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface'; -import { type WorkspaceHealthIssue } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; -import { - WorkspaceHealthMode, - type WorkspaceHealthOptions, -} from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface'; - -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; -import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; -import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; -import { FieldMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/field-metadata-health.service'; -import { ObjectMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/object-metadata-health.service'; -import { WorkspaceFixService } from 'src/engine/workspace-manager/workspace-health/services/workspace-fix.service'; -import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; - -@Injectable() -export class WorkspaceHealthService { - private readonly logger = new Logger(WorkspaceHealthService.name); - - constructor( - @InjectDataSource() - private readonly coreDataSource: DataSource, - private readonly dataSourceService: DataSourceService, - private readonly objectMetadataService: ObjectMetadataServiceV2, - private readonly databaseStructureService: DatabaseStructureService, - private readonly objectMetadataHealthService: ObjectMetadataHealthService, - private readonly fieldMetadataHealthService: FieldMetadataHealthService, - private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, - private readonly workspaceFixService: WorkspaceFixService, - ) {} - - async healthCheck( - workspaceId: string, - options: WorkspaceHealthOptions = { mode: WorkspaceHealthMode.All }, - ): Promise { - const schemaName = getWorkspaceSchemaName(workspaceId); - const issues: WorkspaceHealthIssue[] = []; - - const dataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( - workspaceId, - ); - - // Check if a data source exists for this workspace - if (!dataSourceMetadata) { - throw new NotFoundException( - `DataSource for workspace id ${workspaceId} not found`, - ); - } - - const objectMetadataCollection = - await this.objectMetadataService.findManyWithinWorkspace(workspaceId); - - // Check if object metadata exists for this workspace - if (!objectMetadataCollection || objectMetadataCollection.length === 0) { - throw new NotFoundException(`Workspace with id ${workspaceId} not found`); - } - - for (const objectMetadata of objectMetadataCollection) { - const tableName = computeObjectTargetTable(objectMetadata); - const workspaceTableColumns = - await this.databaseStructureService.getWorkspaceTableColumns( - schemaName, - tableName, - ); - - if (!workspaceTableColumns || workspaceTableColumns.length === 0) { - throw new NotFoundException( - `Table ${tableName} not found in schema ${schemaName}`, - ); - } - - // Check object metadata health - const objectIssues = await this.objectMetadataHealthService.healthCheck( - schemaName, - objectMetadata, - options, - ); - - issues.push(...objectIssues); - - // Check fields metadata health - const fieldIssues = await this.fieldMetadataHealthService.healthCheck( - computeObjectTargetTable(objectMetadata), - workspaceTableColumns, - objectMetadata.fields, - options, - ); - - issues.push(...fieldIssues); - } - - return issues; - } - - async fixIssues( - workspaceId: string, - issues: WorkspaceHealthIssue[], - options: { - type: WorkspaceHealthFixKind; - applyChanges?: boolean; - }, - ): Promise<{ - workspaceMigrations: Partial[]; - metadataEntities: unknown[]; - }> { - let workspaceMigrations: Partial[] = []; - let metadataEntities: unknown[] = []; - - // Set default options - options.applyChanges ??= true; - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - const manager = queryRunner.manager; - - try { - const workspaceMigrationRepository = manager.getRepository( - WorkspaceMigrationEntity, - ); - const objectMetadataCollection = - await this.objectMetadataService.findManyWithinWorkspace(workspaceId); - - workspaceMigrations = - await this.workspaceFixService.createWorkspaceMigrations( - manager, - objectMetadataCollection, - options.type, - issues, - ); - - metadataEntities = await this.workspaceFixService.createMetadataUpdates( - manager, - objectMetadataCollection, - options.type, - issues, - ); - - // Save workspace migrations into the database - await workspaceMigrationRepository.save(workspaceMigrations); - - if (!options.applyChanges) { - // Rollback transactions - await queryRunner.rollbackTransaction(); - - await queryRunner.release(); - - return { - workspaceMigrations, - metadataEntities, - }; - } - - // Commit the transaction - await queryRunner.commitTransaction(); - - // Apply pending migrations - await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( - workspaceId, - ); - } catch (error) { - await queryRunner.rollbackTransaction(); - this.logger.error('Fix of issues failed with:', error); - } finally { - await queryRunner.release(); - } - - return { - workspaceMigrations, - metadataEntities, - }; - } -} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts index 247c2b85c38..a07f39e1af1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts @@ -8,6 +8,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; @@ -17,7 +18,6 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role. import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { DevSeederModule } from 'src/engine/workspace-manager/dev-seeder/dev-seeder.module'; -import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module'; import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module'; @@ -32,10 +32,10 @@ import { WorkspaceManagerService } from './workspace-manager.service'; DevSeederModule, DataSourceModule, WorkspaceSyncMetadataModule, - WorkspaceHealthModule, FeatureFlagModule, PermissionsModule, AiAgentModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, TypeOrmModule.forFeature([UserWorkspaceEntity, WorkspaceEntity]), RoleModule, UserRoleModule, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index 38fda2b9610..aedf8f86bdf 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -10,6 +10,7 @@ import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { type DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; @@ -48,6 +49,7 @@ export class WorkspaceManagerService { private readonly roleTargetsRepository: Repository, protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly applicationService: ApplicationService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} public async init({ @@ -133,20 +135,27 @@ export class WorkspaceManagerService { featureFlags: Record, twentyStandardFlatApplication: FlatApplication, ) { - const createdObjectMetadata = - await this.objectMetadataServiceV2.findManyWithinWorkspace(workspaceId); + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], + }, + ); await standardObjectsPrefillData( this.coreDataSource, dataSourceMetadata.schema, - createdObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, ); await prefillCoreViews({ twentyStandardFlatApplication, coreDataSource: this.coreDataSource, workspaceId, - objectMetadataItems: createdObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, workspaceSchemaName: dataSourceMetadata.schema, featureFlags, }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts index f6efdfd4acc..8af5486778f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module.ts @@ -1,22 +1,20 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; -import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module'; import { SyncWorkspaceLoggerModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/services/sync-workspace-logger.module'; import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module'; -import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command'; @Module({ imports: [ WorkspaceSyncMetadataModule, - WorkspaceHealthModule, WorkspaceModule, DataSourceModule, WorkspaceDataSourceModule, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts index e9c4485f3a1..2f730732f94 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { FieldMetadataType, compositeTypeDefinitions, @@ -15,7 +16,6 @@ import { } from 'typeorm'; import { type DeepPartial } from 'typeorm/common/DeepPartial'; import { v4 as uuidV4 } from 'uuid'; -import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { type PartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; import { type PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface'; diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts index e56b567246f..10aa462fb49 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts @@ -1,7 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -9,11 +9,12 @@ import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-res import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; +import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; @WorkspaceQueryHook(`connectedAccount.destroyOne`) export class ConnectedAccountDeleteOnePreQueryHook @@ -46,18 +47,23 @@ export class ConnectedAccountDeleteOnePreQueryHook connectedAccountId, }); - const objectMetadata = await this.objectMetadataRepository.findOneOrFail({ - where: { - nameSingular: 'messageChannel', - workspaceId: workspace.id, - }, - }); + const objectMetadataEntity = + await this.objectMetadataRepository.findOneOrFail({ + where: { + nameSingular: 'messageChannel', + workspaceId: workspace.id, + }, + relations: ['fields', 'indexMetadatas', 'views'], + }); + + const flatObjectMetadata = + fromObjectMetadataEntityToFlatObjectMetadata(objectMetadataEntity); // TODO: handle cascade events for delete this.workspaceEventEmitter.emitDatabaseBatchEvent({ objectMetadataNameSingular: 'messageChannel', action: DatabaseEventAction.DESTROYED, - objectMetadata, + objectMetadata: flatObjectMetadata, events: messageChannels.map((messageChannel) => ({ recordId: messageChannel.id, properties: { diff --git a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts index bd7e64b3c81..6f684eed443 100644 --- a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts +++ b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts @@ -4,16 +4,18 @@ import { type ObjectRecord } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { In } from 'typeorm'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { type ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { parseEventNameOrThrow } from 'src/engine/workspace-event-emitter/utils/parse-event-name'; +import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity'; +import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity'; import { TimelineActivityRepository } from 'src/modules/timeline/repositories/timeline-activity.repository'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; import { type TimelineActivityPayload } from 'src/modules/timeline/types/timeline-activity-payload'; -import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity'; -import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity'; type ActivityType = 'note' | 'task'; @@ -23,6 +25,7 @@ export class TimelineActivityService { @InjectObjectMetadataRepository(TimelineActivityWorkspaceEntity) private readonly timelineActivityRepository: TimelineActivityRepository, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} private targetObjects: Record = { @@ -279,6 +282,23 @@ export class TimelineActivityService { return []; } + const { flatFieldMetadataMaps } = + await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFieldMetadataMaps'], + }, + ); + + const fields = getFlatFieldsFromFlatObjectMetadata( + objectMetadata, + flatFieldMetadataMaps, + ); + + const activityObjectMetadataId = fields.find( + (field) => field.name === activityType, + )?.relationTargetObjectMetadataId; + return events .map((event) => { const activity = activities.find( @@ -291,10 +311,6 @@ export class TimelineActivityService { return; } - const activityObjectMetadataId = objectMetadata.fields.find( - (field) => field.name === activityType, - )?.relationTargetObjectMetadataId; - if (!isDefined(activityObjectMetadataId)) { return; } diff --git a/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts b/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts index f31a420fdb3..03c37b2cf3e 100644 --- a/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts +++ b/packages/twenty-server/src/modules/timeline/timeline-activity.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; +import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { TimelineActivityService } from 'src/modules/timeline/services/timeline-activity.service'; @@ -11,6 +12,7 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o TimelineActivityWorkspaceEntity, ]), TwentyORMModule, + WorkspaceManyOrAllFlatEntityMapsCacheModule, ], providers: [TimelineActivityService], exports: [TimelineActivityService], diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts index 822a6ca4aed..dc10507df3c 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts @@ -2,13 +2,12 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { buildObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-metadata-item-with-field-maps.util'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { @@ -32,8 +31,9 @@ import { } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; export type ObjectMetadataInfo = { - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }; @Injectable() @@ -95,55 +95,52 @@ export class WorkflowCommonWorkspaceService { return { ...workflowVersion, trigger: workflowVersion.trigger }; } - async getObjectMetadataMaps( - workspaceId: string, - ): Promise { - const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } = + async getFlatEntityMaps(workspaceId: string): Promise<{ + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; + }> { + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, - flatMapsKeys: [ - 'flatObjectMetadataMaps', - 'flatFieldMetadataMaps', - 'flatIndexMaps', - ], + flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], }, ); const { idByNameSingular } = buildObjectIdByNameMaps( flatObjectMetadataMaps, ); - const objectMetadataMaps: ObjectMetadataMaps = { - byId: {}, - idByNameSingular, + + return { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular: idByNameSingular, }; - - for (const [id, flatObj] of Object.entries(flatObjectMetadataMaps.byId)) { - if (isDefined(flatObj)) { - objectMetadataMaps.byId[id] = buildObjectMetadataItemWithFieldMaps( - flatObj, - flatFieldMetadataMaps, - flatIndexMaps, - ); - } - } - - return objectMetadataMaps; } - async getObjectMetadataItemWithFieldsMaps( + async getObjectMetadataInfo( objectNameSingular: string, workspaceId: string, ): Promise { - const objectMetadataMaps = await this.getObjectMetadataMaps(workspaceId); + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = await this.getFlatEntityMaps(workspaceId); - const objectMetadataItemWithFieldsMaps = - getObjectMetadataMapItemByNameSingular( - objectMetadataMaps, - objectNameSingular, + const objectId = objectIdByNameSingular[objectNameSingular]; + + if (!isDefined(objectId)) { + throw new WorkflowCommonException( + `Failed to read: Object ${objectNameSingular} not found`, + WorkflowCommonExceptionCode.OBJECT_METADATA_NOT_FOUND, ); + } - if (!objectMetadataItemWithFieldsMaps) { + const flatObjectMetadata = flatObjectMetadataMaps.byId[objectId]; + + if (!isDefined(flatObjectMetadata)) { throw new WorkflowCommonException( `Failed to read: Object ${objectNameSingular} not found`, WorkflowCommonExceptionCode.OBJECT_METADATA_NOT_FOUND, @@ -151,8 +148,9 @@ export class WorkflowCommonWorkspaceService { } return { - objectMetadataItemWithFieldsMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }; } diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts index 80466fd0c8e..320ca4e2ddf 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts @@ -1,13 +1,13 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { + mockFlatFieldMetadataMaps, + mockFlatObjectMetadataMaps, + mockObjectIdByNameSingular, +} from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response'; import { type FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type'; -const companyMockObjectMetadataItem = mockObjectMetadataItemsWithFieldMaps.find( - (item) => item.nameSingular === 'company', -)!; - describe('generateFakeFormResponse', () => { it('should generate fake responses for a form schema', async () => { const formFieldMetadataItems: FormFieldMetadata[] = [ @@ -41,19 +41,11 @@ describe('generateFakeFormResponse', () => { }, ]; - const mockObjectMetadataMaps = { - byId: { - [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, - }, - idByNameSingular: { - [companyMockObjectMetadataItem.nameSingular]: - companyMockObjectMetadataItem.id, - }, - }; - const result = generateFakeFormResponse({ formFieldMetadataItems, - objectMetadataMaps: mockObjectMetadataMaps, + flatObjectMetadataMaps: mockFlatObjectMetadataMaps, + flatFieldMetadataMaps: mockFlatFieldMetadataMaps, + objectIdByNameSingular: mockObjectIdByNameSingular, }); expect(result).toEqual({ @@ -103,7 +95,7 @@ describe('generateFakeFormResponse', () => { }, }, name: { - fieldMetadataId: 'nameFieldMetadataId', + fieldMetadataId: 'nameFieldMetadataId-company', icon: 'test-field-icon', isLeaf: true, label: 'Name', diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts index e28882fe592..1fd3bc6404c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record-event.spec.ts @@ -1,5 +1,5 @@ import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { mockCompanyObjectMetadataInfo } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; @@ -25,33 +25,13 @@ describe('generateFakeObjectRecordEvent', () => { }, }; - const companyMockObjectMetadataItem = - mockObjectMetadataItemsWithFieldMaps.find( - (item) => item.nameSingular === 'company', - )!; - - const mockObjectMetadataMaps = { - byId: { - [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, - }, - idByNameSingular: { - [companyMockObjectMetadataItem.nameSingular]: - companyMockObjectMetadataItem.id, - }, - }; - - const objectMetadataInfo = { - objectMetadataMaps: mockObjectMetadataMaps, - objectMetadataItemWithFieldsMaps: companyMockObjectMetadataItem, - }; - beforeEach(() => { (generateObjectRecordFields as jest.Mock).mockReturnValue(mockFields); }); it('should generate record with "after" prefix for CREATED action', () => { const result = generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, DatabaseEventAction.CREATED, ); @@ -82,7 +62,7 @@ describe('generateFakeObjectRecordEvent', () => { it('should generate record with "after" prefix for UPDATED action', () => { const result = generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, DatabaseEventAction.UPDATED, ); @@ -113,7 +93,7 @@ describe('generateFakeObjectRecordEvent', () => { it('should generate record with "before" prefix for DELETED action', () => { const result = generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, DatabaseEventAction.DELETED, ); @@ -144,7 +124,7 @@ describe('generateFakeObjectRecordEvent', () => { it('should generate record with "before" prefix for DESTROYED action', () => { const result = generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, DatabaseEventAction.DESTROYED, ); @@ -175,7 +155,7 @@ describe('generateFakeObjectRecordEvent', () => { it('should generate record with "after" prefix for UPSERTED action', () => { const result = generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, DatabaseEventAction.UPSERTED, ); @@ -207,7 +187,7 @@ describe('generateFakeObjectRecordEvent', () => { it('should throw error for unknown action', () => { expect(() => { generateFakeObjectRecordEvent( - objectMetadataInfo, + mockCompanyObjectMetadataInfo, 'UNKNOWN' as DatabaseEventAction, ); }).toThrow("Unknown action 'UNKNOWN'"); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts index be6955fa161..144546ab46c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-object-record.spec.ts @@ -1,4 +1,4 @@ -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { mockCompanyObjectMetadataInfo } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; @@ -12,28 +12,11 @@ jest.mock( }), ); -const companyMockObjectMetadataItem = mockObjectMetadataItemsWithFieldMaps.find( - (item) => item.nameSingular === 'company', -)!; - -const mockObjectMetadataMaps = { - byId: { - [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, - }, - idByNameSingular: { - [companyMockObjectMetadataItem.nameSingular]: - companyMockObjectMetadataItem.id, - }, -}; - -const objectMetadataInfo = { - objectMetadataMaps: mockObjectMetadataMaps, - objectMetadataItemWithFieldsMaps: companyMockObjectMetadataItem, -}; - describe('generateFakeObjectRecord', () => { it('should generate a record with correct object metadata', () => { - const result = generateFakeObjectRecord({ objectMetadataInfo }); + const result = generateFakeObjectRecord({ + objectMetadataInfo: mockCompanyObjectMetadataInfo, + }); expect(result).toEqual({ object: { @@ -53,10 +36,12 @@ describe('generateFakeObjectRecord', () => { }); it('should call generateObjectRecordFields with the object metadata', () => { - generateFakeObjectRecord({ objectMetadataInfo }); + generateFakeObjectRecord({ + objectMetadataInfo: mockCompanyObjectMetadataInfo, + }); expect(generateObjectRecordFields).toHaveBeenCalledWith({ - objectMetadataInfo, + objectMetadataInfo: mockCompanyObjectMetadataInfo, depth: 0, maxDepth: 1, }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts index 7c4261bfbaa..483badde691 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-object-record-fields.spec.ts @@ -1,6 +1,6 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; +import { mockCompanyObjectMetadataInfo } from 'src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps'; import { generateFakeRecordField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-record-field'; import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; @@ -17,26 +17,6 @@ describe('generateObjectRecordFields', () => { jest.clearAllMocks(); }); - const companyMockObjectMetadataItem = - mockObjectMetadataItemsWithFieldMaps.find( - (item) => item.nameSingular === 'company', - )!; - - const mockObjectMetadataMaps = { - byId: { - [companyMockObjectMetadataItem.id]: companyMockObjectMetadataItem, - }, - idByNameSingular: { - [companyMockObjectMetadataItem.nameSingular]: - companyMockObjectMetadataItem.id, - }, - }; - - const objectMetadataInfo = { - objectMetadataMaps: mockObjectMetadataMaps, - objectMetadataItemWithFieldsMaps: companyMockObjectMetadataItem, - }; - it('should generate fields for valid fields only', () => { (shouldGenerateFieldFakeValue as jest.Mock).mockImplementation( (field) => field.type !== FieldMetadataType.RELATION, @@ -51,7 +31,9 @@ describe('generateObjectRecordFields', () => { }), ); - const result = generateObjectRecordFields({ objectMetadataInfo }); + const result = generateObjectRecordFields({ + objectMetadataInfo: mockCompanyObjectMetadataInfo, + }); expect(result).toEqual({ domainName: { @@ -75,7 +57,9 @@ describe('generateObjectRecordFields', () => { it('should return empty object when no valid fields', () => { (shouldGenerateFieldFakeValue as jest.Mock).mockReturnValue(false); - const result = generateObjectRecordFields({ objectMetadataInfo }); + const result = generateObjectRecordFields({ + objectMetadataInfo: mockCompanyObjectMetadataInfo, + }); expect(result).toEqual({}); expect(shouldGenerateFieldFakeValue).toHaveBeenCalledTimes(2); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts index 8d7bf2de09a..e99bed9a9af 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts @@ -1,99 +1,72 @@ -import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; +import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('shouldGenerateFieldFakeValue', () => { it('should return true for active non-system fields', () => { - const field = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', + const field = getFlatFieldMetadataMock({ objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000002', isSystem: false, isActive: true, type: FieldMetadataType.TEXT, name: 'testField', - label: 'Test Field', - isNullable: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), + universalIdentifier: 'test-field-universal-id', }); expect(shouldGenerateFieldFakeValue(field)).toBe(true); }); it('should return true for system id field', () => { - const field = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', + const field = getFlatFieldMetadataMock({ objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000003', isSystem: true, isActive: true, type: FieldMetadataType.UUID, name: 'id', - label: 'ID', - isNullable: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), + universalIdentifier: 'id-field-universal-id', }); expect(shouldGenerateFieldFakeValue(field)).toBe(true); }); it('should return false for inactive fields', () => { - const field = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', + const field = getFlatFieldMetadataMock({ objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000004', isSystem: false, isActive: false, type: FieldMetadataType.TEXT, name: 'testField', - label: 'Test Field', - isNullable: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), + universalIdentifier: 'inactive-field-universal-id', }); expect(shouldGenerateFieldFakeValue(field)).toBe(false); }); it('should return true for system fields', () => { - const field = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', + const field = getFlatFieldMetadataMock({ objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000005', isSystem: true, isActive: true, type: FieldMetadataType.TEXT, name: 'testField', - label: 'Test Field', - isNullable: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), + universalIdentifier: 'system-field-universal-id', }); expect(shouldGenerateFieldFakeValue(field)).toBe(true); }); - it('should return false for relation fields', () => { - const field = getMockFieldMetadataEntity({ - workspaceId: '20202020-0000-0000-0000-000000000000', + it('should return false for many-to-one relation fields', () => { + const field = getFlatFieldMetadataMock({ objectMetadataId: '20202020-0000-0000-0000-000000000001', - id: '20202020-0000-0000-0000-000000000006', isSystem: false, isActive: true, type: FieldMetadataType.RELATION, name: 'testField', - label: 'Test Field', - isNullable: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), + universalIdentifier: 'relation-field-universal-id', + settings: { + relationType: RelationType.MANY_TO_ONE, + }, }); expect(shouldGenerateFieldFakeValue(field)).toBe(false); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts index c5657ebd9cf..b7c11d0063a 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts @@ -1,7 +1,8 @@ import { isDefined } from 'twenty-shared/utils'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { type Leaf, type Node, @@ -12,12 +13,16 @@ import { type FormFieldMetadata } from 'src/modules/workflow/workflow-executor/w type GenerateFakeFormResponseArgs = { formFieldMetadataItems: FormFieldMetadata[]; - objectMetadataMaps: ObjectMetadataMaps; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; + objectIdByNameSingular: Record; }; export const generateFakeFormResponse = ({ formFieldMetadataItems, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }: GenerateFakeFormResponseArgs): Record => { const result = formFieldMetadataItems.map((formFieldMetadata) => { if (formFieldMetadata.type === 'RECORD') { @@ -25,13 +30,18 @@ export const generateFakeFormResponse = ({ return undefined; } - const objectMetadataItemWithFieldsMaps = - getObjectMetadataMapItemByNameSingular( - objectMetadataMaps, - formFieldMetadata?.settings?.objectName, - ); + const objectId = + objectIdByNameSingular[formFieldMetadata?.settings?.objectName]; - if (!isDefined(objectMetadataItemWithFieldsMaps)) { + if (!isDefined(objectId)) { + throw new Error( + `Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`, + ); + } + + const flatObjectMetadata = flatObjectMetadataMaps.byId[objectId]; + + if (!isDefined(flatObjectMetadata)) { throw new Error( `Object metadata not found for object name ${formFieldMetadata?.settings?.objectName}`, ); @@ -43,8 +53,9 @@ export const generateFakeFormResponse = ({ label: formFieldMetadata.label, value: generateFakeObjectRecord({ objectMetadataInfo: { - objectMetadataItemWithFieldsMaps, - objectMetadataMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }, }), }, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts index 8b59e3bf46b..e7684f41330 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event.ts @@ -13,6 +13,7 @@ const generateFakeObjectRecordEventWithPrefix = ({ objectMetadataInfo: ObjectMetadataInfo; prefix: string; }): RecordOutputSchema => { + const { flatObjectMetadata } = objectMetadataInfo; const recordFields = generateObjectRecordFields({ objectMetadataInfo }); const prefixedRecordFields = Object.entries(recordFields).reduce( (acc, [key, value]) => { @@ -26,12 +27,11 @@ const generateFakeObjectRecordEventWithPrefix = ({ return { object: { isLeaf: true, - icon: - objectMetadataInfo.objectMetadataItemWithFieldsMaps.icon ?? undefined, - label: objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular, - value: objectMetadataInfo.objectMetadataItemWithFieldsMaps.description, + icon: flatObjectMetadata.icon ?? undefined, + label: flatObjectMetadata.labelSingular, + value: flatObjectMetadata.description, fieldIdName: `${prefix}.id`, - objectMetadataId: objectMetadataInfo.objectMetadataItemWithFieldsMaps.id, + objectMetadataId: flatObjectMetadata.id, }, fields: prefixedRecordFields, _outputSchemaType: 'RECORD', diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts index 9ae8b623339..0fe4757e5f3 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record.ts @@ -11,15 +11,16 @@ export const generateFakeObjectRecord = ({ depth?: number; maxDepth?: number; }): RecordOutputSchema => { + const { flatObjectMetadata } = objectMetadataInfo; + return { object: { isLeaf: true, - icon: - objectMetadataInfo.objectMetadataItemWithFieldsMaps.icon ?? undefined, - label: objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular, - value: objectMetadataInfo.objectMetadataItemWithFieldsMaps.description, + icon: flatObjectMetadata.icon ?? undefined, + label: flatObjectMetadata.labelSingular, + value: flatObjectMetadata.description, fieldIdName: 'id', - objectMetadataId: objectMetadataInfo.objectMetadataItemWithFieldsMaps.id, + objectMetadataId: flatObjectMetadata.id, }, fields: generateObjectRecordFields({ objectMetadataInfo, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts index cfea6703624..44ac25b7267 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts @@ -1,6 +1,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { type ObjectMetadataInfo } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { type FieldOutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record'; @@ -17,67 +18,71 @@ export const generateObjectRecordFields = ({ depth?: number; maxDepth?: number; }): Record => { - const objectMetadata = objectMetadataInfo.objectMetadataItemWithFieldsMaps; + const { flatObjectMetadata, flatObjectMetadataMaps, flatFieldMetadataMaps } = + objectMetadataInfo; - return Object.values(objectMetadata.fieldsById).reduce( - (acc: Record, field) => { - if (!shouldGenerateFieldFakeValue(field)) { - return acc; + const result: Record = {}; + + for (const fieldId of flatObjectMetadata.fieldMetadataIds) { + const field = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: fieldId, + }); + + if (!shouldGenerateFieldFakeValue(field)) { + continue; + } + + if (field.type !== FieldMetadataType.RELATION) { + result[field.name] = generateFakeRecordField({ + type: field.type, + label: field.label, + icon: field.icon ?? undefined, + fieldMetadataId: field.id, + }); + + continue; + } + + if (!isDefined(field.relationTargetObjectMetadataId)) { + continue; + } + + if (depth < maxDepth) { + const relationTargetObjectMetadata = + flatObjectMetadataMaps.byId[field.relationTargetObjectMetadataId]; + + if (!isDefined(relationTargetObjectMetadata)) { + continue; } - if (field.type !== FieldMetadataType.RELATION) { - acc[field.name] = generateFakeRecordField({ - type: field.type, - label: field.label, - icon: field.icon ?? undefined, - fieldMetadataId: field.id, - }); + result[field.name] = { + isLeaf: false, + icon: field.icon ?? undefined, + label: field.label, + type: field.type, + fieldMetadataId: field.id, + value: generateFakeObjectRecord({ + objectMetadataInfo: { + flatObjectMetadata: relationTargetObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + }, + depth: depth + 1, + }), + }; + } else if (depth === maxDepth) { + const relationIdFieldName = `${field.name}Id`; + const relationIdFieldLabel = camelToTitleCase(relationIdFieldName); - return acc; - } + result[relationIdFieldName] = generateFakeRecordField({ + type: FieldMetadataType.UUID, + label: relationIdFieldLabel, + icon: field.icon ?? undefined, + fieldMetadataId: field.id, + }); + } + } - if (!isDefined(field.relationTargetObjectMetadataId)) { - return acc; - } - - if (depth < maxDepth) { - const relationTargetObjectMetadata = - objectMetadataInfo.objectMetadataMaps.byId[ - field.relationTargetObjectMetadataId - ]; - - if (!isDefined(relationTargetObjectMetadata)) { - return acc; - } - - acc[field.name] = { - isLeaf: false, - icon: field.icon ?? undefined, - label: field.label, - type: field.type, - fieldMetadataId: field.id, - value: generateFakeObjectRecord({ - objectMetadataInfo: { - objectMetadataItemWithFieldsMaps: relationTargetObjectMetadata, - objectMetadataMaps: objectMetadataInfo.objectMetadataMaps, - }, - depth: depth + 1, - }), - }; - } else if (depth === maxDepth) { - const relationIdFieldName = `${field.name}Id`; - const relationIdFieldLabel = camelToTitleCase(relationIdFieldName); - - acc[relationIdFieldName] = generateFakeRecordField({ - type: FieldMetadataType.UUID, - label: relationIdFieldLabel, - icon: field.icon ?? undefined, - fieldMetadataId: field.id, - }); - } - - return acc; - }, - {} as Record, - ); + return result; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts index e297f093b68..c589cef30fb 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts @@ -1,21 +1,22 @@ -import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; -const isManyToOneRelationField = (field: FieldMetadataEntity) => - (field as FieldMetadataEntity).settings - ?.relationType === 'MANY_TO_ONE'; +const isManyToOneRelationField = (field: FlatFieldMetadata) => { + if (isFlatFieldMetadataOfType(field, FieldMetadataType.RELATION)) { + return field.settings?.relationType === RelationType.MANY_TO_ONE; + } + + return false; +}; const EXCLUDED_SYSTEM_FIELDS = ['searchVector', 'position']; -// TODO refactor -export const shouldGenerateFieldFakeValue = ( - field: FieldMetadataEntity, -) => { +export const shouldGenerateFieldFakeValue = (field: FlatFieldMetadata) => { return ( field.isActive && !(EXCLUDED_SYSTEM_FIELDS.includes(field.name) && field.isSystem) && - (field.type !== FieldMetadataType.RELATION || - isManyToOneRelationField(field as unknown as FieldMetadataEntity)) + !isManyToOneRelationField(field) ); }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts index 9d8eb939310..03e9d9f9648 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts @@ -216,7 +216,7 @@ export class WorkflowSchemaWorkspaceService { } const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( nameSingular, workspaceId, ); @@ -241,14 +241,14 @@ export class WorkflowSchemaWorkspaceService { }); const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( objectType, workspaceId, ); const first: Node = { isLeaf: false, - label: `First ${objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular ?? 'Record'}`, + label: `First ${objectMetadataInfo.flatObjectMetadata.labelSingular ?? 'Record'}`, icon: 'IconAlpha', type: 'object', value: recordOutputSchema, @@ -256,7 +256,7 @@ export class WorkflowSchemaWorkspaceService { const all: Leaf = { isLeaf: true, - label: `All ${objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural ?? 'Records'}`, + label: `All ${objectMetadataInfo.flatObjectMetadata.labelPlural ?? 'Records'}`, type: 'array', icon: 'IconListDetails', value: 'Returns an array of records', @@ -283,7 +283,7 @@ export class WorkflowSchemaWorkspaceService { maxDepth?: number; }): Promise { const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( objectType, workspaceId, ); @@ -302,14 +302,18 @@ export class WorkflowSchemaWorkspaceService { formFieldMetadataItems: FormFieldMetadata[]; workspaceId: string; }): Promise { - const objectMetadataMaps = - await this.workflowCommonWorkspaceService.getObjectMetadataMaps( - workspaceId, - ); + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, + } = + await this.workflowCommonWorkspaceService.getFlatEntityMaps(workspaceId); return generateFakeFormResponse({ formFieldMetadataItems, - objectMetadataMaps, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + objectIdByNameSingular, }); } @@ -337,20 +341,18 @@ export class WorkflowSchemaWorkspaceService { if (availability.type === 'BULK_RECORDS') { const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( availability.objectNameSingular, workspaceId, ); return { - [objectMetadataInfo.objectMetadataItemWithFieldsMaps.namePlural]: { - label: - objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural, + [objectMetadataInfo.flatObjectMetadata.namePlural]: { + label: objectMetadataInfo.flatObjectMetadata.labelPlural, isLeaf: true, type: 'array', value: - 'Array of ' + - objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural, + 'Array of ' + objectMetadataInfo.flatObjectMetadata.labelPlural, }, }; } @@ -417,7 +419,7 @@ export class WorkflowSchemaWorkspaceService { case WorkflowTriggerType.MANUAL: { if (trigger.settings.availability?.type === 'BULK_RECORDS') { const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( trigger.settings.availability.objectNameSingular, workspaceId, ); @@ -425,8 +427,7 @@ export class WorkflowSchemaWorkspaceService { return { label: 'Current Item (' + - objectMetadataInfo.objectMetadataItemWithFieldsMaps - .labelSingular + + objectMetadataInfo.flatObjectMetadata.labelSingular + ')', isLeaf: false, type: 'object', @@ -468,7 +469,7 @@ export class WorkflowSchemaWorkspaceService { switch (step.type) { case WorkflowActionType.FIND_RECORDS: { const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( step.settings.input.objectName, workspaceId, ); @@ -476,7 +477,7 @@ export class WorkflowSchemaWorkspaceService { return { label: 'Current Item (' + - objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelSingular + + objectMetadataInfo.flatObjectMetadata.labelSingular + ')', isLeaf: false, type: 'object', diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts index de49125db6b..9c459fd52b0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts @@ -6,6 +6,7 @@ import { isDefined, isValidUuid } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; +import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema'; import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; @@ -26,7 +27,6 @@ import { type WorkflowEmptyAction, type WorkflowFormAction, } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; - const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = { outputSchema: {}, errorHandlingOptions: { @@ -460,14 +460,15 @@ export class WorkflowVersionStepOperationsWorkspaceService { // @ts-expect-error legacy noImplicitAny isValidUuid(response[key].id) ) { - const objectMetadataInfo = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( + const { flatObjectMetadata, flatFieldMetadataMaps } = + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( field.settings.objectName, workspaceId, ); - const relationFieldsNames = Object.values( - objectMetadataInfo.objectMetadataItemWithFieldsMaps.fieldsById, + const relationFieldsNames = getFlatFieldsFromFlatObjectMetadata( + flatObjectMetadata, + flatFieldMetadataMaps, ) .filter((field) => field.type === FieldMetadataType.RELATION) .map((field) => field.name); diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts index 03084f9dc3a..2012a17ae96 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/__tests__/workflow-database-event-trigger.listener.spec.ts @@ -1,13 +1,13 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { AutomatedTriggerType } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowDatabaseEventTriggerListener } from 'src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener'; import { WorkflowTriggerJob } from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job'; -import { getMockObjectMetadataEntity } from 'src/utils/__test__/get-object-metadata-entity.mock'; -import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock'; describe('WorkflowDatabaseEventTriggerListener', () => { let listener: WorkflowDatabaseEventTriggerListener; @@ -18,6 +18,35 @@ describe('WorkflowDatabaseEventTriggerListener', () => { find: jest.fn(), }; + const createMockFlatObjectMetadata = ( + overrides: Partial, + ): FlatObjectMetadata => + ({ + id: 'test-object-metadata', + workspaceId: 'test-workspace', + nameSingular: 'testObject', + namePlural: 'testObjects', + labelSingular: 'Test Object', + labelPlural: 'Test Objects', + description: 'Test object for testing', + targetTableName: 'test_objects', + isSystem: false, + isCustom: false, + isActive: true, + isRemote: false, + isAuditLogged: true, + isSearchable: true, + createdAt: new Date(), + updatedAt: new Date(), + icon: 'Icon123', + universalIdentifier: 'test-object-metadata', + fieldMetadataIds: [], + indexMetadataIds: [], + viewIds: [], + applicationId: null, + ...overrides, + }) as FlatObjectMetadata; + beforeEach(async () => { twentyORMGlobalManager = { getRepositoryForWorkspace: jest.fn().mockResolvedValue(mockRepository), @@ -46,37 +75,10 @@ describe('WorkflowDatabaseEventTriggerListener', () => { provide: WorkflowCommonWorkspaceService, useValue: { getWorkflowById: jest.fn(), - getObjectMetadataItemWithFieldsMaps: jest.fn().mockResolvedValue({ - objectMetadataMaps: { - byId: { - 'test-object-metadata': { - nameSingular: 'testObject', - namePlural: 'testObjects', - }, - }, - }, - objectMetadataItemWithFieldsMaps: - getMockObjectMetadataItemWithFieldsMaps({ - id: 'test-object-metadata', - workspaceId: 'test-workspace', - nameSingular: 'testObject', - namePlural: 'testObjects', - labelSingular: 'Test Object', - labelPlural: 'Test Objects', - description: 'Test object for testing', - indexMetadatas: [], - targetTableName: 'test_objects', - isSystem: false, - isCustom: false, - isActive: true, - isRemote: false, - isAuditLogged: true, - isSearchable: true, - icon: 'Icon123', - fieldIdByJoinColumnName: {}, - fieldsById: {}, - fieldIdByName: {}, - }), + getObjectMetadataInfo: jest.fn().mockResolvedValue({ + flatObjectMetadata: createMockFlatObjectMetadata({}), + flatObjectMetadataMaps: { byId: {}, byName: {} }, + flatFieldMetadataMaps: { byId: {}, byName: {} }, }), }, }, @@ -93,30 +95,10 @@ describe('WorkflowDatabaseEventTriggerListener', () => { const databaseEventName = 'testEvent'; const workflowId = 'test-workflow'; - const mockPayload = { + const mockPayload: WorkspaceEventBatch = { workspaceId, name: databaseEventName, - objectMetadata: getMockObjectMetadataEntity({ - id: 'test-object-metadata', - workspaceId, - nameSingular: 'testObject', - namePlural: 'testObjects', - labelSingular: 'Test Object', - labelPlural: 'Test Objects', - description: 'Test object for testing', - targetTableName: 'test_objects', - isSystem: false, - isCustom: false, - isActive: true, - isRemote: false, - isAuditLogged: true, - isSearchable: true, - createdAt: new Date(), - updatedAt: new Date(), - fields: [], - indexMetadatas: [], - icon: 'Icon123', - }), + objectMetadata: createMockFlatObjectMetadata({}), events: [ { recordId: 'test-record', @@ -205,7 +187,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => { }); it('should handle create events correctly', async () => { - const createPayload = { + const createPayload: WorkspaceEventBatch = { ...mockPayload, name: 'createEvent', events: [ @@ -242,7 +224,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => { }); it('should handle delete events correctly', async () => { - const deletePayload = { + const deletePayload: WorkspaceEventBatch = { ...mockPayload, name: 'deleteEvent', events: [ @@ -279,7 +261,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => { }); it('should handle destroy events correctly', async () => { - const destroyPayload = { + const destroyPayload: WorkspaceEventBatch = { ...mockPayload, name: 'destroyEvent', events: [ @@ -316,7 +298,7 @@ describe('WorkflowDatabaseEventTriggerListener', () => { }); it('should handle multiple events in a batch', async () => { - const batchPayload = { + const batchPayload: WorkspaceEventBatch = { ...mockPayload, events: [ mockPayload.events[0], diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts index ce16475a135..92ebc4b9129 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/workflow-database-event-trigger.listener.ts @@ -15,8 +15,11 @@ import { type ObjectRecordUpsertEvent } from 'src/engine/core-modules/event-emit import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; -import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type'; import { @@ -135,17 +138,21 @@ export class WorkflowDatabaseEventTriggerListener { payload: WorkspaceEventBatch, ) { const workspaceId = payload.workspaceId; - const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - payload.objectMetadata.nameSingular, - workspaceId, - ); + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = await this.workflowCommonWorkspaceService.getObjectMetadataInfo( + payload.objectMetadata.nameSingular, + workspaceId, + ); await this.enrichRecordsWithRelations({ records: payload.events.map((event) => event.properties.after), workspaceId, - objectMetadataMaps, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); } @@ -153,23 +160,28 @@ export class WorkflowDatabaseEventTriggerListener { payload: WorkspaceEventBatch, ) { const workspaceId = payload.workspaceId; - const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - payload.objectMetadata.nameSingular, - workspaceId, - ); + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = await this.workflowCommonWorkspaceService.getObjectMetadataInfo( + payload.objectMetadata.nameSingular, + workspaceId, + ); await this.enrichRecordsWithRelations({ records: payload.events.map((event) => event.properties.before), workspaceId, - objectMetadataMaps, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); await this.enrichRecordsWithRelations({ records: payload.events.map((event) => event.properties.after), workspaceId, - objectMetadataMaps, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); } @@ -177,17 +189,21 @@ export class WorkflowDatabaseEventTriggerListener { payload: WorkspaceEventBatch, ) { const workspaceId = payload.workspaceId; - const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - payload.objectMetadata.nameSingular, - workspaceId, - ); + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = await this.workflowCommonWorkspaceService.getObjectMetadataInfo( + payload.objectMetadata.nameSingular, + workspaceId, + ); await this.enrichRecordsWithRelations({ records: payload.events.map((event) => event.properties.before), workspaceId, - objectMetadataMaps, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); } @@ -195,36 +211,50 @@ export class WorkflowDatabaseEventTriggerListener { payload: WorkspaceEventBatch, ) { const workspaceId = payload.workspaceId; - const { objectMetadataMaps, objectMetadataItemWithFieldsMaps } = - await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( - payload.objectMetadata.nameSingular, - workspaceId, - ); + const { + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, + } = await this.workflowCommonWorkspaceService.getObjectMetadataInfo( + payload.objectMetadata.nameSingular, + workspaceId, + ); await this.enrichRecordsWithRelations({ records: payload.events.map((event) => event.properties.before), - objectMetadataMaps, workspaceId, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }); } private async enrichRecordsWithRelations({ records, workspaceId, - objectMetadataMaps, - objectMetadataItemWithFieldsMaps, + flatObjectMetadata, + flatObjectMetadataMaps, + flatFieldMetadataMaps, }: { records: Partial[]; workspaceId: string; - objectMetadataMaps: ObjectMetadataMaps; - objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; + flatObjectMetadata: FlatObjectMetadata; + flatObjectMetadataMaps: FlatEntityMaps; + flatFieldMetadataMaps: FlatEntityMaps; }) { + const { fieldIdByJoinColumnName } = buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + for (const [joinColumnName, joinFieldId] of Object.entries( - objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName, + fieldIdByJoinColumnName, )) { - const joinField = - objectMetadataItemWithFieldsMaps.fieldsById[joinFieldId]; + const joinField = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: joinFieldId, + }); + const joinRecordIds = records .map((record) => record[joinColumnName]) .filter(isDefined); @@ -240,7 +270,7 @@ export class WorkflowDatabaseEventTriggerListener { } const relatedObjectMetadataNameSingular = - objectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular; + flatObjectMetadataMaps.byId[relatedObjectMetadataId]?.nameSingular; if (!isDefined(relatedObjectMetadataNameSingular)) { continue; diff --git a/packages/twenty-server/src/utils/__test__/get-object-metadata-item-with-fields-maps.mock.ts b/packages/twenty-server/src/utils/__test__/get-object-metadata-item-with-fields-maps.mock.ts deleted file mode 100644 index ab310cc46da..00000000000 --- a/packages/twenty-server/src/utils/__test__/get-object-metadata-item-with-fields-maps.mock.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { - type GetMockObjectMetadataEntityOverride, - getMockObjectMetadataEntity, -} from 'src/utils/__test__/get-object-metadata-entity.mock'; - -type GetMockObjectMetadataItemWithFielsMapsOverride = - GetMockObjectMetadataEntityOverride & - Required< - Pick< - ObjectMetadataItemWithFieldMaps, - | 'fieldsById' - | 'fieldIdByJoinColumnName' - | 'fieldIdByName' - | 'indexMetadatas' - > - >; - -export const getMockObjectMetadataItemWithFieldsMaps = ({ - fieldsById, - fieldIdByJoinColumnName, - fieldIdByName, - indexMetadatas, - ...objectMetadataOverrides -}: GetMockObjectMetadataItemWithFielsMapsOverride): ObjectMetadataItemWithFieldMaps => { - const { fields: _fields, ...objectMetadata } = getMockObjectMetadataEntity( - objectMetadataOverrides, - ); - - return { - ...objectMetadata, - fieldsById, - fieldIdByJoinColumnName, - fieldIdByName, - indexMetadatas, - }; -}; diff --git a/packages/twenty-server/test/integration/graphql/suites/group-by-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/group-by-resolver.integration-spec.ts index 691b54b08be..0fdf5e46299 100644 --- a/packages/twenty-server/test/integration/graphql/suites/group-by-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/group-by-resolver.integration-spec.ts @@ -320,20 +320,19 @@ describe('group-by resolver (integration)', () => { expect(groups).toBeDefined(); expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(2); - // Expect two groups: January 2025 and March 2025 - const janGroup = groups.find((g: any) => - g.groupByDimensionValues?.[0]?.startsWith?.('2025-01-01'), - ); - const marGroup = groups.find((g: any) => - g.groupByDimensionValues?.[0]?.startsWith?.('2025-03-01'), - ); + // Expect two groups: one with 2 records (January) and one with 1 record (March) + // Note: DATE_TRUNC returns dates in server timezone, which when converted to UTC + // may show as the previous day at 23:00 (e.g., 2024-12-31T23:00 for Jan 1 local) + const groupWith2Records = groups.find((g: any) => g.totalCount === 2); + const groupWith1Record = groups.find((g: any) => g.totalCount === 1); - expect(janGroup).toBeDefined(); - expect(marGroup).toBeDefined(); + expect(groupWith2Records).toBeDefined(); + expect(groupWith1Record).toBeDefined(); - expect(janGroup.totalCount).toBe(2); - expect(marGroup.totalCount).toBe(1); + expect(groupWith2Records.totalCount).toBe(2); + expect(groupWith1Record.totalCount).toBe(1); }); describe('group by week', () => { diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts index b94d10c06f5..fe2ff14f8c5 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/nested-relation-queries.integration-spec.ts @@ -602,7 +602,21 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', it('should connect a morph relation ownerSurveyResult on pet via the connect feature', async () => { const PET_OBJECT_NAME = 'pet'; + const SURVEY_RESULT_OBJECT_NAME = 'surveyResult'; const TEST_PET_ID = TEST_PET_ID_1; + const TEST_SURVEY_RESULT_ID = TEST_SURVEY_RESULT_1_ID; + + // Create the survey result record first + await makeGraphqlAPIRequest( + createOneOperationFactory({ + objectMetadataSingularName: SURVEY_RESULT_OBJECT_NAME, + gqlFields: 'id', + data: { + id: TEST_SURVEY_RESULT_ID, + name: 'Test Survey Result', + }, + }), + ); await makeGraphqlAPIRequest( createOneOperationFactory({ @@ -615,8 +629,6 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', }), ); - const TEST_SURVEY_RESULT_ID = TEST_SURVEY_RESULT_1_ID; - const updatePetOwnerSurveyResultOp = updateOneOperationFactory({ objectMetadataSingularName: PET_OBJECT_NAME, recordId: TEST_PET_ID, @@ -630,7 +642,7 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', }, }); - let response = await makeGraphqlAPIRequest(updatePetOwnerSurveyResultOp); + const response = await makeGraphqlAPIRequest(updatePetOwnerSurveyResultOp); expect(response.body.data.updatePet).toBeDefined(); expect(response.body.data.updatePet.ownerSurveyResult).toBeDefined(); @@ -642,7 +654,21 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', it('should disconnect a morph relation successfully', async () => { const PET_OBJECT_NAME = 'pet'; + const SURVEY_RESULT_OBJECT_NAME = 'surveyResult'; const TEST_PET_ID = TEST_PET_ID_2; + const TEST_SURVEY_RESULT_ID = TEST_SURVEY_RESULT_1_ID; + + // Create the survey result record first (if not already created by previous test) + await makeGraphqlAPIRequest( + createOneOperationFactory({ + objectMetadataSingularName: SURVEY_RESULT_OBJECT_NAME, + gqlFields: 'id', + data: { + id: TEST_SURVEY_RESULT_ID, + name: 'Test Survey Result', + }, + }), + ); await makeGraphqlAPIRequest( createOneOperationFactory({ @@ -655,8 +681,6 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)', }), ); - const TEST_SURVEY_RESULT_ID = TEST_SURVEY_RESULT_1_ID; - const updatePetOwnerSurveyResultOp = updateOneOperationFactory({ objectMetadataSingularName: PET_OBJECT_NAME, recordId: TEST_PET_ID, diff --git a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/fields-permissions/update-permissions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/fields-permissions/update-permissions.integration-spec.ts index 65828a1a507..917d3af2239 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/fields-permissions/update-permissions.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/fields-permissions/update-permissions.integration-spec.ts @@ -386,7 +386,7 @@ describe('Field update permissions restrictions', () => { objectMetadataPluralName: 'companies', gqlFields: COMPANY_GQL_FIELDS_WITHOUT_EMPLOYEES, data: { name: 'UpdatedCompany' }, - filter: { name: { ilike: '%aa%' } }, + filter: { id: { eq: companyId } }, }); const response = diff --git a/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts index d298b6e8caa..8d61662c1a9 100644 --- a/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/workspace/successful-user-and-workspace-creation.integration-spec.ts @@ -1,3 +1,5 @@ +import { randomUUID } from 'crypto'; + import { activateWorkspace } from 'test/integration/graphql/utils/activate-workspace.util'; import { deleteUser } from 'test/integration/graphql/utils/delete-user.util'; import { findManyApplications } from 'test/integration/graphql/utils/find-many-applications.util'; @@ -27,9 +29,11 @@ describe('Successful user and workspace creation', () => { }); it('should sign up a new user and create a new workspace successfully', async () => { + const uniqueEmail = `test-${randomUUID()}@example.com`; + const { data } = await signUp({ input: { - email: `test-1234@example.com`, + email: uniqueEmail, password: 'Test123!@#', }, @@ -39,6 +43,12 @@ describe('Successful user and workspace creation', () => { createdUserAccessToken = data.signUp.tokens.accessOrWorkspaceAgnosticToken.token; + // Mark email as verified to bypass email verification requirement + await testDataSource.query( + 'UPDATE core."user" SET "isEmailVerified" = true WHERE email = $1', + [uniqueEmail], + ); + const { data: { signUpInNewWorkspace: signUpInNewWorkspaceData }, } = await signUpInNewWorkspace({ @@ -121,9 +131,11 @@ describe('Successful user and workspace creation', () => { }); it('should delete workspace and related metadata entities when last user is deleted', async () => { + const uniqueEmail = `test-delete-${randomUUID()}@example.com`; + const { data } = await signUp({ input: { - email: `test-delete-5678@example.com`, + email: uniqueEmail, password: 'Test123!@#', }, expectToFail: false, @@ -132,6 +144,12 @@ describe('Successful user and workspace creation', () => { createdUserAccessToken = data.signUp.tokens.accessOrWorkspaceAgnosticToken.token; + // Mark email as verified to bypass email verification requirement + await testDataSource.query( + 'UPDATE core."user" SET "isEmailVerified" = true WHERE email = $1', + [uniqueEmail], + ); + const { data: { signUpInNewWorkspace: signUpInNewWorkspaceData }, } = await signUpInNewWorkspace({ diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts index 305cb4cfe5f..608d742e70a 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts @@ -1,3 +1,5 @@ +import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util'; + import { type AgentToolTestContext, createAgentToolTestModule, @@ -45,7 +47,11 @@ describe('AgentToolGeneratorService Integration', () => { }); jest .spyOn(context.objectMetadataService, 'findManyWithinWorkspace') - .mockResolvedValue([context.testObjectMetadata]); + .mockResolvedValue([ + fromObjectMetadataEntityToFlatObjectMetadata( + context.testObjectMetadata, + ), + ]); const tools = await context.agentToolService.generateToolsForAgent( context.testAgentId, @@ -89,7 +95,11 @@ describe('AgentToolGeneratorService Integration', () => { }); jest .spyOn(context.objectMetadataService, 'findManyWithinWorkspace') - .mockResolvedValue([context.testObjectMetadata]); + .mockResolvedValue([ + fromObjectMetadataEntityToFlatObjectMetadata( + context.testObjectMetadata, + ), + ]); const tools = await context.agentToolService.generateToolsForAgent( context.testAgentId, @@ -146,7 +156,9 @@ describe('AgentToolGeneratorService Integration', () => { }); jest .spyOn(context.objectMetadataService, 'findManyWithinWorkspace') - .mockResolvedValue([workflowObject]); + .mockResolvedValue([ + fromObjectMetadataEntityToFlatObjectMetadata(workflowObject), + ]); const tools = await context.agentToolService.generateToolsForAgent( context.testAgentId, diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/create-delete-and-create-object-metadata-v2.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/create-delete-and-create-object-metadata-v2.integration-spec.ts index a7059640db5..ec297ecaebd 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/create-delete-and-create-object-metadata-v2.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/create-delete-and-create-object-metadata-v2.integration-spec.ts @@ -2,7 +2,6 @@ import { type CreateOneFieldFactoryInput } from 'test/integration/metadata/suite import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; -import { CUSTOM_OBJECT_DISHES } from 'test/integration/metadata/suites/object-metadata/constants/custom-object-dishes.constants'; import { type CreateOneObjectFactoryInput } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata-query-factory.util'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; @@ -12,6 +11,7 @@ import { isDefined } from 'twenty-shared/utils'; describe('Handle same object sequential operations test suite', () => { let createdObjectMetadataId: string | undefined = undefined; + const uniqueSuffix = Date.now().toString().slice(-8); afterEach(async () => { if (!isDefined(createdObjectMetadataId)) { @@ -35,19 +35,12 @@ describe('Handle same object sequential operations test suite', () => { createdObjectMetadataId = undefined; }); it('It should handle creation deletion and creation of the same metadata entity gracefully', async () => { - const { - labelPlural, - description, - labelSingular, - namePlural, - nameSingular, - } = CUSTOM_OBJECT_DISHES; const createObjectInput: CreateOneObjectFactoryInput = { - labelPlural, - description, - labelSingular, - namePlural, - nameSingular, + namePlural: `dishes${uniqueSuffix}`, + nameSingular: `dish${uniqueSuffix}`, + labelPlural: `Dishes ${uniqueSuffix}`, + labelSingular: `Dish ${uniqueSuffix}`, + description: 'My favorite dishes', icon: 'IconBuildingSkyscraper', isLabelSyncedWithName: false, }; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts index ebbbfbe6959..88580853a69 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/rename-custom-object.integration-spec.ts @@ -8,6 +8,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; describe('Custom object renaming', () => { let listingObjectId = ''; + const uniqueSuffix = Date.now().toString().slice(-8); const STANDARD_OBJECT_RELATIONS = [ 'noteTarget', @@ -79,10 +80,10 @@ describe('Custom object renaming', () => { fillStandardObjectRelationsMapObjectMetadataId(standardObjects); const CUSTOM_OBJECT = { - namePlural: 'customObjectNamePlural', - nameSingular: 'customObjectNameSingular', - labelPlural: 'customObjectLabelPlural', - labelSingular: 'customObjectLabelSingular', + namePlural: `customObjects${uniqueSuffix}`, + nameSingular: `customObject${uniqueSuffix}`, + labelPlural: `Custom Objects ${uniqueSuffix}`, + labelSingular: `Custom Object ${uniqueSuffix}`, description: 'Custom object description', icon: 'IconListNumbers', isLabelSyncedWithName: false, @@ -135,10 +136,10 @@ describe('Custom object renaming', () => { it('2. should rename custom object', async () => { // Arrange - const HOUSE_NAME_SINGULAR = 'house'; - const HOUSE_NAME_PLURAL = 'houses'; - const HOUSE_LABEL_SINGULAR = 'House'; - const HOUSE_LABEL_PLURAL = 'Houses'; + const HOUSE_NAME_SINGULAR = `house${uniqueSuffix}`; + const HOUSE_NAME_PLURAL = `houses${uniqueSuffix}`; + const HOUSE_LABEL_SINGULAR = `House ${uniqueSuffix}`; + const HOUSE_LABEL_PLURAL = `Houses ${uniqueSuffix}`; // Act const { data } = await updateOneObjectMetadata({