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 e2b794b2dc4..fa0e1cd1120 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 @@ -37,6 +37,7 @@ export class GraphqlQuerySelectedFieldsRelationParser { const relationAccumulator = fieldParser.parse( fieldValue, targetObjectMetadata, + this.objectMetadataMaps, ); accumulator.select[fieldKey] = { diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts index 5b0105a8af0..ef0a54ee332 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,14 +1,16 @@ -import { capitalize } from 'twenty-shared/utils'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { capitalize, isDefined } from 'twenty-shared/utils'; import { GraphqlQuerySelectedFieldsAggregateParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-aggregate.parser'; import { GraphqlQuerySelectedFieldsRelationParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields-relation.parser'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { type 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 { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export type GraphqlQuerySelectedFieldsResult = { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -33,6 +35,7 @@ export class GraphqlQuerySelectedFieldsParser { // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, + objectMetadataMaps: ObjectMetadataMaps, ): GraphqlQuerySelectedFieldsResult { const accumulator: GraphqlQuerySelectedFieldsResult = { select: {}, @@ -45,6 +48,7 @@ export class GraphqlQuerySelectedFieldsParser { graphqlSelectedFields, objectMetadataMapItem, accumulator, + objectMetadataMaps, ); return accumulator; @@ -56,66 +60,108 @@ export class GraphqlQuerySelectedFieldsParser { accumulator, ); - this.parseRecordField( + this.parseRecordFields( graphqlSelectedFields, objectMetadataMapItem, accumulator, + objectMetadataMaps, ); return accumulator; } - private parseRecordField( + private parseRecordFields( // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, accumulator: GraphqlQuerySelectedFieldsResult, + objectMetadataMaps: ObjectMetadataMaps, ): void { - for (const [fieldKey, fieldValue] of Object.entries( - graphqlSelectedFields, + for (const fieldMetadata of Object.values( + objectMetadataMapItem.fieldsById, )) { - const fieldMetadataBasedOnName = - objectMetadataMapItem.fieldsById[ - objectMetadataMapItem.fieldIdByName[fieldKey] - ]; + if ( + isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION) + ) { + const graphqlSelectedFieldValue = + graphqlSelectedFields[fieldMetadata.name]; - const isFieldForeignKey = - !fieldMetadataBasedOnName && fieldKey.endsWith('Id'); - - if (isFieldForeignKey) { - const fieldMetadataBasedOnRelationName = - objectMetadataMapItem.fieldsById[ - objectMetadataMapItem.fieldIdByName[fieldKey.slice(0, -2)] - ]; - - if (fieldMetadataBasedOnRelationName) { - accumulator.select[fieldKey] = true; // field is not a connection so should not be treated as a relation + if (!isDefined(graphqlSelectedFieldValue)) { continue; } - } - const fieldMetadata = fieldMetadataBasedOnName; + this.graphqlQuerySelectedFieldsRelationParser.parseRelationField( + fieldMetadata, + fieldMetadata.name, + graphqlSelectedFieldValue, + accumulator, + ); - if (!fieldMetadata) { continue; } - if (isRelationFieldMetadataType(fieldMetadata.type)) { + if ( + isFieldMetadataEntityOfType( + fieldMetadata, + FieldMetadataType.MORPH_RELATION, + ) + ) { + const targetObjectMetadata = + objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + + if ( + !fieldMetadata.settings?.relationType || + !isDefined(targetObjectMetadata) + ) { + continue; + } + + const morphRelationFieldName = computeMorphRelationFieldName({ + fieldName: fieldMetadata.name, + relationDirection: fieldMetadata.settings.relationType, + targetObjectMetadata, + }); + + const graphqlSelectedFieldValue = + graphqlSelectedFields[morphRelationFieldName]; + + if (!isDefined(graphqlSelectedFieldValue)) { + continue; + } + this.graphqlQuerySelectedFieldsRelationParser.parseRelationField( fieldMetadata, - fieldKey, - fieldValue, + morphRelationFieldName, + graphqlSelectedFieldValue, accumulator, ); - } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { + + continue; + } + + if (isCompositeFieldMetadataType(fieldMetadata.type)) { + const graphqlSelectedFieldValue = + graphqlSelectedFields[fieldMetadata.name]; + + if (!isDefined(graphqlSelectedFieldValue)) { + continue; + } + const compositeResult = this.parseCompositeField( fieldMetadata, - fieldValue, + graphqlSelectedFieldValue, ); Object.assign(accumulator.select, compositeResult); - } else { - accumulator.select[fieldKey] = true; + + continue; + } + + const graphqlSelectedFieldValue = + graphqlSelectedFields[fieldMetadata.name]; + + if (isDefined(graphqlSelectedFieldValue)) { + accumulator.select[fieldMetadata.name] = true; } } } @@ -125,6 +171,7 @@ export class GraphqlQuerySelectedFieldsParser { graphqlSelectedFields: Partial>, objectMetadataMapItem: ObjectMetadataItemWithFieldMaps, accumulator: GraphqlQuerySelectedFieldsResult, + objectMetadataMaps: ObjectMetadataMaps, ): void { this.aggregateParser.parse( graphqlSelectedFields, @@ -134,7 +181,12 @@ export class GraphqlQuerySelectedFieldsParser { const node = graphqlSelectedFields.edges.node; - this.parseRecordField(node, objectMetadataMapItem, accumulator); + this.parseRecordFields( + node, + objectMetadataMapItem, + accumulator, + objectMetadataMaps, + ); } private isRootConnection( 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 e09eb485eba..35fb2954355 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 @@ -119,6 +119,7 @@ export class GraphqlQueryParser { parentObjectMetadata: ObjectMetadataItemWithFieldMaps, // eslint-disable-next-line @typescript-eslint/no-explicit-any graphqlSelectedFields: Partial>, + objectMetadataMaps: ObjectMetadataMaps, ): GraphqlQuerySelectedFieldsResult { const objectMetadataMapItem = getObjectMetadataMapItemByNameSingular( this.objectMetadataMaps, @@ -139,6 +140,7 @@ export class GraphqlQueryParser { return selectedFieldsParser.parse( graphqlSelectedFields, objectMetadataMapItem, + objectMetadataMaps, ); } } 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 337b46fdc6c..b04018128cb 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 @@ -17,11 +17,13 @@ import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-q import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; +import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { type 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 ObjectRecordsToGraphqlConnectionHelper { @@ -167,24 +169,42 @@ export class ObjectRecordsToGraphqlConnectionHelper { // eslint-disable-next-line @typescript-eslint/no-explicit-any const processedObjectRecord: Record = {}; - for (const [key, value] of Object.entries(objectRecord)) { - const fieldMetadataId = objectMetadata.fieldIdByName[key]; - const fieldMetadata = objectMetadata.fieldsById[fieldMetadataId]; + for (const fieldMetadata of Object.values(objectMetadata.fieldsById)) { + if (isCompositeFieldMetadataType(fieldMetadata.type)) { + const objectValue = objectRecord[fieldMetadata.name]; - if (!fieldMetadata) { - processedObjectRecord[key] = value; + if (!isDefined(objectValue)) { + continue; + } + processedObjectRecord[fieldMetadata.name] = this.processCompositeField( + fieldMetadata, + objectValue, + ); continue; } - if (isRelationFieldMetadataType(fieldMetadata.type)) { - if (Array.isArray(value)) { + if (isFieldMetadataTypeRelation(fieldMetadata)) { + const fieldMetadataNameWithId = `${fieldMetadata.name}Id`; + + if (isDefined(objectRecord[fieldMetadataNameWithId])) { + processedObjectRecord[fieldMetadataNameWithId] = + objectRecord[fieldMetadataNameWithId]; + } + + const objectValue = objectRecord[fieldMetadata.name]; + + if (!isDefined(objectValue)) { + continue; + } + + if (Array.isArray(objectValue)) { const targetObjectMetadata = getTargetObjectMetadataOrThrow( fieldMetadata, this.objectMetadataMaps, ); - processedObjectRecord[key] = this.createConnection({ - objectRecords: value, + processedObjectRecord[fieldMetadata.name] = this.createConnection({ + objectRecords: objectValue, parentObjectRecord: objectRecord, objectRecordsAggregatedValues: objectRecordsAggregatedValues[fieldMetadata.name], @@ -194,20 +214,20 @@ export class ObjectRecordsToGraphqlConnectionHelper { take, totalCount: objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ?? - value.length, + objectValue.length, order, hasNextPage: false, hasPreviousPage: false, depth: depth + 1, }); - } else if (isPlainObject(value)) { + } else if (isPlainObject(objectValue)) { const targetObjectMetadata = getTargetObjectMetadataOrThrow( fieldMetadata, this.objectMetadataMaps, ); - processedObjectRecord[key] = this.processRecord({ - objectRecord: value, + processedObjectRecord[fieldMetadata.name] = this.processRecord({ + objectRecord: objectValue, objectRecordsAggregatedValues: objectRecordsAggregatedValues[fieldMetadata.name], selectedAggregatedFields: @@ -219,17 +239,93 @@ export class ObjectRecordsToGraphqlConnectionHelper { depth: depth + 1, }); } - } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { - processedObjectRecord[key] = this.processCompositeField( - fieldMetadata, - value, - ); - } else { - processedObjectRecord[key] = this.formatFieldValue( - value, - fieldMetadata.type, - ); + continue; } + + if (isFieldMetadataTypeMorphRelation(fieldMetadata)) { + const targetObjectMetadata = + this.objectMetadataMaps.byId[ + fieldMetadata.relationTargetObjectMetadataId + ]; + + if ( + !fieldMetadata.settings?.relationType || + !isDefined(targetObjectMetadata) + ) { + continue; + } + + const morphRelationFieldName = computeMorphRelationFieldName({ + fieldName: fieldMetadata.name, + relationDirection: fieldMetadata.settings.relationType, + targetObjectMetadata, + }); + + const fieldMetadataNameWithId = `${fieldMetadata.name}Id`; + + if (isDefined(objectRecord[fieldMetadataNameWithId])) { + processedObjectRecord[fieldMetadataNameWithId] = + objectRecord[fieldMetadataNameWithId]; + } + + const objectValue = objectRecord[morphRelationFieldName]; + + if (!isDefined(objectValue)) { + continue; + } + + if (Array.isArray(objectValue)) { + processedObjectRecord[morphRelationFieldName] = this.createConnection( + { + objectRecords: objectValue, + parentObjectRecord: objectRecord, + objectRecordsAggregatedValues: + objectRecordsAggregatedValues[fieldMetadata.name], + selectedAggregatedFields: + selectedAggregatedFields[fieldMetadata.name], + objectName: targetObjectMetadata.nameSingular, + take, + totalCount: + objectRecordsAggregatedValues[fieldMetadata.name]?.totalCount ?? + objectValue.length, + order, + hasNextPage: false, + hasPreviousPage: false, + depth: depth + 1, + }, + ); + } else if (isPlainObject(objectValue)) { + const targetObjectMetadata = getTargetObjectMetadataOrThrow( + fieldMetadata, + this.objectMetadataMaps, + ); + + processedObjectRecord[morphRelationFieldName] = this.processRecord({ + objectRecord: objectValue, + objectRecordsAggregatedValues: + objectRecordsAggregatedValues[fieldMetadata.name], + selectedAggregatedFields: + selectedAggregatedFields[fieldMetadata.name], + objectName: targetObjectMetadata.nameSingular, + take, + totalCount, + order, + depth: depth + 1, + }); + } + continue; + } + + const objectValue = objectRecord[fieldMetadata.name]; + + if (!isDefined(objectValue)) { + continue; + } + + processedObjectRecord[fieldMetadata.name] = this.formatFieldValue( + objectValue, + fieldMetadata.type, + ); } return processedObjectRecord as T; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts index 10b6060910f..38a80a06270 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts @@ -12,6 +12,7 @@ import { } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper'; import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; +import { getFieldMetadataFromGraphQLField } from 'src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util'; import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; import { type AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util'; import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; @@ -110,15 +111,20 @@ export class ProcessNestedRelationsV2Helper { roleId?: string; selectedFields: Record; }): Promise { - const sourceFieldMetadataId = - parentObjectMetadataItem.fieldIdByName[sourceFieldName]; - const sourceFieldMetadata = - parentObjectMetadataItem.fieldsById[sourceFieldMetadataId]; + const sourceFieldMetadata = getFieldMetadataFromGraphQLField({ + objectMetadataItem: parentObjectMetadataItem, + graphQLField: sourceFieldName, + objectMetadataMaps, + }); if ( !isFieldMetadataEntityOfType( sourceFieldMetadata, FieldMetadataType.RELATION, + ) && + !isFieldMetadataEntityOfType( + sourceFieldMetadata, + FieldMetadataType.MORPH_RELATION, ) ) { // TODO: Maybe we should throw an error here ? @@ -154,6 +160,7 @@ export class ProcessNestedRelationsV2Helper { select: selectedFields, relations: nestedRelations, objectMetadataItemWithFieldMaps: targetObjectMetadata, + objectMetadataMaps, }); targetObjectQueryBuilder = targetObjectQueryBuilder.setFindOptions({ @@ -165,7 +172,8 @@ export class ProcessNestedRelationsV2Helper { idField: relationType === RelationType.ONE_TO_MANY ? 'id' - : `${sourceFieldName}Id`, + : (sourceFieldMetadata.settings.joinColumnName ?? + `${sourceFieldName}Id`), }); const fieldMetadataTargetRelationColumnName = @@ -246,10 +254,11 @@ export class ProcessNestedRelationsV2Helper { parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; sourceFieldName: string; }) { - const targetFieldMetadataId = - parentObjectMetadataItem.fieldIdByName[sourceFieldName]; - const targetFieldMetadata = - parentObjectMetadataItem.fieldsById[targetFieldMetadataId]; + const targetFieldMetadata = getFieldMetadataFromGraphQLField({ + objectMetadataItem: parentObjectMetadataItem, + graphQLField: sourceFieldName, + objectMetadataMaps, + }); const targetObjectMetadata = getTargetObjectMetadataOrThrow( targetFieldMetadata, 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 ee2cc6d0acc..1b1e6259669 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 @@ -174,6 +174,7 @@ export abstract class GraphqlQueryBaseResolverService< graphqlQueryParser.parseSelectedFields( objectMetadataItemWithFieldMaps, selectedFields, + options.objectMetadataMaps, ); const graphqlQueryResolverExecutionArgs = { 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 9aec811a584..30350f86eba 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 @@ -3,8 +3,8 @@ import { Injectable } from '@nestjs/common'; import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; import { capitalize, isDefined } from 'twenty-shared/utils'; import { - type FindOperator, In, + type FindOperator, type InsertResult, type ObjectLiteral, } from 'typeorm'; @@ -52,6 +52,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol executionArgs, objectRecords, objectMetadataItemWithFieldMaps, + objectMetadataMaps, ); await this.processNestedRelationsIfNeeded({ @@ -73,12 +74,14 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol executionArgs: GraphqlQueryResolverExecutionArgs, ): Promise { if (!executionArgs.args.upsert) { - const { objectMetadataItemWithFieldMaps } = executionArgs.options; + const { objectMetadataItemWithFieldMaps, objectMetadataMaps } = + executionArgs.options; const selectedColumns = buildColumnsToReturn({ select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); return await executionArgs.repository.insert( @@ -94,7 +97,8 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol private async performUpsertOperation( executionArgs: GraphqlQueryResolverExecutionArgs, ): Promise { - const { objectMetadataItemWithFieldMaps } = executionArgs.options; + const { objectMetadataItemWithFieldMaps, objectMetadataMaps } = + executionArgs.options; const conflictingFields = this.getConflictingFields( objectMetadataItemWithFieldMaps, @@ -120,6 +124,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); if (recordsToUpdate.length > 0) { @@ -375,6 +380,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol executionArgs: GraphqlQueryResolverExecutionArgs, objectRecords: InsertResult, objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, + objectMetadataMaps: ObjectMetadataMaps, ): Promise { const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, @@ -384,6 +390,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const upsertedRecords = await queryBuilder 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 6f98f76ba94..debd53b1a0d 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 @@ -34,6 +34,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const objectRecords: InsertResult = !executionArgs.args.upsert @@ -60,6 +61,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const upsertedRecords = (await queryBuilder 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 a40f99923c9..0fb36df3999 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 @@ -48,6 +48,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const deletedObjectRecords = await queryBuilder 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 bfdcf773b12..062ac22055d 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 @@ -40,6 +40,7 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const deletedObjectRecords = await queryBuilder 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 165d459f565..097ba73dd17 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 @@ -46,6 +46,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const deletedObjectRecords = await queryBuilder 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 1c5af2adf52..33c0fa581ec 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 @@ -38,6 +38,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const deletedObjectRecords = await queryBuilder 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 cbc6c4b7a33..a3f9402c47a 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 @@ -68,6 +68,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); if (executionArgs.args.ids) { 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 aebf973f2e3..9bc00da1de0 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 @@ -122,6 +122,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const objectRecords = (await queryBuilder 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 df6046b97f6..a29dca36180 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 @@ -56,6 +56,7 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const objectRecord = await queryBuilder diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts index bc66091c371..5a266a0b8c5 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service.ts @@ -247,7 +247,8 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv priorityRecordId: string, mergedData: Partial, ): Promise { - const { objectMetadataItemWithFieldMaps } = executionArgs.options; + const { objectMetadataItemWithFieldMaps, objectMetadataMaps } = + executionArgs.options; const queryBuilder = executionArgs.repository.createQueryBuilder( objectMetadataItemWithFieldMaps.nameSingular, @@ -257,6 +258,7 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const updatedObjectRecords = await queryBuilder 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 6b1a590729f..f9ad6fd8f6d 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 @@ -48,6 +48,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const restoredObjectRecords = await queryBuilder 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 9156a876f6c..f0a6c24e5cb 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 @@ -40,6 +40,7 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const restoredObjectRecords = await queryBuilder 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 27e8f74dec9..c66906b52b5 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 @@ -48,6 +48,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const updatedObjectRecords = await queryBuilder 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 93dc5d04524..cd3c1a63695 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 @@ -40,6 +40,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv select: executionArgs.graphqlQuerySelectedFieldsResult.select, relations: executionArgs.graphqlQuerySelectedFieldsResult.relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }); const updatedObjectRecords = await queryBuilder diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts index 84578ea05b4..0cfa2649094 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/__tests__/build-columns-to-select.spec.ts @@ -101,6 +101,16 @@ describe('buildColumnsToSelect', () => { }, }; + const mockObjectMetadataMaps = { + byId: { + '9af20778-2f2c-4e22-ae83-2e77e479b57c': { + id: '9af20778-2f2c-4e22-ae83-2e77e479b57c', + nameSingular: 'company', + namePlural: 'companies', + }, + }, + }; + it('should build columns to select with relation fields', () => { const select = { nameFirstName: true, @@ -118,6 +128,7 @@ describe('buildColumnsToSelect', () => { select, relations, objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, + objectMetadataMaps: mockObjectMetadataMaps, }); expect(result).toEqual({ @@ -139,6 +150,7 @@ describe('buildColumnsToSelect', () => { select, relations, objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, + objectMetadataMaps: mockObjectMetadataMaps, }); expect(result).toEqual({ @@ -161,6 +173,7 @@ describe('buildColumnsToSelect', () => { select, relations, objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, + objectMetadataMaps: mockObjectMetadataMaps, }); expect(result).toEqual({ @@ -182,6 +195,7 @@ describe('buildColumnsToSelect', () => { select, relations, objectMetadataItemWithFieldMaps: mockObjectMetadataItemWithFieldMaps, + objectMetadataMaps: mockObjectMetadataMaps, }); expect(result).toEqual({ @@ -219,6 +233,7 @@ describe('buildColumnsToSelect', () => { select, relations, objectMetadataItemWithFieldMaps: mockObjectMetadataWithOneToMany, + objectMetadataMaps: mockObjectMetadataMaps, }); expect(result).toEqual({ @@ -256,6 +271,7 @@ describe('buildColumnsToSelect', () => { select, relations, objectMetadataItemWithFieldMaps: mockObjectMetadataWithoutJoinColumn, + objectMetadataMaps: mockObjectMetadataMaps, }); expect(result).toEqual({ diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts index 7e00ac03c3f..c4a7ab3a4d8 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return.ts @@ -1,20 +1,24 @@ import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; export const buildColumnsToReturn = ({ select, relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }: { select: Record; relations: Record; objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + objectMetadataMaps: ObjectMetadataMaps; }): string[] => { return Object.entries( buildColumnsToSelect({ select, relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }), ) .filter(([_columnName, value]) => value === true) diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts index afefb751ac4..0669f3c2530 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts @@ -1,23 +1,34 @@ -import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; +import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; +import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export const buildColumnsToSelect = ({ select, relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, }: { select: Record; relations: Record; objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; + objectMetadataMaps: { + byId: Partial< + Record< + string, + Pick + > + >; + }; }) => { const requiredRelationColumns = getRequiredRelationColumns( relations, objectMetadataItemWithFieldMaps, + objectMetadataMaps, ); const fieldsToSelect: Record = Object.entries(select) @@ -35,37 +46,59 @@ export const buildColumnsToSelect = ({ const getRequiredRelationColumns = ( relations: Record, - objectMetadataItem: ObjectMetadataItemWithFieldMaps, + objectMetadataItem: Pick, + objectMetadataMaps: { + byId: Partial< + Record< + string, + Pick + > + >; + }, ): string[] => { const requiredColumns: string[] = []; - for (const [relationFieldName, _] of Object.entries(relations)) { - const fieldMetadataId = objectMetadataItem.fieldIdByName[relationFieldName]; + for (const fieldMetadata of Object.values(objectMetadataItem.fieldsById)) { + if (isFieldMetadataTypeRelation(fieldMetadata)) { + const relationValue = relations[fieldMetadata.name]; - if (!fieldMetadataId) { - throw new InternalServerError( - `Field metadata not found for relation field name: ${relationFieldName}`, - ); + if ( + !isDefined(relationValue) || + !isDefined(fieldMetadata?.settings?.joinColumnName) || + fieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE + ) { + continue; + } + + requiredColumns.push(fieldMetadata.settings.joinColumnName); } - const fieldMetadata = objectMetadataItem.fieldsById[fieldMetadataId]; + if (isFieldMetadataTypeMorphRelation(fieldMetadata)) { + const targetObjectMetadata = + objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; - if (!fieldMetadata) { - throw new InternalServerError( - `Field metadata not found for relation field name: ${relationFieldName}`, - ); - } + if ( + !fieldMetadata.settings?.relationType || + !isDefined(targetObjectMetadata) + ) { + continue; + } - if ( - !isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION) - ) { - continue; - } + const morphRelationFieldName = computeMorphRelationFieldName({ + fieldName: fieldMetadata.name, + relationDirection: fieldMetadata.settings.relationType, + targetObjectMetadata, + }); + + const relationValue = relations[morphRelationFieldName]; + + if ( + !isDefined(relationValue) || + !isDefined(fieldMetadata?.settings?.joinColumnName) + ) { + continue; + } - if ( - fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE && - fieldMetadata.settings?.joinColumnName - ) { requiredColumns.push(fieldMetadata.settings.joinColumnName); } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts new file mode 100644 index 00000000000..b7827ccff47 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/get-field-metadata-from-graphql-field.util.ts @@ -0,0 +1,85 @@ +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util'; +import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; +import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; +import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; + +type GetFieldMetadataFromGraphQLFieldArgs = { + objectMetadataItem: ObjectMetadataItemWithFieldMaps; + graphQLField: string; + objectMetadataMaps: ObjectMetadataMaps; +}; + +export function getFieldMetadataFromGraphQLField({ + objectMetadataItem, + graphQLField, + objectMetadataMaps, +}: GetFieldMetadataFromGraphQLFieldArgs) { + const sourceFieldMetadataId = objectMetadataItem.fieldIdByName[graphQLField]; + let sourceFieldMetadata = + objectMetadataItem.fieldsById[sourceFieldMetadataId]; + + // If empty, it could be a morph relation + if (!isDefined(sourceFieldMetadata)) { + const morphRelationsWithTargetObjectMetadata = Object.values( + objectMetadataItem.fieldsById, + ) + .filter( + (fieldMetadata) => + fieldMetadata.type === FieldMetadataType.MORPH_RELATION, + ) + .map((fieldMetadata) => { + const targetObjectMetadata = getTargetObjectMetadataOrThrow( + fieldMetadata, + objectMetadataMaps, + ); + + return { + fieldMetadata, + targetObjectMetadata, + }; + }); + + const possibleGraphQLFieldNames: { + graphQLField: string; + fieldMetadata: FieldMetadataEntity; + targetObjectMetadata: ObjectMetadataItemWithFieldMaps; + }[] = []; + + morphRelationsWithTargetObjectMetadata.map((morphRelation) => { + if ( + !isFieldMetadataTypeMorphRelation(morphRelation.fieldMetadata) || + !morphRelation.fieldMetadata.settings?.relationType + ) { + return; + } + + const name = computeMorphRelationFieldName({ + fieldName: morphRelation.fieldMetadata.name, + relationDirection: morphRelation.fieldMetadata.settings.relationType, + targetObjectMetadata: morphRelation.targetObjectMetadata, + }); + + possibleGraphQLFieldNames.push({ + graphQLField: name, + fieldMetadata: morphRelation.fieldMetadata, + targetObjectMetadata: morphRelation.targetObjectMetadata, + }); + }); + + const fieldMetdata = possibleGraphQLFieldNames.find( + (possibleGraphQLFieldName) => + possibleGraphQLFieldName.graphQLField === graphQLField, + )?.fieldMetadata; + + if (fieldMetdata) { + sourceFieldMetadata = fieldMetdata; + } + } + + return sourceFieldMetadata; +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts index 34e0fd58faf..dc6e2aa339d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory.ts @@ -1,9 +1,9 @@ import { Injectable, Logger } from '@nestjs/common'; import { + GraphQLObjectType, type GraphQLFieldConfigArgumentMap, type GraphQLFieldConfigMap, - GraphQLObjectType, } from 'graphql'; import { FieldMetadataType } from 'twenty-shared/types'; @@ -14,6 +14,7 @@ import { RelationTypeV2Factory } from 'src/engine/api/graphql/workspace-schema-b import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage'; import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util'; import { objectContainsRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-relation-field'; +import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; @@ -46,6 +47,7 @@ export class ExtendObjectTypeDefinitionV2Factory { public create( objectMetadata: ObjectMetadataEntity, options: WorkspaceBuildSchemaOptions, + objectMetadataCollection: ObjectMetadataEntity[], ): ObjectTypeDefinition { const kind = ObjectTypeDefinitionKind.Plain; const gqlType = this.typeDefinitionsStorage.getObjectTypeByKey( @@ -94,7 +96,11 @@ export class ExtendObjectTypeDefinitionV2Factory { ...config, fields: () => ({ ...config.fields, - ...this.generateFields(objectMetadata, options), + ...this.generateFields( + objectMetadata, + options, + objectMetadataCollection, + ), }), }), }; @@ -103,6 +109,7 @@ export class ExtendObjectTypeDefinitionV2Factory { private generateFields( objectMetadata: ObjectMetadataEntity, options: WorkspaceBuildSchemaOptions, + objectMetadataCollection: ObjectMetadataEntity[], // eslint-disable-next-line @typescript-eslint/no-explicit-any ): GraphQLFieldConfigMap { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -151,7 +158,27 @@ export class ExtendObjectTypeDefinitionV2Factory { ); } - fields[fieldMetadata.name] = { + const objectMetadataTarget = objectMetadataCollection.find( + (objectMetadata) => + objectMetadata.id === fieldMetadata.relationTargetObjectMetadataId, + ); + + if (!objectMetadataTarget) { + throw new Error( + `Object Metadata Target not found for Id: ${fieldMetadata.relationTargetObjectMetadataId} on fieldMetadata name: ${fieldMetadata.name}`, + ); + } + + const fieldName = + fieldMetadata.type === FieldMetadataType.MORPH_RELATION + ? computeMorphRelationFieldName({ + fieldName: fieldMetadata.name, + relationDirection: fieldMetadata.settings.relationType, + targetObjectMetadata: objectMetadataTarget, + }) + : fieldMetadata.name; + + fields[fieldName] = { type: relationType, args: argsType, description: fieldMetadata.description, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts index 960c1c26546..4436803e465 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/type-definitions.generator.ts @@ -8,6 +8,7 @@ import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/wor import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory'; import { ExtendObjectTypeDefinitionV2Factory } from 'src/engine/api/graphql/workspace-schema-builder/factories/extend-object-type-definition-v2.factory'; import { RelationConnectInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory'; +import { objectContainsMorphRelationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @@ -269,7 +270,11 @@ export class TypeDefinitionsGenerator { ) { // Generate extended object type defs only for objects that contain composite fields const objectMetadataCollectionWithCompositeFields = - objectMetadataCollection.filter(objectContainsRelationField); + objectMetadataCollection.filter( + (obj) => + objectContainsRelationField(obj) || + objectContainsMorphRelationField(obj), + ); const workspaceId = objectMetadataCollectionWithCompositeFields[0]?.workspaceId; @@ -282,6 +287,7 @@ export class TypeDefinitionsGenerator { this.extendObjectTypeDefinitionV2Factory.create( objectMetadata, options, + objectMetadataCollection, ), ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.util.ts index 9567b9ab7e0..1e0675264b9 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.util.ts @@ -167,10 +167,10 @@ const generateRelationField = < options: WorkspaceBuildSchemaOptions; typeFactory: TypeFactory; }) => { - const relationField = {}; + const relationFields = {}; if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) { - return relationField; + return relationFields; } const { joinColumnName, fieldMetadataName } = @@ -188,14 +188,14 @@ const generateRelationField = < ); // @ts-expect-error legacy noImplicitAny - relationField[joinColumnName] = { + relationFields[joinColumnName] = { type, description: fieldMetadata.description, }; //TODO : temporary - continue ej/1278 branch (https://github.com/twentyhq/core-team-issues/issues/1278 issue) before removing this if (fieldMetadata.type === FieldMetadataType.MORPH_RELATION) - return relationField; + return relationFields; if ( [InputTypeDefinitionKind.Create, InputTypeDefinitionKind.Update].includes( @@ -215,15 +215,16 @@ const generateRelationField = < isRelationConnectField: true, }, ); + + // todo @guillim + // @ts-expect-error legacy noImplicitAny + relationFields[fieldMetadataName] = { + type: type, + description: fieldMetadata.description, + }; } - // @ts-expect-error legacy noImplicitAny - relationField[fieldMetadataName] = { - type: type, - description: fieldMetadata.description, - }; - - return relationField; + return relationFields; }; // Type guard diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util.ts new file mode 100644 index 00000000000..58a4ea5a136 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/object-contains-morph-relation-field.util.ts @@ -0,0 +1,10 @@ +import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { isMorphRelationFieldMetadataType } from 'src/engine/utils/is-morph-relation-field-metadata-type.util'; + +export const objectContainsMorphRelationField = ( + objectMetadata: ObjectMetadataEntity, +): boolean => { + return objectMetadata.fields.some((field) => + isMorphRelationFieldMetadataType(field.type), + ); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util.ts new file mode 100644 index 00000000000..b0987a9cb64 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util.ts @@ -0,0 +1,32 @@ +import { capitalize } from 'twenty-shared/utils'; + +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + +import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; + +type ComputeMorphRelationFieldNameArgs = { + fieldName: string; + relationDirection: RelationType; + targetObjectMetadata: Pick< + ObjectMetadataEntity, + 'nameSingular' | 'namePlural' + >; +}; + +export const computeMorphRelationFieldName = ({ + fieldName, + relationDirection, + targetObjectMetadata, +}: ComputeMorphRelationFieldNameArgs): string => { + if (relationDirection === RelationType.MANY_TO_ONE) { + return `${fieldName}${capitalize(targetObjectMetadata.nameSingular)}`; + } + + if (relationDirection === RelationType.ONE_TO_MANY) { + return `${fieldName}${capitalize(targetObjectMetadata.namePlural)}`; + } + + throw new Error( + `Invalid relation direction: ${relationDirection} for field ${fieldName}`, + ); +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts index aa383e9d295..bae07ae768f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/generate-object-metadata-maps.util.ts @@ -20,7 +20,14 @@ export const generateObjectMetadataMaps = ( for (const fieldMetadata of objectMetadata.fields) { if ( - isFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.RELATION) + isFieldMetadataEntityOfType( + fieldMetadata, + FieldMetadataType.RELATION, + ) || + isFieldMetadataEntityOfType( + fieldMetadata, + FieldMetadataType.MORPH_RELATION, + ) ) { if (fieldMetadata.settings?.joinColumnName) { fieldIdByJoinColumnNameMap[fieldMetadata.settings.joinColumnName] = diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts index 9b36a654adb..69b7b7d8e6d 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-relation.factory.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { FieldMetadataType } from 'twenty-shared/types'; import { type EntitySchemaRelationOptions } from 'typeorm'; +import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { determineSchemaRelationDetails } from 'src/engine/twenty-orm/utils/determine-schema-relation-details.util'; @@ -52,7 +53,25 @@ export class EntitySchemaRelationFactory { objectMetadataMaps, ); - entitySchemaRelationMap[fieldMetadata.name] = { + const targetObjectMetadata = + objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId]; + + if (!targetObjectMetadata) { + throw new Error( + `Target object metadata not found for field ${fieldMetadata.name}`, + ); + } + + const fieldName = + fieldMetadata.type === FieldMetadataType.MORPH_RELATION + ? computeMorphRelationFieldName({ + fieldName: fieldMetadata.name, + relationDirection: fieldMetadata.settings.relationType, + targetObjectMetadata, + }) + : fieldMetadata.name; + + entitySchemaRelationMap[fieldName] = { type: schemaRelationDetails.relationType, target: schemaRelationDetails.target, inverseSide: schemaRelationDetails.inverseSide, diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/find-many-morph-relation-records.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/find-many-morph-relation-records.integration-spec.ts new file mode 100644 index 00000000000..7bf31284328 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/find-many-morph-relation-records.integration-spec.ts @@ -0,0 +1,208 @@ +import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util'; +import { makeGraphqlAPIRequestWithApiKey } from 'test/integration/graphql/utils/make-graphql-api-request-with-api-key.util'; +import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; +import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { type EachTestingContext } from 'twenty-shared/testing'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; + +// todo @guillim : make ths test work to cover morph CORE api +xdescribe('findMany morph relation records', () => { + let createdObjectMetadataPersonId = ''; + let createdObjectMetadataOpportunityId = ''; + let createdObjectMetadataCompanyId = ''; + + beforeEach(async () => { + const { + data: { + createOneObject: { id: objectMetadataPersonId }, + }, + } = await createOneObjectMetadata({ + input: { + nameSingular: 'personForMorphRelation', + namePlural: 'peopleForMorphRelation', + labelSingular: 'Person For Morph Relation', + labelPlural: 'People For Morph Relation', + icon: 'IconPerson', + }, + }); + + createdObjectMetadataPersonId = objectMetadataPersonId; + + const { + data: { + createOneObject: { id: objectMetadataCompanyId }, + }, + } = await createOneObjectMetadata({ + input: { + nameSingular: 'companyForMorphRelation', + namePlural: 'companiesForMorphRelation', + labelSingular: 'Company For Morph Relation', + labelPlural: 'Companies For Morph Relation', + icon: 'IconCompany', + }, + }); + + createdObjectMetadataCompanyId = objectMetadataCompanyId; + + const { + data: { + createOneObject: { id: objectMetadataOpportunityId }, + }, + } = await createOneObjectMetadata({ + input: { + nameSingular: 'opportunityForMorphRelation', + namePlural: 'opportunitiesForMorphRelation', + labelSingular: 'Opportunity For Morph Relation', + labelPlural: 'Opportunities For Morph Relation', + icon: 'IconOpportunity', + }, + }); + + createdObjectMetadataOpportunityId = objectMetadataOpportunityId; + }); + afterEach(async () => { + await deleteOneObjectMetadata({ + input: { idToDelete: createdObjectMetadataPersonId }, + }); + await deleteOneObjectMetadata({ + input: { idToDelete: createdObjectMetadataOpportunityId }, + }); + await deleteOneObjectMetadata({ + input: { idToDelete: createdObjectMetadataCompanyId }, + }); + }); + + type EachTestingContextArray = EachTestingContext< + (args: { + objectMetadataId: string; + firstTargetObjectMetadataId: string; + secondTargetObjectMetadataId: string; + }) => { + relationType: RelationType; + objectMetadataId: string; + firstTargetObjectMetadataId: string; + secondTargetObjectMetadataId: string; + type: FieldMetadataType; + } + >[]; + + const eachTestingContextArray: EachTestingContextArray = [ + { + title: + 'should find many records with a MORPH_RELATION field type MANY_TO_ONE', + context: ({ + objectMetadataId, + firstTargetObjectMetadataId, + secondTargetObjectMetadataId, + }) => ({ + relationType: RelationType.MANY_TO_ONE, + objectMetadataId, + firstTargetObjectMetadataId, + secondTargetObjectMetadataId, + type: FieldMetadataType.MORPH_RELATION, + }), + }, + // { + // title: + // 'should find many records with a MORPH_RELATION field type ONE_TO_MANY', + // context: ({ + // objectMetadataId, + // firstTargetObjectMetadataId, + // secondTargetObjectMetadataId, + // }) => ({ + // relationType: RelationType.ONE_TO_MANY, + // objectMetadataId, + // firstTargetObjectMetadataId, + // secondTargetObjectMetadataId, + // type: FieldMetadataType.MORPH_RELATION, + // }), + // }, + ]; + + it.each(eachTestingContextArray)('$title', async ({ context }) => { + const contextPayload = context({ + objectMetadataId: createdObjectMetadataOpportunityId, + firstTargetObjectMetadataId: createdObjectMetadataPersonId, + secondTargetObjectMetadataId: createdObjectMetadataCompanyId, + }); + + const createdField = await createMorphRelationBetweenObjects({ + objectMetadataId: contextPayload.objectMetadataId, + firstTargetObjectMetadataId: contextPayload.firstTargetObjectMetadataId, + secondTargetObjectMetadataId: contextPayload.secondTargetObjectMetadataId, + type: contextPayload.type, + relationType: contextPayload.relationType, + }); + + // todo : fill with records + + expect(createdField.id).toBeDefined(); + expect(createdField.name).toBe('owner'); + + const MANY_TO_ONE_GQL_FIELDS = ` + id + name + ownerPersonForMorphRelationId + ownerPersonForMorphRelation { + id + name + } + ownerCompanyForMorphRelationId + ownerCompanyForMorphRelation { + id + name + } + `; + + // const ONE_TO_MANY_GQL_FIELDS = ` + // id + // name + // ownerPersonId + // ownerPerson { + // edges { + // node { + // id + // name + // } + // } + // } + // ownerCompanyId + // ownerCompany { + // edges { + // node { + // id + // name + // } + // } + // } + // `; + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'opportunityForMorphRelation', + objectMetadataPluralName: 'opportunitiesForMorphRelation', + gqlFields: MANY_TO_ONE_GQL_FIELDS, + }); + const response = await makeGraphqlAPIRequestWithApiKey(graphqlOperation); + + expect(response.body.errors).toBeUndefined(); + expect(response.body.data).toBeDefined(); + expect( + response.body.data.opportunitiesForMorphRelation.edges, + ).toBeDefined(); + // const isManyToOne = + // contextPayload.relationType === RelationType.MANY_TO_ONE; + + // if (isManyToOne) { + // ); + // } else { + + // } + + await deleteOneFieldMetadata({ + input: { idToDelete: createdField.id }, + }).catch(); + }); +});