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:
+16
@@ -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({
|
||||
|
||||
+4
@@ -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)
|
||||
|
||||
+58
-25
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+85
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user