Skip to content

Commit

Permalink
feat(core): introduce GraphQL normalizer
Browse files Browse the repository at this point in the history
closes #234
  • Loading branch information
ostridm committed Mar 28, 2024
1 parent 585f7d2 commit 2d17f70
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 13 deletions.
23 changes: 15 additions & 8 deletions packages/core/src/importers/GraphQLImporter.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,7 +13,7 @@ export class GraphQLImporter extends BaseImporter<ImporterType.GRAPHQL> {
return ImporterType.GRAPHQL;
}

constructor() {
constructor(private readonly normalizer = new GraphQLNormalizer()) {
super();
}

Expand All @@ -26,7 +27,7 @@ export class GraphQLImporter extends BaseImporter<ImporterType.GRAPHQL> {
return spec
? {
...spec,
doc: await this.tryConvertSDL(spec.doc)
doc: await this.normalize(spec.doc)
}
: spec;
} catch {
Expand Down Expand Up @@ -55,21 +56,27 @@ export class GraphQLImporter extends BaseImporter<ImporterType.GRAPHQL> {
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<GraphQL.Document> {
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(
Expand Down
122 changes: 122 additions & 0 deletions packages/core/src/importers/GraphQLNormalizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
GraphQL,
IntrospectionDirective,
IntrospectionField,
IntrospectionInputValue,
IntrospectionInterfaceType,
IntrospectionObjectType,
IntrospectionType,
IntrospectionNamedTypeRef,
IntrospectionSchema
} from '../types';

type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
type DeepPartial<T> = { [P in keyof T]?: DeepPartial<T[P]> };
type DpDw<T> = DeepPartial<DeepWriteable<T>>;

export class GraphQLNormalizer {
public normalize(input: GraphQL.Document): GraphQL.Document {
const schema = (input as DpDw<GraphQL.Document>).data?.__schema;

if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
return input;
}

const result = JSON.parse(JSON.stringify(input)) as DpDw<GraphQL.Document>;

this.normalizeSchema(result.data.__schema);

return result as GraphQL.Document;
}

private normalizeSchema(
schema: DeepPartial<DeepWriteable<IntrospectionSchema>>
) {
this.normalizeTypeRef(schema.queryType);
this.normalizeTypeRef(schema.mutationType);
this.normalizeTypeRef(schema.subscriptionType);

this.normalizeDirectives(schema);
this.normalizeTypes(schema);
}

private normalizeTypeRef(
typeRef: DpDw<IntrospectionNamedTypeRef<IntrospectionObjectType>>
) {
if (!!typeRef && typeof typeRef === 'object' && !Array.isArray(typeRef)) {
typeRef.kind ??= 'OBJECT';
}
}

private normalizeTypes(obj: { types?: DpDw<IntrospectionType>[] }): void {
obj.types ??= [];
if (Array.isArray(obj.types)) {
obj.types.forEach((t) => this.normalizeType(t));
}
}

private normalizeType(type: DpDw<IntrospectionType>) {
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<IntrospectionDirective>[];
}) {
obj.directives ??= [];
if (Array.isArray(obj.directives)) {
obj.directives.forEach((directive) => this.normalizeArgs(directive));
}
}
private normalizeFields(obj: { fields?: DpDw<IntrospectionField>[] }) {
obj.fields ??= [];
if (Array.isArray(obj.fields)) {
obj.fields.forEach((field) => this.normalizeArgs(field));
}
}

private normalizeInterfaces(obj: {
interfaces?: DpDw<IntrospectionInterfaceType>[];
}) {
obj.interfaces ??= [];
}

private normalizePossibleTypes(obj: {
possibleTypes?: DpDw<IntrospectionObjectType>[];
}) {
obj.possibleTypes ??= [];
}

private normalizeInputFields(obj: {
inputFields?: DpDw<IntrospectionInputValue>[];
}) {
obj.inputFields ??= [];
}

private normalizeArgs(obj: { args?: DpDw<IntrospectionInputValue>[] }) {
obj.args ??= [];
}

private isTypeKind<
T extends IntrospectionType['kind'],
U extends IntrospectionType
>(kind: T, type: DpDw<U>): type is DpDw<Extract<U, { kind: T }>> {
return typeof type === 'object' && 'kind' in type && type.kind === kind;
}
}
7 changes: 2 additions & 5 deletions packages/core/tests/fixtures/graphql.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
{
"name": "id",
"description": null,
"args": [],
"args": null,
"type": {
"kind": "NON_NULL",
"name": null,
Expand All @@ -35,7 +35,7 @@
}
],
"inputFields": null,
"interfaces": [],
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
Expand Down Expand Up @@ -70,7 +70,6 @@
{
"name": "id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
Expand Down Expand Up @@ -147,7 +146,6 @@
{
"name": "barField",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
Expand Down Expand Up @@ -223,7 +221,6 @@
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
Expand Down

0 comments on commit 2d17f70

Please sign in to comment.