simplify gql schema gen (#14426)

closes https://github.com/twentyhq/core-team-issues/issues/1358
This commit is contained in:
Etienne
2025-09-15 14:01:53 +02:00
committed by GitHub
parent 1cdfc0fd62
commit 06605b2f36
80 changed files with 2728 additions and 2080 deletions
@@ -2,7 +2,7 @@ import { GraphQLBoolean, GraphQLInt, GraphQLString } from 'graphql';
import { type WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
@@ -13,20 +13,20 @@ describe('getResolverArgs', () => {
last: { type: GraphQLInt, isNullable: true },
before: { type: GraphQLString, isNullable: true },
after: { type: GraphQLString, isNullable: true },
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: true },
filter: { kind: GqlInputTypeDefinitionKind.Filter, isNullable: true },
orderBy: {
kind: InputTypeDefinitionKind.OrderBy,
kind: GqlInputTypeDefinitionKind.OrderBy,
isNullable: true,
isArray: true,
},
limit: { type: GraphQLInt, isNullable: true },
},
findOne: {
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
filter: { kind: GqlInputTypeDefinitionKind.Filter, isNullable: false },
},
createMany: {
data: {
kind: InputTypeDefinitionKind.Create,
kind: GqlInputTypeDefinitionKind.Create,
isNullable: false,
isArray: true,
},
@@ -37,7 +37,7 @@ describe('getResolverArgs', () => {
},
},
createOne: {
data: { kind: InputTypeDefinitionKind.Create, isNullable: false },
data: { kind: GqlInputTypeDefinitionKind.Create, isNullable: false },
upsert: {
isArray: false,
isNullable: true,
@@ -46,16 +46,16 @@ describe('getResolverArgs', () => {
},
updateOne: {
id: { type: UUIDScalarType, isNullable: false },
data: { kind: InputTypeDefinitionKind.Update, isNullable: false },
data: { kind: GqlInputTypeDefinitionKind.Update, isNullable: false },
},
deleteOne: {
id: { type: UUIDScalarType, isNullable: false },
},
restoreMany: {
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
filter: { kind: GqlInputTypeDefinitionKind.Filter, isNullable: false },
},
destroyMany: {
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
filter: { kind: GqlInputTypeDefinitionKind.Filter, isNullable: false },
},
};
@@ -0,0 +1,13 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { type CompositeProperty } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
export function computeCompositeFieldTypeOptions(
property: CompositeProperty<FieldMetadataType>,
) {
return {
nullable: !property.isRequired,
isArray:
property.type === FieldMetadataType.MULTI_SELECT || property.isArray,
};
}
@@ -0,0 +1,20 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { type TypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const computeFieldInputTypeOptions = <T extends FieldMetadataType>(
fieldMetadata: FieldMetadataEntity<T>,
kind: GqlInputTypeDefinitionKind,
): TypeOptions => {
return {
nullable: fieldMetadata.isNullable ?? undefined,
defaultValue: fieldMetadata.defaultValue,
isArray:
kind !== GqlInputTypeDefinitionKind.Filter &&
fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
isIdField: fieldMetadata.name === 'id',
};
};
@@ -0,0 +1,9 @@
import { computeCompositeFieldEnumTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-enum-type-key.util';
describe('computeCompositeFieldEnumTypeKey', () => {
it('should compute the correct key', () => {
expect(computeCompositeFieldEnumTypeKey('Actor', 'source')).toBe(
'ActorSourceEnum',
);
});
});
@@ -0,0 +1,15 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
describe('computeCompositeFieldInputTypeKey', () => {
it('should compute the correct key for FULL_NAME field', () => {
expect(
computeCompositeFieldInputTypeKey(
FieldMetadataType.FULL_NAME,
GqlInputTypeDefinitionKind.Create,
),
).toBe('FullNameCreateInput');
});
});
@@ -0,0 +1,13 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { computeCompositeFieldObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-object-type-key.util';
describe('computeCompositeFieldObjectTypeKey', () => {
it('should return the correct composite field object type key', () => {
const fieldMetadataType = FieldMetadataType.LINKS;
const result = computeCompositeFieldObjectTypeKey(fieldMetadataType);
expect(result).toBe('Links');
});
});
@@ -0,0 +1,7 @@
import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util';
describe('computeEnumFieldGqlTypeKey', () => {
it('should compute the correct key', () => {
expect(computeEnumFieldGqlTypeKey('User', 'role')).toBe('UserRoleEnum');
});
});
@@ -0,0 +1,13 @@
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util';
describe('computeObjectMetadataInputTypeKey', () => {
it('should compute the correct key for user object with Create kind', () => {
expect(
computeObjectMetadataInputTypeKey(
'user',
GqlInputTypeDefinitionKind.Create,
),
).toBe('UserCreateInput');
});
});
@@ -0,0 +1,13 @@
import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/object-type-definition-kind.enum';
import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util';
describe('computeObjectMetadataObjectTypeKey', () => {
it('should compute the correct key for user object with Plain kind', () => {
expect(
computeObjectMetadataObjectTypeKey(
'user',
ObjectTypeDefinitionKind.Plain,
),
).toBe('User');
});
});
@@ -0,0 +1,11 @@
import { computeRelationConnectInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-relation-connect-input-type-key.util';
describe('computeRelationConnectInputTypeKey', () => {
it('should return the correct relation connect input type key', () => {
const objectMetadataNameId = '20202020-f401-4d8a-a731-64d007c27bad';
const result = computeRelationConnectInputTypeKey(objectMetadataNameId);
expect(result).toBe('20202020-f401-4d8a-a731-64d007c27bad-ConnectInput');
});
});
@@ -0,0 +1,10 @@
import { pascalCase } from 'src/utils/pascal-case';
export const computeCompositeFieldEnumTypeKey = (
fieldMetadataType: string,
compositePropertyName: string,
): string => {
return `${pascalCase(fieldMetadataType)}${pascalCase(
compositePropertyName,
)}Enum`;
};
@@ -0,0 +1,13 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { type GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { pascalCase } from 'src/utils/pascal-case';
export const computeCompositeFieldInputTypeKey = (
fieldType: FieldMetadataType,
kind: GqlInputTypeDefinitionKind,
): string => {
const name = pascalCase(fieldType.toString().toLowerCase());
return `${pascalCase(name)}${kind.toString()}Input`;
};
@@ -0,0 +1,10 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/object-type-definition-kind.enum';
import { pascalCase } from 'src/utils/pascal-case';
export const computeCompositeFieldObjectTypeKey = (
compositeFieldMetadataType: FieldMetadataType,
): string => {
return `${pascalCase(compositeFieldMetadataType)}${ObjectTypeDefinitionKind.Plain.toString()}`;
};
@@ -0,0 +1,10 @@
import { pascalCase } from 'src/utils/pascal-case';
export const computeEnumFieldGqlTypeKey = (
objectMetadataName: string,
fieldMetadataName: string,
): string => {
return `${pascalCase(objectMetadataName)}${pascalCase(
fieldMetadataName,
)}Enum`;
};
@@ -0,0 +1,9 @@
import { type GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { pascalCase } from 'src/utils/pascal-case';
export const computeObjectMetadataInputTypeKey = (
objectMetadataNameSingular: string,
kind: GqlInputTypeDefinitionKind,
) => {
return `${pascalCase(objectMetadataNameSingular)}${kind.toString()}Input`;
};
@@ -0,0 +1,9 @@
import { type ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/object-type-definition-kind.enum';
import { pascalCase } from 'src/utils/pascal-case';
export const computeObjectMetadataObjectTypeKey = (
objectMetadataNameSingular: string,
kind: ObjectTypeDefinitionKind,
) => {
return `${pascalCase(objectMetadataNameSingular)}${kind.toString()}`;
};
@@ -0,0 +1,3 @@
export const computeRelationConnectInputTypeKey = (
objectMetadataNameId: string,
) => `${objectMetadataNameId}-ConnectInput`;
@@ -0,0 +1,25 @@
import {
GraphQLBoolean,
type GraphQLEnumType,
GraphQLInputObjectType,
type GraphQLInputType,
GraphQLList,
} from 'graphql';
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
export const createGqlEnumFilterType = (
enumType: GraphQLEnumType,
): GraphQLInputType => {
return new GraphQLInputObjectType({
name: `${enumType.name}Filter`,
fields: () => ({
eq: { type: enumType },
neq: { type: enumType },
in: { type: new GraphQLList(enumType) },
containsAny: { type: new GraphQLList(enumType) },
is: { type: FilterIs },
isEmptyArray: { type: GraphQLBoolean },
}),
});
};
@@ -1,237 +0,0 @@
import {
type GraphQLFieldConfigMap,
type GraphQLInputFieldConfigMap,
type GraphQLInputType,
type GraphQLOutputType,
} from 'graphql';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-options.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 { type 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 { extractGraphQLRelationFieldNames } from 'src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util';
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
type TypeFactory<T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind> =
{
create: (
target: string,
fieldType: FieldMetadataType,
kind: T,
options: WorkspaceBuildSchemaOptions,
additionalOptions: {
nullable?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultValue?: any;
isArray: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settings: any;
isIdField: boolean;
isRelationConnectField?: boolean;
},
) => T extends InputTypeDefinitionKind
? GraphQLInputType
: GraphQLOutputType;
};
export const generateFields = <
T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind,
>({
objectMetadata,
kind,
options,
typeFactory,
}: {
objectMetadata: ObjectMetadataEntity;
kind: T;
options: WorkspaceBuildSchemaOptions;
typeFactory: TypeFactory<T>;
}): T extends InputTypeDefinitionKind
? GraphQLInputFieldConfigMap
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
GraphQLFieldConfigMap<any, any> => {
const allGeneratedFields = {};
for (const fieldMetadata of objectMetadata.fields) {
let generatedField;
const isRelation = isFieldMetadataRelationOrMorphRelation(fieldMetadata);
if (isRelation) {
generatedField = generateRelationField({
fieldMetadata: fieldMetadata as FieldMetadataEntity<
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>,
kind,
options,
typeFactory,
});
} else {
generatedField = generateField({
fieldMetadata,
kind,
options,
typeFactory,
});
}
Object.assign(allGeneratedFields, generatedField);
}
return allGeneratedFields;
};
const getTarget = <T extends FieldMetadataType>(
fieldMetadata: FieldMetadataEntity<T>,
) => {
return isCompositeFieldMetadataType(fieldMetadata.type)
? fieldMetadata.type.toString()
: fieldMetadata.id;
};
const getTypeFactoryOptions = <T extends FieldMetadataType>(
fieldMetadata: FieldMetadataEntity<T>,
kind: InputTypeDefinitionKind | ObjectTypeDefinitionKind,
) => {
return isInputTypeDefinitionKind(kind)
? {
nullable: fieldMetadata.isNullable ?? undefined,
defaultValue: fieldMetadata.defaultValue,
isArray:
kind !== InputTypeDefinitionKind.Filter &&
fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
isIdField: fieldMetadata.name === 'id',
}
: {
nullable: fieldMetadata.isNullable ?? undefined,
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: FieldMetadataEntity;
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,
}: {
fieldMetadata: FieldMetadataEntity<
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>;
kind: T;
options: WorkspaceBuildSchemaOptions;
typeFactory: TypeFactory<T>;
}) => {
const relationFields = {};
if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) {
return relationFields;
}
const { joinColumnName, fieldMetadataName } =
extractGraphQLRelationFieldNames(fieldMetadata);
const target = getTarget(fieldMetadata);
const typeFactoryOptions = getTypeFactoryOptions(fieldMetadata, kind);
let type = typeFactory.create(
target,
fieldMetadata.type,
kind,
options,
typeFactoryOptions,
);
// @ts-expect-error legacy noImplicitAny
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 relationFields;
if (
[InputTypeDefinitionKind.Create, InputTypeDefinitionKind.Update].includes(
kind as InputTypeDefinitionKind,
) &&
isDefined(fieldMetadata.relationTargetObjectMetadataId)
) {
type = typeFactory.create(
formatRelationConnectInputTarget(
fieldMetadata.relationTargetObjectMetadataId,
),
fieldMetadata.type,
kind,
options,
{
...typeFactoryOptions,
isRelationConnectField: true,
},
);
// todo @guillim
// @ts-expect-error legacy noImplicitAny
relationFields[fieldMetadataName] = {
type: type,
description: fieldMetadata.description,
};
}
return relationFields;
};
// Type guard
const isInputTypeDefinitionKind = (
kind: InputTypeDefinitionKind | ObjectTypeDefinitionKind,
): kind is InputTypeDefinitionKind => {
return Object.values(InputTypeDefinitionKind).includes(
kind as InputTypeDefinitionKind,
);
};
@@ -3,7 +3,7 @@ import { GraphQLBoolean, GraphQLInt, GraphQLString } from 'graphql';
import { type WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { type ArgMetadata } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
export const getResolverArgs = (
@@ -33,11 +33,11 @@ export const getResolverArgs = (
isNullable: true,
},
filter: {
kind: InputTypeDefinitionKind.Filter,
kind: GqlInputTypeDefinitionKind.Filter,
isNullable: true,
},
orderBy: {
kind: InputTypeDefinitionKind.OrderBy,
kind: GqlInputTypeDefinitionKind.OrderBy,
isNullable: true,
isArray: true,
},
@@ -46,14 +46,14 @@ export const getResolverArgs = (
case 'deleteMany':
return {
filter: {
kind: InputTypeDefinitionKind.Filter,
kind: GqlInputTypeDefinitionKind.Filter,
isNullable: false,
},
};
case 'createMany':
return {
data: {
kind: InputTypeDefinitionKind.Create,
kind: GqlInputTypeDefinitionKind.Create,
isNullable: false,
isArray: true,
},
@@ -66,7 +66,7 @@ export const getResolverArgs = (
case 'createOne':
return {
data: {
kind: InputTypeDefinitionKind.Create,
kind: GqlInputTypeDefinitionKind.Create,
isNullable: false,
},
upsert: {
@@ -82,7 +82,7 @@ export const getResolverArgs = (
isNullable: false,
},
data: {
kind: InputTypeDefinitionKind.Update,
kind: GqlInputTypeDefinitionKind.Update,
isNullable: false,
},
};
@@ -94,7 +94,7 @@ export const getResolverArgs = (
isArray: true,
},
data: {
kind: InputTypeDefinitionKind.Create,
kind: GqlInputTypeDefinitionKind.Create,
isNullable: true,
isArray: true,
},
@@ -116,18 +116,18 @@ export const getResolverArgs = (
case 'updateMany':
return {
data: {
kind: InputTypeDefinitionKind.Update,
kind: GqlInputTypeDefinitionKind.Update,
isNullable: false,
},
filter: {
kind: InputTypeDefinitionKind.Filter,
kind: GqlInputTypeDefinitionKind.Filter,
isNullable: false,
},
};
case 'restoreMany':
return {
filter: {
kind: InputTypeDefinitionKind.Filter,
kind: GqlInputTypeDefinitionKind.Filter,
isNullable: false,
},
};
@@ -141,7 +141,7 @@ export const getResolverArgs = (
case 'destroyMany':
return {
filter: {
kind: InputTypeDefinitionKind.Filter,
kind: GqlInputTypeDefinitionKind.Filter,
isNullable: false,
},
};