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
@@ -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;
}