From c87ccfa3c7bd4d411f7b21aa1913a66fc5724fa0 Mon Sep 17 00:00:00 2001 From: Weiko Date: Wed, 28 Aug 2024 19:02:45 +0200 Subject: [PATCH] refactor graphql query runner connection mapper (#6771) --- .../errors/graphql-query-runner.exception.ts | 1 + .../graphql-query-filter-field.parser.spec.ts | 2 +- ...aphql-query-filter-operator.parser.spec.ts | 2 +- .../graphql-query-filter-condition.parser.ts | 0 .../graphql-query-filter-field.parser.ts | 0 .../graphql-query-filter-operator.parser.ts | 0 .../graphql-query-order.parser.spec.ts | 2 +- .../graphql-query-order.parser.ts | 0 ...graphql-selected-fields-relation.parser.ts | 43 ++++ .../graphql-selected-fields.parser.ts | 12 +- .../graphql-query.parser.ts | 8 +- .../graphql-query-runner.service.ts | 14 +- ...ct-records-to-graphql-connection.mapper.ts | 183 ++++++++++++++++++ ...graphql-selected-fields-relation.parser.ts | 79 -------- .../utils/connection.util.ts | 114 ----------- .../utils/cursors.util.ts | 34 ++++ .../get-relation-object-metadata.util.ts | 39 ++++ .../input-type-definition.factory.ts | 6 +- .../factories/input-type.factory.ts | 2 +- .../object-metadata.service.ts | 1 + 20 files changed, 326 insertions(+), 216 deletions(-) rename packages/twenty-server/src/engine/api/graphql/graphql-query-runner/{parsers => graphql-query-parsers}/graphql-query-filter/__tests__/graphql-query-filter-field.parser.spec.ts (93%) rename packages/twenty-server/src/engine/api/graphql/graphql-query-runner/{parsers => graphql-query-parsers}/graphql-query-filter/__tests__/graphql-query-filter-operator.parser.spec.ts (97%) rename packages/twenty-server/src/engine/api/graphql/graphql-query-runner/{parsers => graphql-query-parsers}/graphql-query-filter/graphql-query-filter-condition.parser.ts (100%) rename packages/twenty-server/src/engine/api/graphql/graphql-query-runner/{parsers => graphql-query-parsers}/graphql-query-filter/graphql-query-filter-field.parser.ts (100%) rename packages/twenty-server/src/engine/api/graphql/graphql-query-runner/{parsers => graphql-query-parsers}/graphql-query-filter/graphql-query-filter-operator.parser.ts (100%) rename packages/twenty-server/src/engine/api/graphql/graphql-query-runner/{parsers => graphql-query-parsers}/graphql-query-order/__tests__/graphql-query-order.parser.spec.ts (94%) rename packages/twenty-server/src/engine/api/graphql/graphql-query-runner/{parsers => graphql-query-parsers}/graphql-query-order/graphql-query-order.parser.ts (100%) create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts rename packages/twenty-server/src/engine/api/graphql/graphql-query-runner/{parsers/graphql-selected-fields => graphql-query-parsers/graphql-query-selected-fields}/graphql-selected-fields.parser.ts (88%) rename packages/twenty-server/src/engine/api/graphql/graphql-query-runner/{parsers => graphql-query-parsers}/graphql-query.parser.ts (79%) create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser.ts delete mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/connection.util.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util.ts diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts index 7646b95ff..d7ea0e979 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception.ts @@ -14,4 +14,5 @@ export enum GraphqlQueryRunnerExceptionCode { UNSUPPORTED_OPERATOR = 'UNSUPPORTED_OPERATOR', ARGS_CONFLICT = 'ARGS_CONFLICT', FIELD_NOT_FOUND = 'FIELD_NOT_FOUND', + OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND', } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-field.parser.spec.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/__tests__/graphql-query-filter-field.parser.spec.ts similarity index 93% rename from packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-field.parser.spec.ts rename to packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/__tests__/graphql-query-filter-field.parser.spec.ts index 1924cdc38..bce39127f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-field.parser.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/__tests__/graphql-query-filter-field.parser.spec.ts @@ -2,7 +2,7 @@ import { FindOperator, Not } from 'typeorm'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; -import { GraphqlQueryFilterFieldParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-field.parser'; +import { GraphqlQueryFilterFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; describe('GraphqlQueryFilterFieldParser', () => { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-operator.parser.spec.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/__tests__/graphql-query-filter-operator.parser.spec.ts similarity index 97% rename from packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-operator.parser.spec.ts rename to packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/__tests__/graphql-query-filter-operator.parser.spec.ts index 9f98bff81..7b3d2e5a9 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/__tests__/graphql-query-filter-operator.parser.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/__tests__/graphql-query-filter-operator.parser.spec.ts @@ -12,7 +12,7 @@ import { } from 'typeorm'; import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { GraphqlQueryFilterOperatorParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-operator.parser'; +import { GraphqlQueryFilterOperatorParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser'; describe('GraphqlQueryFilterOperatorParser', () => { let parser: GraphqlQueryFilterOperatorParser; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/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 similarity index 100% rename from packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts rename to packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/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 similarity index 100% rename from packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-field.parser.ts rename to packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts similarity index 100% rename from packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts rename to packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-operator.parser.ts diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/__tests__/graphql-query-order.parser.spec.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/__tests__/graphql-query-order.parser.spec.ts similarity index 94% rename from packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/__tests__/graphql-query-order.parser.spec.ts rename to packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/__tests__/graphql-query-order.parser.spec.ts index ab8a02dbd..8eb5895d3 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/__tests__/graphql-query-order.parser.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/__tests__/graphql-query-order.parser.spec.ts @@ -1,6 +1,6 @@ import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; -import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser'; +import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; import { FieldMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/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 similarity index 100% rename from packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser.ts rename to packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser.ts 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 new file mode 100644 index 000000000..4d2209921 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser.ts @@ -0,0 +1,43 @@ +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; +import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; +import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; + +export class GraphqlQuerySelectedFieldsRelationParser { + private objectMetadataMap: ObjectMetadataMap; + + constructor(objectMetadataMap: ObjectMetadataMap) { + this.objectMetadataMap = objectMetadataMap; + } + + parseRelationField( + fieldMetadata: FieldMetadataInterface, + fieldKey: string, + fieldValue: any, + result: { select: Record; relations: Record }, + ): void { + result.relations[fieldKey] = true; + + if (!fieldValue || typeof fieldValue !== 'object') { + return; + } + + const referencedObjectMetadata = getRelationObjectMetadata( + fieldMetadata, + this.objectMetadataMap, + ); + + const relationFields = referencedObjectMetadata.fields; + const fieldParser = new GraphqlQuerySelectedFieldsParser( + this.objectMetadataMap, + ); + const subResult = fieldParser.parse(fieldValue, relationFields); + + result.select[fieldKey] = { + id: true, + ...subResult.select, + }; + result.relations[fieldKey] = subResult.relations; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-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 similarity index 88% rename from packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser.ts rename to packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts index bc09c12d1..8abcafd6d 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-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 @@ -4,7 +4,7 @@ import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { GraphqlSelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser'; +import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser'; import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; @@ -13,12 +13,12 @@ import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field- import { capitalize } from 'src/utils/capitalize'; import { isPlainObject } from 'src/utils/is-plain-object'; -export class GraphQLSelectedFieldsParser { - private graphqlSelectedFieldsRelationParser: GraphqlSelectedFieldsRelationParser; +export class GraphqlQuerySelectedFieldsParser { + private graphqlQuerySelectedFieldsRelationParser: GraphqlQuerySelectedFieldsRelationParser; constructor(objectMetadataMap: ObjectMetadataMap) { - this.graphqlSelectedFieldsRelationParser = - new GraphqlSelectedFieldsRelationParser(objectMetadataMap); + this.graphqlQuerySelectedFieldsRelationParser = + new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMap); } parse( @@ -57,7 +57,7 @@ export class GraphQLSelectedFieldsParser { } if (isRelationFieldMetadataType(fieldMetadata.type)) { - this.graphqlSelectedFieldsRelationParser.parseRelationField( + this.graphqlQuerySelectedFieldsRelationParser.parseRelationField( fieldMetadata, fieldKey, fieldValue, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts similarity index 79% rename from packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query.parser.ts rename to packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts index 4d83a607a..ed52b4ef4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-query.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts @@ -10,9 +10,9 @@ import { } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; -import { GraphqlQueryFilterConditionParser as GraphqlQueryFilterParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-filter/graphql-query-filter-condition.parser'; -import { GraphqlQueryOrderFieldParser as GraphqlQueryOrderParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query-order/graphql-query-order.parser'; -import { GraphQLSelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser'; +import { GraphqlQueryFilterConditionParser as GraphqlQueryFilterParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser'; +import { GraphqlQueryOrderFieldParser as GraphqlQueryOrderParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; +import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { FieldMetadataMap, ObjectMetadataMap, @@ -61,7 +61,7 @@ export class GraphqlQueryParser { ); } - const selectedFieldsParser = new GraphQLSelectedFieldsParser( + const selectedFieldsParser = new GraphqlQuerySelectedFieldsParser( this.objectMetadataMap, ); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts index b653d8ea7..152d44e8e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts @@ -17,13 +17,11 @@ import { GraphqlQueryRunnerException, GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-query.parser'; +import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; +import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper'; import { applyRangeFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/apply-range-filter.util'; -import { - createConnection, - decodeCursor, -} from 'src/engine/api/graphql/graphql-query-runner/utils/connection.util'; import { convertObjectMetadataToMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; +import { decodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -120,11 +118,15 @@ export class GraphqlQueryRunnerService { const objectRecords = await repository.find(findOptions); - return createConnection( + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap); + + return typeORMObjectRecordsParser.createConnection( (objectRecords as ObjectRecord[]) ?? [], take, totalCount, order, + objectMetadataItem.nameSingular, ); } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts new file mode 100644 index 000000000..eecf0c0e1 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts @@ -0,0 +1,183 @@ +import { FindOptionsOrderValue } from 'typeorm'; + +import { Record as IRecord } 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 { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { CONNECTION_MAX_DEPTH } from 'src/engine/api/graphql/graphql-query-runner/constants/connection-max-depth.constant'; +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; +import { encodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; +import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; +import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { FieldMetadataType } 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 { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { isPlainObject } from 'src/utils/is-plain-object'; + +export class ObjectRecordsToGraphqlConnectionMapper { + private objectMetadataMap: ObjectMetadataMap; + + constructor(objectMetadataMap: ObjectMetadataMap) { + this.objectMetadataMap = objectMetadataMap; + } + + public createConnection( + objectRecords: ObjectRecord[], + take: number, + totalCount: number, + order: Record | undefined, + objectName: string, + depth = 0, + ): IConnection { + const edges = (objectRecords ?? []).map((objectRecord) => ({ + node: this.processRecord( + objectRecord, + take, + totalCount, + order, + objectName, + depth, + ), + cursor: encodeCursor(objectRecord, order), + })); + + return { + edges, + pageInfo: { + hasNextPage: objectRecords.length === take && totalCount > take, + hasPreviousPage: false, + startCursor: edges[0]?.cursor, + endCursor: edges[edges.length - 1]?.cursor, + }, + totalCount: totalCount, + }; + } + + private processRecord>( + objectRecord: T, + take: number, + totalCount: number, + order: Record | undefined, + objectName: string, + depth = 0, + ): T { + if (depth >= CONNECTION_MAX_DEPTH) { + throw new GraphqlQueryRunnerException( + `Maximum depth of ${CONNECTION_MAX_DEPTH} reached`, + GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED, + ); + } + + const objectMetadata = this.objectMetadataMap[objectName]; + + if (!objectMetadata) { + throw new GraphqlQueryRunnerException( + `Object metadata not found for ${objectName}`, + GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, + ); + } + + const processedObjectRecord: Record = {}; + + for (const [key, value] of Object.entries(objectRecord)) { + const fieldMetadata = objectMetadata.fields[key]; + + if (!fieldMetadata) { + processedObjectRecord[key] = value; + continue; + } + + if (isRelationFieldMetadataType(fieldMetadata.type)) { + if (Array.isArray(value)) { + processedObjectRecord[key] = this.createConnection( + value, + take, + value.length, + order, + getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap) + .nameSingular, + depth + 1, + ); + } else if (isPlainObject(value)) { + processedObjectRecord[key] = this.processRecord( + value, + take, + totalCount, + order, + getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap) + .nameSingular, + depth + 1, + ); + } + } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { + processedObjectRecord[key] = this.processCompositeField( + fieldMetadata, + value, + ); + } else { + processedObjectRecord[key] = this.formatFieldValue( + value, + fieldMetadata.type, + ); + } + } + + return processedObjectRecord as T; + } + + private processCompositeField( + fieldMetadata: FieldMetadataInterface, + fieldValue: any, + ): Record { + const compositeType = compositeTypeDefinitions.get( + fieldMetadata.type as CompositeFieldMetadataType, + ); + + if (!compositeType) { + throw new Error( + `Composite type definition not found for type: ${fieldMetadata.type}`, + ); + } + + return Object.entries(fieldValue).reduce( + (acc, [subFieldKey, subFieldValue]) => { + if (subFieldKey === '__typename') return acc; + + const subFieldMetadata = compositeType.properties.find( + (property) => property.name === subFieldKey, + ); + + if (!subFieldMetadata) { + throw new Error( + `Sub field metadata not found for composite type: ${fieldMetadata.type}`, + ); + } + + acc[subFieldKey] = this.formatFieldValue( + subFieldValue, + subFieldMetadata.type, + ); + + return acc; + }, + {} as Record, + ); + } + + private formatFieldValue(value: any, fieldType: FieldMetadataType) { + switch (fieldType) { + case FieldMetadataType.RAW_JSON: + return value ? JSON.stringify(value) : value; + case FieldMetadataType.DATE: + case FieldMetadataType.DATE_TIME: + return value instanceof Date ? value.toISOString() : value; + default: + return value; + } + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser.ts deleted file mode 100644 index c4648850a..000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields-relation.parser.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - -import { GraphQLSelectedFieldsParser } from 'src/engine/api/graphql/graphql-query-runner/parsers/graphql-selected-fields/graphql-selected-fields.parser'; -import { - ObjectMetadataMap, - ObjectMetadataMapItem, -} from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; -import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -import { - deduceRelationDirection, - RelationDirection, -} from 'src/engine/utils/deduce-relation-direction.util'; - -export class GraphqlSelectedFieldsRelationParser { - private objectMetadataMap: ObjectMetadataMap; - - constructor(objectMetadataMap: ObjectMetadataMap) { - this.objectMetadataMap = objectMetadataMap; - } - - parseRelationField( - fieldMetadata: FieldMetadataInterface, - fieldKey: string, - fieldValue: any, - result: { select: Record; relations: Record }, - ): void { - result.relations[fieldKey] = true; - - if (!fieldValue || typeof fieldValue !== 'object') { - return; - } - - const relationMetadata = - fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; - - if (!relationMetadata) { - throw new Error( - `Relation metadata not found for field ${fieldMetadata.name}`, - ); - } - - const relationDirection = deduceRelationDirection( - fieldMetadata, - relationMetadata, - ); - const referencedObjectMetadata = this.getReferencedObjectMetadata( - relationMetadata, - relationDirection, - ); - - const relationFields = referencedObjectMetadata.fields; - const fieldParser = new GraphQLSelectedFieldsParser(this.objectMetadataMap); - const subResult = fieldParser.parse(fieldValue, relationFields); - - result.select[fieldKey] = { - id: true, - ...subResult.select, - }; - result.relations[fieldKey] = subResult.relations; - } - - private getReferencedObjectMetadata( - relationMetadata: RelationMetadataEntity, - relationDirection: RelationDirection, - ): ObjectMetadataMapItem { - const referencedObjectMetadata = - relationDirection === RelationDirection.TO - ? this.objectMetadataMap[relationMetadata.fromObjectMetadataId] - : this.objectMetadataMap[relationMetadata.toObjectMetadataId]; - - if (!referencedObjectMetadata) { - throw new Error( - `Referenced object metadata not found for relation ${relationMetadata.id}`, - ); - } - - return referencedObjectMetadata; - } -} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/connection.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/connection.util.ts deleted file mode 100644 index 7adfc7944..000000000 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/connection.util.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { FindOptionsOrderValue } from 'typeorm'; - -import { Record as IRecord } 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 { CONNECTION_MAX_DEPTH } from 'src/engine/api/graphql/graphql-query-runner/constants/connection-max-depth.constant'; -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { isPlainObject } from 'src/utils/is-plain-object'; - -export const createConnection = ( - objectRecords: ObjectRecord[], - take: number, - totalCount: number, - order: Record | undefined, - depth = 0, -): IConnection => { - const edges = (objectRecords ?? []).map((objectRecord) => ({ - node: processNestedConnections( - objectRecord, - take, - totalCount, - order, - depth, - ), - cursor: encodeCursor(objectRecord, order), - })); - - return { - edges, - pageInfo: { - hasNextPage: objectRecords.length === take && totalCount > take, - hasPreviousPage: false, - startCursor: edges[0]?.cursor, - endCursor: edges[edges.length - 1]?.cursor, - }, - totalCount: totalCount, - }; -}; - -const processNestedConnections = >( - objectRecord: T, - take: number, - totalCount: number, - order: Record | undefined, - depth = 0, -): T => { - if (depth >= CONNECTION_MAX_DEPTH) { - throw new GraphqlQueryRunnerException( - `Maximum depth of ${CONNECTION_MAX_DEPTH} reached`, - GraphqlQueryRunnerExceptionCode.MAX_DEPTH_REACHED, - ); - } - - const processedObjectRecords: Record = { ...objectRecord }; - - for (const [key, value] of Object.entries(objectRecord)) { - if (Array.isArray(value)) { - if (value.length > 0 && typeof value[0] !== 'object') { - processedObjectRecords[key] = value; - } else { - processedObjectRecords[key] = createConnection( - value, - take, - value.length, - order, - depth + 1, - ); - } - } else if (value instanceof Date) { - processedObjectRecords[key] = value.toISOString(); - } else if (isPlainObject(value)) { - processedObjectRecords[key] = processNestedConnections( - value, - take, - totalCount, - order, - depth + 1, - ); - } else { - processedObjectRecords[key] = value; - } - } - - return processedObjectRecords as T; -}; - -export const decodeCursor = (cursor: string): Record => { - try { - return JSON.parse(Buffer.from(cursor, 'base64').toString()); - } catch (err) { - throw new GraphqlQueryRunnerException( - `Invalid cursor: ${cursor}`, - GraphqlQueryRunnerExceptionCode.INVALID_CURSOR, - ); - } -}; - -export const encodeCursor = ( - objectRecord: ObjectRecord, - order: Record | undefined, -): string => { - const cursor = {}; - - Object.keys(order ?? []).forEach((key) => { - cursor[key] = objectRecord[key]; - }); - - cursor['id'] = objectRecord.id; - - return Buffer.from(JSON.stringify(Object.values(cursor))).toString('base64'); -}; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts new file mode 100644 index 000000000..5adc036e4 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts @@ -0,0 +1,34 @@ +import { FindOptionsOrderValue } from 'typeorm'; + +import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; + +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; + +export const decodeCursor = (cursor: string): Record => { + try { + return JSON.parse(Buffer.from(cursor, 'base64').toString()); + } catch (err) { + throw new GraphqlQueryRunnerException( + `Invalid cursor: ${cursor}`, + GraphqlQueryRunnerExceptionCode.INVALID_CURSOR, + ); + } +}; + +export const encodeCursor = ( + objectRecord: ObjectRecord, + order: Record | undefined, +): string => { + const cursor = {}; + + Object.keys(order ?? []).forEach((key) => { + cursor[key] = objectRecord[key]; + }); + + cursor['id'] = objectRecord.id; + + return Buffer.from(JSON.stringify(Object.values(cursor))).toString('base64'); +}; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util.ts new file mode 100644 index 000000000..4b5028032 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util.ts @@ -0,0 +1,39 @@ +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { ObjectMetadataMap } from 'src/engine/api/graphql/graphql-query-runner/utils/convert-object-metadata-to-map.util'; +import { + deduceRelationDirection, + RelationDirection, +} from 'src/engine/utils/deduce-relation-direction.util'; + +export const getRelationObjectMetadata = ( + fieldMetadata: FieldMetadataInterface, + objectMetadataMap: ObjectMetadataMap, +) => { + const relationMetadata = + fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata; + + if (!relationMetadata) { + throw new Error( + `Relation metadata not found for field ${fieldMetadata.name}`, + ); + } + + const relationDirection = deduceRelationDirection( + fieldMetadata, + relationMetadata, + ); + + const referencedObjectMetadata = + relationDirection === RelationDirection.TO + ? objectMetadataMap[relationMetadata.fromObjectMetadataId] + : objectMetadataMap[relationMetadata.toObjectMetadataId]; + + if (!referencedObjectMetadata) { + throw new Error( + `Referenced object metadata not found for relation ${relationMetadata.id}`, + ); + } + + return referencedObjectMetadata; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts index 083c7208f..7db06ad36 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts @@ -5,11 +5,11 @@ import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; -import { pascalCase } from 'src/utils/pascal-case'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { FieldMetadataType } 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 { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { pascalCase } from 'src/utils/pascal-case'; import { InputTypeFactory } from './input-type.factory'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts index 880dd3387..5f2d696a0 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type.factory.ts @@ -4,13 +4,13 @@ import { GraphQLInputObjectType, GraphQLInputType, GraphQLList } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; +import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type'; import { TypeMapperService, TypeOptions, } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type'; import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; import { InputTypeDefinitionKind } from './input-type-definition.factory'; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 53df8a651..8661de4eb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -209,6 +209,7 @@ export class ObjectMetadataService extends TypeOrmQueryService