From 07197d1e6d2ca48ced843f74cffc127f1d682039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Wed, 29 Jan 2025 17:04:39 +0100 Subject: [PATCH] feat: new relation in graphql-query-runner (#9883) Fix https://github.com/twentyhq/core-team-issues/issues/300 Within GraphQLQueryRunner the new relation format will be used when the feature flag `IsNewRelationEnabled` is set to true. --- .../errors/graphql-query-runner.exception.ts | 2 + .../graphql-query-filter-condition.parser.ts | 7 +- .../graphql-query-filter-field.parser.ts | 8 +- .../graphql-query-order.parser.ts | 8 +- ...graphql-selected-fields-relation.parser.ts | 25 +- .../graphql-selected-fields.parser.ts | 11 +- .../graphql-query.parser.ts | 7 + .../graphql-query-runner.module.ts | 11 +- ...ct-records-to-graphql-connection.helper.ts | 37 +- .../helpers/process-aggregate.helper.ts | 3 + .../process-nested-relations-v2.helper.ts | 347 ++++++++++++++++++ .../process-nested-relations.helper.ts | 38 +- .../interfaces/base-resolver-service.ts | 12 + ...phql-query-create-many-resolver.service.ts | 15 +- ...aphql-query-create-one-resolver.service.ts | 15 +- ...phql-query-delete-many-resolver.service.ts | 15 +- ...aphql-query-delete-one-resolver.service.ts | 15 +- ...hql-query-destroy-many-resolver.service.ts | 15 +- ...phql-query-destroy-one-resolver.service.ts | 15 +- ...-query-find-duplicates-resolver.service.ts | 13 +- ...raphql-query-find-many-resolver.service.ts | 32 +- ...graphql-query-find-one-resolver.service.ts | 15 +- ...hql-query-restore-many-resolver.service.ts | 15 +- ...phql-query-restore-one-resolver.service.ts | 15 +- .../graphql-query-search-resolver.service.ts | 15 +- ...phql-query-update-many-resolver.service.ts | 15 +- ...aphql-query-update-one-resolver.service.ts | 15 +- .../utils/get-target-object-metadata.util.ts | 31 ++ .../query-result-getters.factory.ts | 87 +++-- .../find-records.workflow-action.ts | 6 + .../record-crud/record-crud-action.module.ts | 2 + 31 files changed, 729 insertions(+), 138 deletions(-) create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-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 747a56308..d147d51ea 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 @@ -21,4 +21,6 @@ export enum GraphqlQueryRunnerExceptionCode { INVALID_ARGS_LAST = 'INVALID_ARGS_LAST', METADATA_CACHE_VERSION_NOT_FOUND = 'METADATA_CACHE_VERSION_NOT_FOUND', METADATA_CACHE_FEATURE_FLAG_RECOMPUTATION_REQUIRED = 'METADATA_CACHE_FEATURE_FLAG_RECOMPUTATION_REQUIRED', + RELATION_SETTINGS_NOT_FOUND = 'RELATION_SETTINGS_NOT_FOUND', + RELATION_TARGET_OBJECT_METADATA_NOT_FOUND = 'RELATION_TARGET_OBJECT_METADATA_NOT_FOUND', } 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 2b7949613..36fa98e7e 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 @@ -6,6 +6,7 @@ import { } from 'typeorm'; import { ObjectRecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; @@ -15,10 +16,14 @@ export class GraphqlQueryFilterConditionParser { private fieldMetadataMapByName: FieldMetadataMap; private queryFilterFieldParser: GraphqlQueryFilterFieldParser; - constructor(fieldMetadataMapByName: FieldMetadataMap) { + constructor( + fieldMetadataMapByName: FieldMetadataMap, + featureFlagsMap: FeatureFlagMap, + ) { this.fieldMetadataMapByName = fieldMetadataMapByName; this.queryFilterFieldParser = new GraphqlQueryFilterFieldParser( this.fieldMetadataMapByName, + featureFlagsMap, ); } 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 4eed5b10a..7e0fefa89 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,6 +1,7 @@ import { capitalize } from 'twenty-shared'; import { WhereExpressionBuilder } from 'typeorm'; +import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { @@ -17,9 +18,14 @@ const ARRAY_OPERATORS = ['in', 'contains', 'notContains']; export class GraphqlQueryFilterFieldParser { private fieldMetadataMapByName: FieldMetadataMap; + private featureFlagsMap: FeatureFlagMap; - constructor(fieldMetadataMapByName: FieldMetadataMap) { + constructor( + fieldMetadataMapByName: FieldMetadataMap, + featureFlagsMap: FeatureFlagMap, + ) { this.fieldMetadataMapByName = fieldMetadataMapByName; + this.featureFlagsMap = featureFlagsMap; } public parse( 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 0ae00ec56..61e7e1ee7 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 @@ -4,6 +4,7 @@ import { ObjectRecordOrderBy, OrderByDirection, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { @@ -16,9 +17,14 @@ import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metada import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; export class GraphqlQueryOrderFieldParser { private fieldMetadataMapByName: FieldMetadataMap; + private featureFlagsMap: FeatureFlagMap; - constructor(fieldMetadataMapByName: FieldMetadataMap) { + constructor( + fieldMetadataMapByName: FieldMetadataMap, + featureFlagsMap: FeatureFlagMap, + ) { this.fieldMetadataMapByName = fieldMetadataMapByName; + this.featureFlagsMap = featureFlagsMap; } parse( 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 54f294acb..6693cfeb9 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,3 +1,4 @@ +import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { @@ -5,13 +6,20 @@ import { GraphqlQuerySelectedFieldsResult, } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { getRelationObjectMetadata } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; +import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; export class GraphqlQuerySelectedFieldsRelationParser { private objectMetadataMaps: ObjectMetadataMaps; + private featureFlagsMap: FeatureFlagMap; - constructor(objectMetadataMaps: ObjectMetadataMaps) { + constructor( + objectMetadataMaps: ObjectMetadataMaps, + featureFlagsMap: FeatureFlagMap, + ) { this.objectMetadataMaps = objectMetadataMaps; + this.featureFlagsMap = featureFlagsMap; } parseRelationField( @@ -26,16 +34,19 @@ export class GraphqlQuerySelectedFieldsRelationParser { accumulator.relations[fieldKey] = true; - const referencedObjectMetadata = getRelationObjectMetadata( - fieldMetadata, - this.objectMetadataMaps, - ); + const isNewRelationEnabled = + this.featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled]; - const relationFields = referencedObjectMetadata.fieldsByName; + const targetObjectMetadata = isNewRelationEnabled + ? getTargetObjectMetadataOrThrow(fieldMetadata, this.objectMetadataMaps) + : getRelationObjectMetadata(fieldMetadata, this.objectMetadataMaps); + + const targetFields = targetObjectMetadata.fieldsByName; const fieldParser = new GraphqlQuerySelectedFieldsParser( this.objectMetadataMaps, + this.featureFlagsMap, ); - const relationAccumulator = fieldParser.parse(fieldValue, relationFields); + const relationAccumulator = fieldParser.parse(fieldValue, targetFields); accumulator.select[fieldKey] = { id: true, 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 a2c2b8916..fd1c97296 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 @@ -1,5 +1,6 @@ import { capitalize } from 'twenty-shared'; +import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser'; @@ -20,9 +21,15 @@ export class GraphqlQuerySelectedFieldsParser { private graphqlQuerySelectedFieldsRelationParser: GraphqlQuerySelectedFieldsRelationParser; private aggregateParser: GraphqlQuerySelectedFieldsAggregateParser; - constructor(objectMetadataMaps: ObjectMetadataMaps) { + constructor( + objectMetadataMaps: ObjectMetadataMaps, + featureFlagsMap: FeatureFlagMap, + ) { this.graphqlQuerySelectedFieldsRelationParser = - new GraphqlQuerySelectedFieldsRelationParser(objectMetadataMaps); + new GraphqlQuerySelectedFieldsRelationParser( + objectMetadataMaps, + featureFlagsMap, + ); this.aggregateParser = new GraphqlQuerySelectedFieldsAggregateParser(); } 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 9dc415fa2..f15acd743 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 @@ -9,6 +9,7 @@ import { ObjectRecordFilter, ObjectRecordOrderBy, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; 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'; @@ -26,18 +27,23 @@ export class GraphqlQueryParser { private objectMetadataMaps: ObjectMetadataMaps; private filterConditionParser: GraphqlQueryFilterConditionParser; private orderFieldParser: GraphqlQueryOrderFieldParser; + private featureFlagsMap: FeatureFlagMap; constructor( fieldMetadataMapByName: FieldMetadataMap, objectMetadataMaps: ObjectMetadataMaps, + featureFlagsMap: FeatureFlagMap, ) { this.objectMetadataMaps = objectMetadataMaps; this.fieldMetadataMapByName = fieldMetadataMapByName; + this.featureFlagsMap = featureFlagsMap; this.filterConditionParser = new GraphqlQueryFilterConditionParser( this.fieldMetadataMapByName, + featureFlagsMap, ); this.orderFieldParser = new GraphqlQueryOrderFieldParser( this.fieldMetadataMapByName, + featureFlagsMap, ); } @@ -122,6 +128,7 @@ export class GraphqlQueryParser { const selectedFieldsParser = new GraphqlQuerySelectedFieldsParser( this.objectMetadataMaps, + this.featureFlagsMap, ); return selectedFieldsParser.parse(graphqlSelectedFields, parentFields); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts index a3384e52a..6ac2d94ac 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts @@ -1,5 +1,8 @@ import { Module } from '@nestjs/common'; +import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; +import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper'; +import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service'; import { GraphqlQueryCreateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service'; import { GraphqlQueryDeleteManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service'; @@ -42,7 +45,13 @@ const graphqlQueryResolvers = [ WorkspaceQueryRunnerModule, FeatureFlagModule, ], - providers: [ApiEventEmitterService, ...graphqlQueryResolvers], + providers: [ + ApiEventEmitterService, + ProcessNestedRelationsHelper, + ProcessNestedRelationsV2Helper, + ProcessAggregateHelper, + ...graphqlQueryResolvers, + ], exports: [...graphqlQueryResolvers], }) export class GraphqlQueryRunnerModule {} 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 3915f4efc..bcf2a5fef 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 @@ -5,6 +5,7 @@ import { ObjectRecordOrderBy, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; +import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.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'; @@ -14,7 +15,9 @@ import { } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; 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 { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; 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'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @@ -26,9 +29,14 @@ import { isPlainObject } from 'src/utils/is-plain-object'; export class ObjectRecordsToGraphqlConnectionHelper { private objectMetadataMaps: ObjectMetadataMaps; + private featureFlagsMap: FeatureFlagMap; - constructor(objectMetadataMaps: ObjectMetadataMaps) { + constructor( + objectMetadataMaps: ObjectMetadataMaps, + featureFlagsMap: FeatureFlagMap, + ) { this.objectMetadataMaps = objectMetadataMaps; + this.featureFlagsMap = featureFlagsMap; } public createConnection({ @@ -146,6 +154,9 @@ export class ObjectRecordsToGraphqlConnectionHelper { ); } + const isNewRelationEnabled = + this.featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled]; + const objectMetadata = getObjectMetadataMapItemByNameSingular( this.objectMetadataMaps, objectName, @@ -170,6 +181,13 @@ export class ObjectRecordsToGraphqlConnectionHelper { if (isRelationFieldMetadataType(fieldMetadata.type)) { if (Array.isArray(value)) { + const targetObjectMetadata = isNewRelationEnabled + ? getTargetObjectMetadataOrThrow( + fieldMetadata, + this.objectMetadataMaps, + ) + : getRelationObjectMetadata(fieldMetadata, this.objectMetadataMaps); + processedObjectRecord[key] = this.createConnection({ objectRecords: value, parentObjectRecord: objectRecord, @@ -177,10 +195,7 @@ export class ObjectRecordsToGraphqlConnectionHelper { objectRecordsAggregatedValues[fieldMetadata.name], selectedAggregatedFields: selectedAggregatedFields[fieldMetadata.name], - objectName: getRelationObjectMetadata( - fieldMetadata, - this.objectMetadataMaps, - ).nameSingular, + objectName: targetObjectMetadata.nameSingular, take, totalCount: objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ?? @@ -191,16 +206,20 @@ export class ObjectRecordsToGraphqlConnectionHelper { depth: depth + 1, }); } else if (isPlainObject(value)) { + const targetObjectMetadata = isNewRelationEnabled + ? getTargetObjectMetadataOrThrow( + fieldMetadata, + this.objectMetadataMaps, + ) + : getRelationObjectMetadata(fieldMetadata, this.objectMetadataMaps); + processedObjectRecord[key] = this.processRecord({ objectRecord: value, objectRecordsAggregatedValues: objectRecordsAggregatedValues[fieldMetadata.name], selectedAggregatedFields: selectedAggregatedFields[fieldMetadata.name], - objectName: getRelationObjectMetadata( - fieldMetadata, - this.objectMetadataMaps, - ).nameSingular, + objectName: targetObjectMetadata.nameSingular, take, totalCount, order, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts index c737ceb6a..c5fad015f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper.ts @@ -1,3 +1,5 @@ +import { Injectable } from '@nestjs/common'; + import { SelectQueryBuilder } from 'typeorm'; import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; @@ -5,6 +7,7 @@ import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builde import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util'; import { isDefined } from 'src/utils/is-defined'; +@Injectable() export class ProcessAggregateHelper { public addSelectedAggregatedFieldsQueriesToQueryBuilder = ({ selectedAggregatedFields, 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 new file mode 100644 index 000000000..b1005ccda --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts @@ -0,0 +1,347 @@ +import { Injectable } from '@nestjs/common'; + +import { + DataSource, + FindOptionsRelations, + ObjectLiteral, + SelectQueryBuilder, +} from 'typeorm'; + +import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} 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 { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; +import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.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 { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; +import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; +import { isRelationFieldMetadata } from 'src/engine/utils/is-relation-field-metadata.util'; + +@Injectable() +export class ProcessNestedRelationsV2Helper { + constructor( + private readonly processAggregateHelper: ProcessAggregateHelper, + ) {} + + public async processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem, + parentObjectRecords, + parentObjectRecordsAggregatedValues = {}, + relations, + aggregate = {}, + limit, + authContext, + dataSource, + }: { + objectMetadataMaps: ObjectMetadataMaps; + parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + parentObjectRecords: T[]; + parentObjectRecordsAggregatedValues?: Record; + relations: Record>; + aggregate?: Record; + limit: number; + authContext: AuthContext; + dataSource: DataSource; + }): Promise { + const processRelationTasks = Object.entries(relations).map( + ([sourceFieldName, nestedRelations]) => + this.processRelation({ + objectMetadataMaps, + parentObjectMetadataItem, + parentObjectRecords, + parentObjectRecordsAggregatedValues, + sourceFieldName, + nestedRelations, + aggregate, + limit, + authContext, + dataSource, + }), + ); + + await Promise.all(processRelationTasks); + } + + private async processRelation({ + objectMetadataMaps, + parentObjectMetadataItem, + parentObjectRecords, + parentObjectRecordsAggregatedValues, + sourceFieldName, + nestedRelations, + aggregate, + limit, + authContext, + dataSource, + }: { + objectMetadataMaps: ObjectMetadataMaps; + parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + parentObjectRecords: T[]; + parentObjectRecordsAggregatedValues: Record; + sourceFieldName: string; + nestedRelations: FindOptionsRelations; + aggregate: Record; + limit: number; + authContext: AuthContext; + dataSource: DataSource; + }): Promise { + const sourceFieldMetadata = + parentObjectMetadataItem.fieldsByName[sourceFieldName]; + + if (!isRelationFieldMetadata(sourceFieldMetadata)) { + // TODO: Maybe we should throw an error here ? + return; + } + + if (!sourceFieldMetadata.settings) { + throw new GraphqlQueryRunnerException( + `Relation settings not found for field ${sourceFieldName}`, + GraphqlQueryRunnerExceptionCode.RELATION_SETTINGS_NOT_FOUND, + ); + } + + const relationType = sourceFieldMetadata.settings?.relationType; + const { targetRelationName, targetObjectMetadata } = + this.getTargetObjectMetadata({ + objectMetadataMaps, + parentObjectMetadataItem, + sourceFieldName, + }); + + const targetObjectRepository = dataSource.getRepository( + targetObjectMetadata.nameSingular, + ); + const targetObjectQueryBuilder = targetObjectRepository.createQueryBuilder( + targetObjectMetadata.nameSingular, + ); + + const relationIds = this.getUniqueIds({ + records: parentObjectRecords, + idField: + relationType === RelationType.ONE_TO_MANY + ? 'id' + : `${sourceFieldName}Id`, + }); + + const { relationResults, relationAggregatedFieldsResult } = + await this.findRelations({ + referenceQueryBuilder: targetObjectQueryBuilder, + column: + relationType === RelationType.ONE_TO_MANY + ? `"${targetRelationName}Id"` + : 'id', + ids: relationIds, + limit, + objectMetadataMaps, + targetObjectMetadata, + aggregate, + sourceFieldName, + }); + + this.assignRelationResults({ + parentRecords: parentObjectRecords, + parentObjectRecordsAggregatedValues, + relationResults, + relationAggregatedFieldsResult, + sourceFieldName, + joinField: + relationType === RelationType.ONE_TO_MANY + ? `${targetRelationName}Id` + : 'id', + 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, + parentObjectRecords: relationResults as ObjectRecord[], + parentObjectRecordsAggregatedValues: relationAggregatedFieldsResult, + relations: nestedRelations as Record< + string, + FindOptionsRelations + >, + aggregate, + limit, + authContext, + dataSource, + }); + } + } + + private getTargetObjectMetadata({ + objectMetadataMaps, + parentObjectMetadataItem, + sourceFieldName, + }: { + objectMetadataMaps: ObjectMetadataMaps; + parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; + sourceFieldName: string; + }) { + const targetFieldMetadata = + parentObjectMetadataItem.fieldsByName[sourceFieldName]; + const targetObjectMetadata = getTargetObjectMetadataOrThrow( + targetFieldMetadata, + objectMetadataMaps, + ); + + if ( + !targetFieldMetadata.relationTargetObjectMetadataId || + !targetFieldMetadata.relationTargetFieldMetadataId + ) { + throw new GraphqlQueryRunnerException( + `Relation target object metadata id or field metadata id not found for field ${sourceFieldName}`, + GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND, + ); + } + + const targetRelationName = + objectMetadataMaps.byId[ + targetFieldMetadata.relationTargetObjectMetadataId + ]?.fieldsById[targetFieldMetadata.relationTargetFieldMetadataId]?.name; + + return { targetRelationName, targetObjectMetadata }; + } + + private getUniqueIds({ + records, + idField, + }: { + records: ObjectRecord[]; + idField: string; + }): any[] { + return [...new Set(records.map((item) => item[idField]))]; + } + + private async findRelations({ + referenceQueryBuilder, + column, + ids, + limit, + objectMetadataMaps, + targetObjectMetadata, + aggregate, + sourceFieldName, + }: { + referenceQueryBuilder: SelectQueryBuilder; + column: string; + ids: any[]; + limit: number; + objectMetadataMaps: ObjectMetadataMaps; + targetObjectMetadata: ObjectMetadataItemWithFieldMaps; + aggregate: Record; + sourceFieldName: string; + }): Promise<{ relationResults: any[]; relationAggregatedFieldsResult: any }> { + if (ids.length === 0) { + return { relationResults: [], relationAggregatedFieldsResult: {} }; + } + + const aggregateForRelation = aggregate[sourceFieldName]; + let relationAggregatedFieldsResult: Record = {}; + + if (aggregateForRelation) { + const aggregateQueryBuilder = referenceQueryBuilder.clone(); + + this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder( + { + selectedAggregatedFields: aggregateForRelation, + queryBuilder: aggregateQueryBuilder, + }, + ); + + const aggregatedFieldsValues = await aggregateQueryBuilder + .addSelect(column) + .where(`${column} IN (:...ids)`, { + ids, + }) + .groupBy(column) + .getRawMany(); + + relationAggregatedFieldsResult = aggregatedFieldsValues.reduce( + (acc, item) => { + const columnWithoutQuotes = column.replace(/["']/g, ''); + const key = item[columnWithoutQuotes]; + const { [column]: _, ...itemWithoutColumn } = item; + + acc[key] = itemWithoutColumn; + + return acc; + }, + {}, + ); + } + + const result = await referenceQueryBuilder + .where(`${column} IN (:...ids)`, { + ids, + }) + .take(limit) + .getMany(); + + const relationResults = formatResult( + result, + targetObjectMetadata, + objectMetadataMaps, + ); + + return { relationResults, relationAggregatedFieldsResult }; + } + + private assignRelationResults({ + parentRecords, + parentObjectRecordsAggregatedValues, + relationResults, + relationAggregatedFieldsResult, + sourceFieldName, + joinField, + relationType, + }: { + parentRecords: ObjectRecord[]; + parentObjectRecordsAggregatedValues: Record; + relationResults: any[]; + relationAggregatedFieldsResult: Record; + sourceFieldName: string; + joinField: string; + relationType: RelationType; + }): void { + parentRecords.forEach((item) => { + if (relationType === RelationType.ONE_TO_MANY) { + item[sourceFieldName] = relationResults.filter( + (rel) => rel[joinField] === item.id, + ); + } else { + if (relationResults.length === 0) { + item[`${sourceFieldName}Id`] = null; + } + item[sourceFieldName] = + relationResults.find( + (rel) => rel.id === item[`${sourceFieldName}Id`], + ) ?? null; + } + }); + + parentObjectRecordsAggregatedValues[sourceFieldName] = + relationAggregatedFieldsResult; + } +} 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 708c79af8..26ee3bc23 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 @@ -1,3 +1,5 @@ +import { Injectable } from '@nestjs/common'; + import { DataSource, FindOptionsRelations, @@ -12,23 +14,28 @@ import { GraphqlQueryRunnerExceptionCode, } 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 { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper'; import { getRelationMetadata, getRelationObjectMetadata, } from 'src/engine/api/graphql/graphql-query-runner/utils/get-relation-object-metadata.util'; import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.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 { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { deduceRelationDirection } from 'src/engine/utils/deduce-relation-direction.util'; +@Injectable() export class ProcessNestedRelationsHelper { - private processAggregateHelper: ProcessAggregateHelper; - - constructor() { - this.processAggregateHelper = new ProcessAggregateHelper(); - } + constructor( + private readonly processNestedRelationsV2Helper: ProcessNestedRelationsV2Helper, + private readonly processAggregateHelper: ProcessAggregateHelper, + private readonly featureFlagService: FeatureFlagService, + ) {} public async processNestedRelations({ objectMetadataMaps, @@ -48,9 +55,28 @@ export class ProcessNestedRelationsHelper { relations: Record>; aggregate?: Record; limit: number; - authContext: any; + authContext: AuthContext; dataSource: DataSource; }): Promise { + const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsNewRelationEnabled, + authContext.workspace.id, + ); + + if (isNewRelationEnabled) { + return this.processNestedRelationsV2Helper.processNestedRelations({ + objectMetadataMaps, + parentObjectMetadataItem, + parentObjectRecords, + parentObjectRecordsAggregatedValues, + relations, + aggregate, + limit, + authContext, + dataSource, + }); + } + const processRelationTasks = Object.entries(relations).map( ([relationName, nestedRelations]) => this.processRelation({ diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts index 1f89164d0..ba3e5d5ae 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts @@ -16,11 +16,13 @@ import { import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; +import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory'; import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -52,6 +54,10 @@ export abstract class GraphqlQueryBaseResolverService< protected readonly apiEventEmitterService: ApiEventEmitterService; @Inject() protected readonly twentyORMGlobalManager: TwentyORMGlobalManager; + @Inject() + protected readonly processNestedRelationsHelper: ProcessNestedRelationsHelper; + @Inject() + protected readonly featureFlagService: FeatureFlagService; public async execute( args: Input, @@ -86,9 +92,15 @@ export abstract class GraphqlQueryBaseResolverService< objectMetadataItemWithFieldMaps.nameSingular, ); + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const graphqlQueryParser = new GraphqlQueryParser( objectMetadataItemWithFieldMaps.fieldsByName, options.objectMetadataMaps, + featureFlagsMap, ); const selectedFields = graphqlFields(options.info); diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts index 070cc21a7..297b014d1 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts @@ -12,7 +12,6 @@ import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolve import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @@ -58,10 +57,8 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol objectMetadataItemWithFieldMaps, ); - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: upsertedRecords, @@ -72,8 +69,16 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return upsertedRecords.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts index e93234028..f7d15a46e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts @@ -12,7 +12,6 @@ import { CreateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @@ -58,10 +57,8 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv objectMetadataItemWithFieldMaps, ); - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: upsertedRecords, @@ -72,8 +69,16 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return typeORMObjectRecordsParser.processRecord({ objectRecord: upsertedRecords[0], diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts index 377d7d6a6..633103575 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts @@ -10,7 +10,6 @@ import { DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolve import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @@ -59,10 +58,8 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol objectMetadataItemWithFieldMaps, ); - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: formattedDeletedRecords, @@ -73,8 +70,16 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return formattedDeletedRecords.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts index 620cf1bfd..581c0143e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts @@ -14,7 +14,6 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @@ -61,10 +60,8 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv const deletedRecord = formattedDeletedRecords[0]; - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: [deletedRecord], @@ -75,8 +72,16 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return typeORMObjectRecordsParser.processRecord({ objectRecord: deletedRecord, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts index abd5acc6d..d618cc30c 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts @@ -10,7 +10,6 @@ import { DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolv import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; @@ -57,10 +56,8 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso objectMetadataItemWithFieldMaps, ); - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: deletedRecords, @@ -71,8 +68,16 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return deletedRecords.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts index c4d51d931..6071e3853 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts @@ -14,7 +14,6 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @Injectable() @@ -57,10 +56,8 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol objectMetadataItemWithFieldMaps, ); - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: deletedRecords, @@ -71,8 +68,16 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return typeORMObjectRecordsParser.processRecord({ objectRecord: deletedRecords[0], diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts index 9543bbdaa..19ca206fa 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts @@ -37,7 +37,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR async resolve( executionArgs: GraphqlQueryResolverExecutionArgs, ): Promise[]> { - const { objectMetadataItemWithFieldMaps, objectMetadataMaps } = + const { objectMetadataItemWithFieldMaps, objectMetadataMaps, authContext } = executionArgs.options; const existingRecordsQueryBuilder = @@ -58,13 +58,22 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR ); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const graphqlQueryParser = new GraphqlQueryParser( objectMetadataItemWithFieldsMaps?.fieldsByName, objectMetadataMaps, + featureFlagsMap, ); const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); let objectRecords: Partial[] = []; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts index f0d51e1de..9f81c733d 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts @@ -21,13 +21,11 @@ import { } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter'; import { getCursor, getPaginationInfo, } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { isDefined } from 'src/utils/is-defined'; @@ -36,7 +34,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve FindManyResolverArgs, IConnection > { - constructor(private readonly featureFlagService: FeatureFlagService) { + constructor(private readonly processAggregateHelper: ProcessAggregateHelper) { super(); } @@ -108,13 +106,13 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve appliedFilters, ); - const processAggregateHelper = new ProcessAggregateHelper(); - - processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder({ - selectedAggregatedFields: - executionArgs.graphqlQuerySelectedFieldsResult.aggregate, - queryBuilder: aggregateQueryBuilder, - }); + this.processAggregateHelper.addSelectedAggregatedFieldsQueriesToQueryBuilder( + { + selectedAggregatedFields: + executionArgs.graphqlQuerySelectedFieldsResult.aggregate, + queryBuilder: aggregateQueryBuilder, + }, + ); const limit = executionArgs.args.first ?? executionArgs.args.last ?? QUERY_MAX_RECORDS; @@ -142,10 +140,8 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve const parentObjectRecordsAggregatedValues = await aggregateQueryBuilder.getRawOne(); - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: objectRecords, @@ -158,8 +154,16 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return typeORMObjectRecordsParser.createConnection({ objectRecords, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts index 5150e1a2f..d68ba1d80 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts @@ -17,7 +17,6 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { WorkspaceQueryRunnerException, WorkspaceQueryRunnerExceptionCode, @@ -65,12 +64,10 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver ); } - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - const objectRecords = [objectRecord]; if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: objectRecords, @@ -81,8 +78,16 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return typeORMObjectRecordsParser.processRecord({ objectRecord: objectRecords[0], diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts index 8ed96cf1c..154a6f985 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts @@ -10,7 +10,6 @@ import { RestoreManyResolverArgs } from 'src/engine/api/graphql/workspace-resolv import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @@ -59,10 +58,8 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso objectMetadataItemWithFieldMaps, ); - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: formattedRestoredRecords, @@ -73,8 +70,16 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return formattedRestoredRecords.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts index 6e7f96eb4..fb6c34625 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts @@ -14,7 +14,6 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; @@ -61,10 +60,8 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol const restoredRecord = formattedRestoredRecords[0]; - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: [restoredRecord], @@ -75,8 +72,16 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return typeORMObjectRecordsParser.processRecord({ objectRecord: restoredRecord, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts index f9d0e5897..16d42ef1a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts @@ -17,7 +17,6 @@ import { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-bu import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { isDefined } from 'src/utils/is-defined'; @@ -33,8 +32,16 @@ export class GraphqlQuerySearchResolverService extends GraphqlQueryBaseResolverS const { authContext, objectMetadataMaps, objectMetadataItemWithFieldMaps } = executionArgs.options; + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); if (!isDefined(executionArgs.args.searchInput)) { return typeORMObjectRecordsParser.createConnection({ @@ -113,10 +120,8 @@ export class GraphqlQuerySearchResolverService extends GraphqlQueryBaseResolverS : 0; const order = undefined; - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: objectRecords, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts index b8dd80bd9..0b0b36805 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts @@ -10,7 +10,6 @@ import { UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolve import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; @@ -83,10 +82,8 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol objectMetadataItemWithFieldMaps, ); - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: formattedUpdatedRecords, @@ -97,8 +94,16 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return formattedUpdatedRecords.map((record: ObjectRecord) => typeORMObjectRecordsParser.processRecord({ diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts index 4feea3166..79255d8e1 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts @@ -14,7 +14,6 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; -import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; @@ -81,10 +80,8 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv const updatedRecord = formattedUpdatedRecords[0]; - const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); - if (executionArgs.graphqlQuerySelectedFieldsResult.relations) { - await processNestedRelationsHelper.processNestedRelations({ + await this.processNestedRelationsHelper.processNestedRelations({ objectMetadataMaps, parentObjectMetadataItem: objectMetadataItemWithFieldMaps, parentObjectRecords: [updatedRecord], @@ -95,8 +92,16 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv }); } + const featureFlagsMap = + await this.featureFlagService.getWorkspaceFeatureFlagsMap( + authContext.workspace.id, + ); + const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMaps); + new ObjectRecordsToGraphqlConnectionHelper( + objectMetadataMaps, + featureFlagsMap, + ); return typeORMObjectRecordsParser.processRecord({ objectRecord: updatedRecord, 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 new file mode 100644 index 000000000..36e23802c --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util.ts @@ -0,0 +1,31 @@ +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; + +export const getTargetObjectMetadataOrThrow = ( + fieldMetadata: FieldMetadataInterface, + objectMetadataMaps: ObjectMetadataMaps, +) => { + if (!fieldMetadata.relationTargetObjectMetadataId) { + throw new GraphqlQueryRunnerException( + `Relation target object metadata id not found for field ${fieldMetadata.name}`, + GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND, + ); + } + + const targetObjectMetadata = + objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + + if (!targetObjectMetadata) { + throw new GraphqlQueryRunnerException( + `Target object metadata not found for field ${fieldMetadata.name}`, + GraphqlQueryRunnerExceptionCode.RELATION_TARGET_OBJECT_METADATA_NOT_FOUND, + ); + } + + return targetObjectMetadata; +}; 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 3422fb480..1be177ba4 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 @@ -16,10 +16,11 @@ 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 { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { isRelationFieldMetadata } from 'src/engine/utils/is-relation-field-metadata.util'; import { isDefined } from 'src/utils/is-defined'; // TODO: find a way to prevent conflict between handlers executing logic on object relations @@ -135,6 +136,11 @@ export class QueryResultGettersFactory { ): Promise { const objectMetadataMapItem = objectMetadataMaps.byId[objectMetadataItemId]; + const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsNewRelationEnabled, + workspaceId, + ); + const handler = this.getHandler(objectMetadataMapItem.nameSingular); const relationFields = Object.keys(record) @@ -143,9 +149,7 @@ export class QueryResultGettersFactory { objectMetadataMapItem.fieldsByName[recordFieldName], ) .filter(isDefined) - .filter((fieldMetadata) => - isRelationFieldMetadataType(fieldMetadata.type), - ); + .filter((fieldMetadata) => isRelationFieldMetadata(fieldMetadata)); const relationFieldsProcessedMap = {} as Record< string, @@ -153,38 +157,53 @@ export class QueryResultGettersFactory { >; for (const relationField of relationFields) { - const relationMetadata = - relationField.fromRelationMetadata ?? relationField.toRelationMetadata; + if (!isNewRelationEnabled) { + const relationMetadata = + relationField.fromRelationMetadata ?? + relationField.toRelationMetadata; - if (!isDefined(relationMetadata)) { - throw new Error('Relation metadata is not defined'); + 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 = + objectMetadataMaps.byId[relationObjectMetadataItemId]; + + if (!isDefined(relationObjectMetadataItem)) { + throw new Error( + `Object metadata not found for id ${relationObjectMetadataItemId}`, + ); + } + + relationFieldsProcessedMap[relationField.name] = + await this.processQueryResultField( + record[relationField.name], + relationObjectMetadataItem.id, + objectMetadataMaps, + workspaceId, + ); + } else { + if (!isDefined(relationField.relationTargetObjectMetadataId)) { + throw new Error('Relation target object metadata id is not defined'); + } + + relationFieldsProcessedMap[relationField.name] = + await this.processQueryResultField( + record[relationField.name], + relationField.relationTargetObjectMetadataId, + objectMetadataMaps, + workspaceId, + ); } - - // 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 = - objectMetadataMaps.byId[relationObjectMetadataItemId]; - - if (!isDefined(relationObjectMetadataItem)) { - throw new Error( - `Object metadata not found for id ${relationObjectMetadataItemId}`, - ); - } - - relationFieldsProcessedMap[relationField.name] = - await this.processQueryResultField( - record[relationField.name], - relationObjectMetadataItem.id, - objectMetadataMaps, - workspaceId, - ); } const objectRecordProcessedWithoutRelationFields = await handler.handle( diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts index 007ead779..b5499c51b 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts @@ -12,6 +12,7 @@ import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interface import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.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 { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; @@ -33,6 +34,7 @@ export class FindRecordsWorflowAction implements WorkflowAction { private readonly twentyORMManager: TwentyORMManager, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, + private readonly featureFlagService: FeatureFlagService, ) {} async execute( @@ -86,9 +88,13 @@ export class FindRecordsWorflowAction implements WorkflowAction { ); } + const featureFlagMaps = + await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId); + const graphqlQueryParser = new GraphqlQueryParser( objectMetadataItemWithFieldsMaps.fieldsByName, objectMetadataMaps, + featureFlagMaps, ); const objectRecords = await this.getObjectRecords( diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module.ts index 6702859d0..5a1a86418 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @@ -14,6 +15,7 @@ import { UpdateRecordWorkflowAction } from 'src/modules/workflow/workflow-execut imports: [ WorkspaceCacheStorageModule, NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), + FeatureFlagModule, ], providers: [ ScopedWorkspaceContextFactory,