Connect - Update Gql schema generation (#13001)

To test : 

```
mutation testMutation {
  createPerson(
    data: {company: {connect: {where: {domainName: {primaryLinkUrl: "airbnb.com"}}}}}
  ) {
    id
  }
}
```

closes https://github.com/twentyhq/core-team-issues/issues/1167
This commit is contained in:
Etienne
2025-07-02 16:37:24 +02:00
committed by GitHub
parent 54e233d7b9
commit ba67e0d5f4
14 changed files with 604 additions and 98 deletions
@@ -5,13 +5,16 @@ import {
GraphQLOutputType,
} from 'graphql';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
import { formatRelationConnectInputTarget } from 'src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
@@ -30,6 +33,7 @@ type TypeFactory<T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settings: any;
isIdField: boolean;
isRelationConnectField?: boolean;
},
) => T extends InputTypeDefinitionKind
? GraphQLInputType
@@ -38,87 +42,189 @@ type TypeFactory<T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind> =
export const generateFields = <
T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind,
>(
objectMetadata: ObjectMetadataInterface,
kind: T,
options: WorkspaceBuildSchemaOptions,
typeFactory: TypeFactory<T>,
): T extends InputTypeDefinitionKind
>({
objectMetadata,
kind,
options,
typeFactory,
isRelationConnectEnabled = false,
}: {
objectMetadata: ObjectMetadataInterface;
kind: T;
options: WorkspaceBuildSchemaOptions;
typeFactory: TypeFactory<T>;
isRelationConnectEnabled?: boolean;
}): T extends InputTypeDefinitionKind
? GraphQLInputFieldConfigMap
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
GraphQLFieldConfigMap<any, any> => {
const fields = {};
const allGeneratedFields = {};
for (const fieldMetadata of objectMetadata.fields) {
let generatedField;
if (
isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
) &&
fieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE
isFieldMetadataInterfaceOfType(fieldMetadata, FieldMetadataType.RELATION)
) {
continue;
generatedField = generateRelationField({
fieldMetadata,
kind,
options,
typeFactory,
isRelationConnectEnabled,
});
} else {
generatedField = generateField({
fieldMetadata,
kind,
options,
typeFactory,
});
}
const target = isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
Object.assign(allGeneratedFields, generatedField);
}
const typeFactoryOptions = isInputTypeDefinitionKind(kind)
? {
nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue,
isArray:
kind !== InputTypeDefinitionKind.Filter &&
fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
isIdField: fieldMetadata.name === 'id',
}
: {
nullable: fieldMetadata.isNullable,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
// Scalar type is already defined in the entity itself.
isIdField: false,
};
return allGeneratedFields;
};
const type = typeFactory.create(
target,
const getTarget = <T extends FieldMetadataType>(
fieldMetadata: FieldMetadataInterface<T>,
) => {
return isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
};
const getTypeFactoryOptions = <T extends FieldMetadataType>(
fieldMetadata: FieldMetadataInterface<T>,
kind: InputTypeDefinitionKind | ObjectTypeDefinitionKind,
) => {
return isInputTypeDefinitionKind(kind)
? {
nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue,
isArray:
kind !== InputTypeDefinitionKind.Filter &&
fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
isIdField: fieldMetadata.name === 'id',
}
: {
nullable: fieldMetadata.isNullable,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
// Scalar type is already defined in the entity itself.
isIdField: false,
};
};
const generateField = <
T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind,
>({
fieldMetadata,
kind,
options,
typeFactory,
}: {
fieldMetadata: FieldMetadataInterface;
kind: T;
options: WorkspaceBuildSchemaOptions;
typeFactory: TypeFactory<T>;
}) => {
const target = getTarget(fieldMetadata);
const typeFactoryOptions = getTypeFactoryOptions(fieldMetadata, kind);
const type = typeFactory.create(
target,
fieldMetadata.type,
kind,
options,
typeFactoryOptions,
);
return {
[fieldMetadata.name]: {
type,
description: fieldMetadata.description,
},
};
};
const generateRelationField = <
T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind,
>({
fieldMetadata,
kind,
options,
typeFactory,
isRelationConnectEnabled,
}: {
fieldMetadata: FieldMetadataInterface<FieldMetadataType.RELATION>;
kind: T;
options: WorkspaceBuildSchemaOptions;
typeFactory: TypeFactory<T>;
isRelationConnectEnabled: boolean;
}) => {
const relationField = {};
if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) {
return relationField;
}
const joinColumnName = fieldMetadata.settings?.joinColumnName;
if (!joinColumnName) {
throw new Error('Join column name is not defined');
}
const target = getTarget(fieldMetadata);
const typeFactoryOptions = getTypeFactoryOptions(fieldMetadata, kind);
let type = typeFactory.create(
target,
fieldMetadata.type,
kind,
options,
typeFactoryOptions,
);
// @ts-expect-error legacy noImplicitAny
relationField[joinColumnName] = {
type,
description: fieldMetadata.description,
};
if (
[InputTypeDefinitionKind.Create, InputTypeDefinitionKind.Update].includes(
kind as InputTypeDefinitionKind,
) &&
isDefined(fieldMetadata.relationTargetObjectMetadataId) &&
isRelationConnectEnabled
) {
type = typeFactory.create(
formatRelationConnectInputTarget(
fieldMetadata.relationTargetObjectMetadataId,
),
fieldMetadata.type,
kind,
options,
typeFactoryOptions,
{
...typeFactoryOptions,
isRelationConnectField: true,
},
);
if (
isFieldMetadataInterfaceOfType(
fieldMetadata,
FieldMetadataType.RELATION,
) &&
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE
) {
const joinColumnName = fieldMetadata.settings?.joinColumnName;
if (!joinColumnName) {
throw new Error('Join column name is not defined');
}
// @ts-expect-error legacy noImplicitAny
fields[joinColumnName] = {
type,
description: fieldMetadata.description,
};
}
// @ts-expect-error legacy noImplicitAny
fields[fieldMetadata.name] = {
type,
description: fieldMetadata.description,
};
}
return fields;
// @ts-expect-error legacy noImplicitAny
relationField[fieldMetadata.name] = {
type: type,
description: fieldMetadata.description,
};
return relationField;
};
// Type guard