morph backend CORE api (#13517)

# Morph Relation needs some work on the core API

Especially on the Morph types

<img width="682" height="394" alt="Screenshot 2025-07-31 at 12 19 40"
src="https://github.com/user-attachments/assets/8b185ae3-3fe7-4886-97b1-ec143bb1b0f4"
/>


## Focus Areas:
- GraphQL schema generation
- Query parsing and field selection
- Morph relation field support
- Input type definition preparation
This commit is contained in:
Guillim
2025-08-12 11:47:02 +02:00
committed by GitHub
parent 6cc947ea01
commit 63c8bb9530
32 changed files with 742 additions and 111 deletions
@@ -37,6 +37,7 @@ export class GraphqlQuerySelectedFieldsRelationParser {
const relationAccumulator = fieldParser.parse(
fieldValue,
targetObjectMetadata,
this.objectMetadataMaps,
);
accumulator.select[fieldKey] = {
@@ -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<Record<string, any>>,
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<Record<string, any>>,
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<Record<string, any>>,
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(
@@ -119,6 +119,7 @@ export class GraphqlQueryParser {
parentObjectMetadata: ObjectMetadataItemWithFieldMaps,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
graphqlSelectedFields: Partial<Record<string, any>>,
objectMetadataMaps: ObjectMetadataMaps,
): GraphqlQuerySelectedFieldsResult {
const objectMetadataMapItem = getObjectMetadataMapItemByNameSingular(
this.objectMetadataMaps,
@@ -139,6 +140,7 @@ export class GraphqlQueryParser {
return selectedFieldsParser.parse(
graphqlSelectedFields,
objectMetadataMapItem,
objectMetadataMaps,
);
}
}
@@ -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<string, any> = {};
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;
@@ -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<string, unknown>;
}): Promise<void> {
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,
@@ -174,6 +174,7 @@ export abstract class GraphqlQueryBaseResolverService<
graphqlQueryParser.parseSelectedFields(
objectMetadataItemWithFieldMaps,
selectedFields,
options.objectMetadataMaps,
);
const graphqlQueryResolverExecutionArgs = {
@@ -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<CreateManyResolverArgs>,
): Promise<InsertResult> {
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<CreateManyResolverArgs>,
): Promise<InsertResult> {
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<CreateManyResolverArgs>,
objectRecords: InsertResult,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
objectMetadataMaps: ObjectMetadataMaps,
): Promise<ObjectRecord[]> {
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
@@ -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
@@ -48,6 +48,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const deletedObjectRecords = await queryBuilder
@@ -40,6 +40,7 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const deletedObjectRecords = await queryBuilder
@@ -46,6 +46,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const deletedObjectRecords = await queryBuilder
@@ -38,6 +38,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const deletedObjectRecords = await queryBuilder
@@ -68,6 +68,7 @@ export class GraphqlQueryFindDuplicatesResolverService extends GraphqlQueryBaseR
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
if (executionArgs.args.ids) {
@@ -122,6 +122,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const objectRecords = (await queryBuilder
@@ -56,6 +56,7 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const objectRecord = await queryBuilder
@@ -247,7 +247,8 @@ export class GraphqlQueryMergeManyResolverService extends GraphqlQueryBaseResolv
priorityRecordId: string,
mergedData: Partial<ObjectRecord>,
): Promise<ObjectRecord> {
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
@@ -48,6 +48,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const restoredObjectRecords = await queryBuilder
@@ -40,6 +40,7 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const restoredObjectRecords = await queryBuilder
@@ -48,6 +48,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const updatedObjectRecords = await queryBuilder
@@ -40,6 +40,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
});
const updatedObjectRecords = await queryBuilder
@@ -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({
@@ -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<string, unknown>;
relations: Record<string, unknown>;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: ObjectMetadataMaps;
}): string[] => {
return Object.entries(
buildColumnsToSelect({
select,
relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
}),
)
.filter(([_columnName, value]) => value === true)
@@ -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<string, unknown>;
relations: Record<string, unknown>;
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
objectMetadataMaps: {
byId: Partial<
Record<
string,
Pick<ObjectMetadataItemWithFieldMaps, 'nameSingular' | 'namePlural'>
>
>;
};
}) => {
const requiredRelationColumns = getRequiredRelationColumns(
relations,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
);
const fieldsToSelect: Record<string, boolean> = Object.entries(select)
@@ -35,37 +46,59 @@ export const buildColumnsToSelect = ({
const getRequiredRelationColumns = (
relations: Record<string, unknown>,
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
objectMetadataItem: Pick<ObjectMetadataItemWithFieldMaps, 'fieldsById'>,
objectMetadataMaps: {
byId: Partial<
Record<
string,
Pick<ObjectMetadataItemWithFieldMaps, 'nameSingular' | 'namePlural'>
>
>;
},
): 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);
}
}
@@ -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;
}