From 2d17f7095acb19e9f897637ea2d99c5e775d09f4 Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Thu, 28 Mar 2024 17:48:42 +0400 Subject: [PATCH] feat(core): introduce GraphQL normalizer closes #234 --- .../core/src/importers/GraphQLImporter.ts | 23 ++-- .../core/src/importers/GraphQLNormalizer.ts | 122 ++++++++++++++++++ packages/core/tests/fixtures/graphql.json | 7 +- 3 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 packages/core/src/importers/GraphQLNormalizer.ts diff --git a/packages/core/src/importers/GraphQLImporter.ts b/packages/core/src/importers/GraphQLImporter.ts index 28d05411..9ba5f704 100644 --- a/packages/core/src/importers/GraphQLImporter.ts +++ b/packages/core/src/importers/GraphQLImporter.ts @@ -1,5 +1,6 @@ import { BaseImporter } from './BaseImporter'; import { ImporterType } from './ImporterType'; +import { GraphQLNormalizer } from './GraphQLNormalizer'; import { isArrayOfStrings } from '../utils'; import { type DocFormat, type Spec } from './Spec'; import { GraphQL, introspectionFromSchema } from '../types'; @@ -12,7 +13,7 @@ export class GraphQLImporter extends BaseImporter { return ImporterType.GRAPHQL; } - constructor() { + constructor(private readonly normalizer = new GraphQLNormalizer()) { super(); } @@ -26,7 +27,7 @@ export class GraphQLImporter extends BaseImporter { return spec ? { ...spec, - doc: await this.tryConvertSDL(spec.doc) + doc: await this.normalize(spec.doc) } : spec; } catch { @@ -55,21 +56,27 @@ export class GraphQLImporter extends BaseImporter { return `${url.hostname}-${checkSum}`.toLowerCase(); } + private async normalize(doc: GraphQL.Document){ + doc = await this.tryConvertSDL(doc); + + return this.normalizer.normalize(doc); + } + private async tryConvertSDL( - obj: GraphQL.Document + doc: GraphQL.Document ): Promise { - if (this.isGraphQLSDLEnvelope(obj)) { - const schema = await loadSchema(obj.data, { + if (this.isGraphQLSDLEnvelope(doc)) { + const schema = await loadSchema(doc.data, { loaders: [] }); - return { - ...obj, + doc = { + ...doc, data: introspectionFromSchema(schema) }; } - return obj; + return doc; } private isGraphQLSDLEnvelope( diff --git a/packages/core/src/importers/GraphQLNormalizer.ts b/packages/core/src/importers/GraphQLNormalizer.ts new file mode 100644 index 00000000..cf8f051f --- /dev/null +++ b/packages/core/src/importers/GraphQLNormalizer.ts @@ -0,0 +1,122 @@ +import { + GraphQL, + IntrospectionDirective, + IntrospectionField, + IntrospectionInputValue, + IntrospectionInterfaceType, + IntrospectionObjectType, + IntrospectionType, + IntrospectionNamedTypeRef, + IntrospectionSchema +} from '../types'; + +type DeepWriteable = { -readonly [P in keyof T]: DeepWriteable }; +type DeepPartial = { [P in keyof T]?: DeepPartial }; +type DpDw = DeepPartial>; + +export class GraphQLNormalizer { + public normalize(input: GraphQL.Document): GraphQL.Document { + const schema = (input as DpDw).data?.__schema; + + if (!schema || typeof schema !== 'object' || Array.isArray(schema)) { + return input; + } + + const result = JSON.parse(JSON.stringify(input)) as DpDw; + + this.normalizeSchema(result.data.__schema); + + return result as GraphQL.Document; + } + + private normalizeSchema( + schema: DeepPartial> + ) { + this.normalizeTypeRef(schema.queryType); + this.normalizeTypeRef(schema.mutationType); + this.normalizeTypeRef(schema.subscriptionType); + + this.normalizeDirectives(schema); + this.normalizeTypes(schema); + } + + private normalizeTypeRef( + typeRef: DpDw> + ) { + if (!!typeRef && typeof typeRef === 'object' && !Array.isArray(typeRef)) { + typeRef.kind ??= 'OBJECT'; + } + } + + private normalizeTypes(obj: { types?: DpDw[] }): void { + obj.types ??= []; + if (Array.isArray(obj.types)) { + obj.types.forEach((t) => this.normalizeType(t)); + } + } + + private normalizeType(type: DpDw) { + if (this.isTypeKind('OBJECT', type)) { + this.normalizeFields(type); + this.normalizeInterfaces(type); + } + + if (this.isTypeKind('INTERFACE', type)) { + this.normalizeFields(type); + this.normalizeInterfaces(type); + this.normalizePossibleTypes(type); + } + + if (this.isTypeKind('UNION', type)) { + this.normalizePossibleTypes(type); + } + + if (this.isTypeKind('INPUT_OBJECT', type)) { + this.normalizeInputFields(type); + } + } + + private normalizeDirectives(obj: { + directives?: DpDw[]; + }) { + obj.directives ??= []; + if (Array.isArray(obj.directives)) { + obj.directives.forEach((directive) => this.normalizeArgs(directive)); + } + } + private normalizeFields(obj: { fields?: DpDw[] }) { + obj.fields ??= []; + if (Array.isArray(obj.fields)) { + obj.fields.forEach((field) => this.normalizeArgs(field)); + } + } + + private normalizeInterfaces(obj: { + interfaces?: DpDw[]; + }) { + obj.interfaces ??= []; + } + + private normalizePossibleTypes(obj: { + possibleTypes?: DpDw[]; + }) { + obj.possibleTypes ??= []; + } + + private normalizeInputFields(obj: { + inputFields?: DpDw[]; + }) { + obj.inputFields ??= []; + } + + private normalizeArgs(obj: { args?: DpDw[] }) { + obj.args ??= []; + } + + private isTypeKind< + T extends IntrospectionType['kind'], + U extends IntrospectionType + >(kind: T, type: DpDw): type is DpDw> { + return typeof type === 'object' && 'kind' in type && type.kind === kind; + } +} diff --git a/packages/core/tests/fixtures/graphql.json b/packages/core/tests/fixtures/graphql.json index 3f1443de..ab116d5d 100644 --- a/packages/core/tests/fixtures/graphql.json +++ b/packages/core/tests/fixtures/graphql.json @@ -20,7 +20,7 @@ { "name": "id", "description": null, - "args": [], + "args": null, "type": { "kind": "NON_NULL", "name": null, @@ -35,7 +35,7 @@ } ], "inputFields": null, - "interfaces": [], + "interfaces": null, "enumValues": null, "possibleTypes": [ { @@ -70,7 +70,6 @@ { "name": "id", "description": null, - "args": [], "type": { "kind": "NON_NULL", "name": null, @@ -147,7 +146,6 @@ { "name": "barField", "description": null, - "args": [], "type": { "kind": "NON_NULL", "name": null, @@ -223,7 +221,6 @@ } ], "inputFields": null, - "interfaces": [], "enumValues": null, "possibleTypes": null },