From c7efc995802f354618bc559ea0013791f353f569 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 13 Nov 2024 17:59:35 +0100 Subject: [PATCH] WIP --- .../person-query-result-getter.handler.ts | 2 + .../query-result-getter-handler.interface.ts | 7 +- .../query-result-getters.factory.ts | 197 +++++++++++++++--- .../utils/isResultAConnection.ts | 28 +++ .../workspace-query-runner.module.ts | 2 + .../log-execution-time.decorator.ts | 10 +- .../relation-metadata.module.ts | 2 + .../relation-metadata.service.ts | 22 +- .../workspace-metadata-cache.service.ts | 34 ++- 9 files changed, 254 insertions(+), 50 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/utils/isResultAConnection.ts diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler.ts index 50bedd2bbc1c..acc12cf7e548 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/person-query-result-getter.handler.ts @@ -12,6 +12,8 @@ export class PersonQueryResultGetterHandler person: PersonWorkspaceEntity, workspaceId: string, ): Promise { + console.log('PersonQueryResultGetterHandler.handle'); + if (!person.id || !person?.avatarUrl) { return person; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface.ts index 2218afcde746..2326ab069303 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface.ts @@ -1,3 +1,8 @@ +import { Record as ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; + export interface QueryResultGetterHandlerInterface { - handle(result: any, workspaceId: string): Promise; + handle( + objectRecord: ObjectRecord, + workspaceId: string, + ): Promise; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts index 1f3b72b762a6..2a8c78c902b0 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory.ts @@ -1,19 +1,41 @@ import { Injectable } from '@nestjs/common'; +import { Record as ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; import { QueryResultGetterHandlerInterface } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-getter-handler.interface'; +import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; +import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { ActivityQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/activity-query-result-getter.handler'; import { AttachmentQueryResultGetterHandler } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/handlers/attachment-query-result-getter.handler'; 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 { + isPossibleFieldValueAConnection, + isPossibleFieldValueARecordArray, +} from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/utils/isResultAConnection'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; +import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; +import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +export type PossibleFieldValue = + | IConnection + | { records: ObjectRecord[] } + | ObjectRecord; + +// 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 +// Right now the factory will override any change made on relations by the handlers @Injectable() export class QueryResultGettersFactory { private handlers: Map; - constructor(private readonly fileService: FileService) { + constructor( + private readonly fileService: FileService, + private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, + ) { this.initializeHandlers(); } @@ -30,37 +52,160 @@ export class QueryResultGettersFactory { ]); } + async processConnection( + connection: IConnection, + objectMetadataItemId: string, + objectMetadataMap: ObjectMetadataMap, + workspaceId: string, + ): Promise { + return { + ...connection, + edges: await Promise.all( + connection.edges.map(async (edge: IEdge) => ({ + ...edge, + node: await this.processRecord( + edge.node, + objectMetadataItemId, + objectMetadataMap, + workspaceId, + ), + })), + ), + }; + } + + async processRecordArray( + result: { records: ObjectRecord[] }, + objectMetadataItemId: string, + objectMetadataMap: ObjectMetadataMap, + workspaceId: string, + ) { + return { + ...result, + records: await Promise.all( + result.records.map( + async (record: ObjectRecord) => + await this.processRecord( + record, + objectMetadataItemId, + objectMetadataMap, + workspaceId, + ), + ), + ), + }; + } + + async processRecord( + record: ObjectRecord, + objectMetadataItemId: string, + objectMetadataMap: ObjectMetadataMap, + workspaceId: string, + ): Promise { + const objectMetadataMapItem = objectMetadataMap[objectMetadataItemId]; + + const handler = this.getHandler(objectMetadataMapItem.nameSingular); + + const relationFields = Object.keys(record) + .map((recordFieldName) => objectMetadataMapItem.fields[recordFieldName]) + .filter((fieldMetadata) => + isRelationFieldMetadataType(fieldMetadata.type), + ); + + // console.log({ + // relationFields, + // }); + + // const relationFieldsProcessedMap = relationFields.reduce< + // Record + // >((relationFieldMap, fieldMetadata) => { + // const relationMetadata = + // fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; + + // if (!isDefined(relationMetadata)) { + // throw new Error('Relation metadata is not defined'); + // } + + // // TODO: computing this by taking the opposite of the current object metadata id + // // is really less than ideal. This should be computed based on the relation metadata + // // But right now it is too complex with the current structure and / or lack of utils + // // around the possible combinations with relation metadata from / to + MANY_TO_ONE / ONE_TO_MANY + // const relationObjectMetadataItemId = + // relationMetadata.fromObjectMetadataId === objectMetadataItemId + // ? relationMetadata.toObjectMetadataId + // : relationMetadata.fromObjectMetadataId; + + // const relationObjectMetadataItem = + // objectMetadataMap[relationObjectMetadataItemId]; + + // if (!isDefined(relationObjectMetadataItem)) { + // throw new Error( + // `Object metadata not found for id ${relationObjectMetadataItemId}`, + // ); + // } + + // relationFieldMap[fieldMetadata.name] = this.processRecord( + // record[fieldMetadata.name], + // relationObjectMetadataItem.id, + // objectMetadataMap, + // workspaceId, + // ); + + // return relationFieldMap; + // }, {}); + + const objectRecordProcessedWithoutRelationFields = await handler.handle( + record, + workspaceId, + ); + + const newRecord = { + ...objectRecordProcessedWithoutRelationFields, + // ...relationFieldsProcessedMap, + }; + + return newRecord; + } + + @LogExecutionTime('QueryResultGettersFactory.create') async create( - result: any, + result: PossibleFieldValue, objectMetadataItem: ObjectMetadataInterface, workspaceId: string, ): Promise { - const handler = this.getHandler(objectMetadataItem.nameSingular); - - if (result.edges) { - return { - ...result, - edges: await Promise.all( - result.edges.map(async (edge: any) => ({ - ...edge, - node: await handler.handle(edge.node, workspaceId), - })), - ), - }; - } + const objectMetadataMap = + await this.workspaceMetadataCacheService.getWorkspaceObjectMetadataMap( + workspaceId, + ); - if (result.records) { - return { - ...result, - records: await Promise.all( - result.records.map( - async (item: any) => await handler.handle(item, workspaceId), - ), - ), - }; - } + // console.log( + // 'QueryResultGettersFactory.create', + // objectMetadataItem.nameSingular, + // result, + // ); - return await handler.handle(result, workspaceId); + if (isPossibleFieldValueAConnection(result)) { + return await this.processConnection( + result, + objectMetadataItem.id, + objectMetadataMap, + workspaceId, + ); + } else if (isPossibleFieldValueARecordArray(result)) { + return await this.processRecordArray( + result, + objectMetadataItem.id, + objectMetadataMap, + workspaceId, + ); + } else { + return await this.processRecord( + result, + objectMetadataItem.id, + objectMetadataMap, + workspaceId, + ); + } } private getHandler(objectType: string): QueryResultGetterHandlerInterface { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/utils/isResultAConnection.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/utils/isResultAConnection.ts new file mode 100644 index 000000000000..7c55456ffa45 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/utils/isResultAConnection.ts @@ -0,0 +1,28 @@ +import { isDefined } from 'class-validator'; + +import { Record as ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; +import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface'; + +import { PossibleFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory'; + +export const isPossibleFieldValueAConnection = ( + result: PossibleFieldValue, +): result is IConnection> => { + return isDefined((result as any).edges); +}; + +export const isPossibleFieldValueARecordArray = ( + result: PossibleFieldValue, +): result is { records: ObjectRecord[] } => { + return Array.isArray((result as any).records); +}; + +export const isPossibleFieldValueARecord = ( + result: PossibleFieldValue, +): result is ObjectRecord => { + return ( + !isPossibleFieldValueAConnection(result) && + !isPossibleFieldValueARecordArray(result) + ); +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts index 8c348e726ff5..9368bd574bf1 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts @@ -12,6 +12,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module'; +import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -20,6 +21,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen @Module({ imports: [ + WorkspaceMetadataCacheModule, AuthModule, WorkspaceQueryBuilderModule, WorkspaceDataSourceModule, diff --git a/packages/twenty-server/src/engine/decorators/observability/log-execution-time.decorator.ts b/packages/twenty-server/src/engine/decorators/observability/log-execution-time.decorator.ts index 6f9fbe560fad..5625b98cf6a1 100644 --- a/packages/twenty-server/src/engine/decorators/observability/log-execution-time.decorator.ts +++ b/packages/twenty-server/src/engine/decorators/observability/log-execution-time.decorator.ts @@ -1,11 +1,13 @@ import { Logger } from '@nestjs/common'; +import { isDefined } from 'class-validator'; + /** * A decorator function that logs the execution time of the decorated method. * * @returns The modified property descriptor with the execution time logging functionality. */ -export function LogExecutionTime() { +export function LogExecutionTime(label?: string | undefined) { return function ( target: any, propertyKey: string, @@ -21,7 +23,11 @@ export function LogExecutionTime() { const end = performance.now(); const executionTime = end - start; - logger.log(`Execution time: ${executionTime.toFixed(2)}ms`); + if (isDefined(label)) { + logger.log(`${label} execution time: ${executionTime.toFixed(2)}ms`); + } else { + logger.log(`Execution time: ${executionTime.toFixed(2)}ms`); + } return result; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.module.ts index b1c954e8a245..557f61cf8969 100644 --- a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.module.ts @@ -13,6 +13,7 @@ import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/ import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { RelationMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/relation-metadata/interceptors/relation-metadata-graphql-api-exception.interceptor'; import { RelationMetadataResolver } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.resolver'; +import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @@ -39,6 +40,7 @@ import { RelationMetadataDTO } from './dtos/relation-metadata.dto'; WorkspaceMigrationModule, WorkspaceCacheStorageModule, WorkspaceMetadataVersionModule, + WorkspaceMetadataCacheModule, ], services: [RelationMetadataService], resolvers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts index d9a1850a5df6..89fcabeeb633 100644 --- a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts @@ -24,6 +24,7 @@ import { import { InvalidStringException } from 'src/engine/metadata-modules/utils/exceptions/invalid-string.exception'; import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; import { validateMetadataNameValidityOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-validity.utils'; +import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { @@ -33,7 +34,6 @@ import { } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { BASE_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; @@ -55,7 +55,7 @@ export class RelationMetadataService extends TypeOrmQueryService, workspaceId: string, ): Promise<(RelationMetadataEntity | NotFoundException)[]> { - const metadataVersion = - await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); - - if (!metadataVersion) { - throw new NotFoundException( - `Metadata version not found for workspace ${workspaceId}`, - ); - } - const objectMetadataMap = - await this.workspaceCacheStorageService.getObjectMetadataMap( + await this.workspaceMetadataCacheService.getWorkspaceObjectMetadataMap( workspaceId, - metadataVersion, ); - if (!objectMetadataMap) { - throw new NotFoundException( - `Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`, - ); - } - const mappedResult = fieldMetadataItems.map((fieldMetadataItem) => { const objectMetadata = objectMetadataMap[fieldMetadataItem.objectMetadataId]; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts index c3d454c9450b..458b1afb9c39 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -6,7 +6,10 @@ import { Repository } from 'typeorm'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { + generateObjectMetadataMap, + ObjectMetadataMap, +} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; import { WorkspaceMetadataCacheException, WorkspaceMetadataCacheExceptionCode, @@ -102,6 +105,33 @@ export class WorkspaceMetadataCacheService { ); } + async getWorkspaceObjectMetadataMap( + workspaceId: string, + ): Promise { + const metadataVersion = + await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); + + if (!metadataVersion) { + throw new NotFoundException( + `Metadata version not found for workspace ${workspaceId}`, + ); + } + + const objectMetadataMap = + await this.workspaceCacheStorageService.getObjectMetadataMap( + workspaceId, + metadataVersion, + ); + + if (!objectMetadataMap) { + throw new NotFoundException( + `Object metadata map not found for workspace ${workspaceId} and metadata version ${metadataVersion}`, + ); + } + + return objectMetadataMap; + } + private async getMetadataVersionFromDatabase( workspaceId: string, ): Promise {