1import type {
2 IntrospectionQuery,
3 IntrospectionTypeRef,
4 IntrospectionInputValue,
5 IntrospectionType,
6} from './graphql';
7
8export interface SchemaField {
9 name: string;
10 type: IntrospectionTypeRef;
11 args(): Record<string, IntrospectionInputValue | void>;
12}
13
14export interface SchemaObject {
15 name: string;
16 kind: 'INTERFACE' | 'OBJECT';
17 interfaces(): Record<string, unknown>;
18 fields(): Record<string, SchemaField | void>;
19}
20
21export interface SchemaUnion {
22 name: string;
23 kind: 'UNION';
24 types(): Record<string, unknown>;
25}
26
27export interface SchemaIntrospector {
28 query: string | null;
29 mutation: string | null;
30 subscription: string | null;
31 types?: Map<string, SchemaObject | SchemaUnion>;
32 isSubType(abstract: string, possible: string): boolean;
33}
34
35export interface PartialIntrospectionSchema {
36 queryType: { name: string; kind?: any };
37 mutationType?: { name: string; kind?: any } | null;
38 subscriptionType?: { name: string; kind?: any } | null;
39 types?: readonly any[];
40}
41
42export type IntrospectionData =
43 | IntrospectionQuery
44 | { __schema: PartialIntrospectionSchema };
45
46export const buildClientSchema = ({
47 __schema,
48}: IntrospectionData): SchemaIntrospector => {
49 const typemap: Map<string, SchemaObject | SchemaUnion> = new Map();
50
51 const buildNameMap = <T extends { name: string }>(
52 arr: ReadonlyArray<T>
53 ): (() => { [name: string]: T }) => {
54 let map: Record<string, T> | void;
55 return () => {
56 if (!map) {
57 map = {};
58 for (let i = 0; i < arr.length; i++) map[arr[i].name] = arr[i];
59 }
60 return map;
61 };
62 };
63
64 const buildType = (
65 type: IntrospectionType
66 ): SchemaObject | SchemaUnion | void => {
67 switch (type.kind) {
68 case 'OBJECT':
69 case 'INTERFACE':
70 return {
71 name: type.name,
72 kind: type.kind as 'OBJECT' | 'INTERFACE',
73 interfaces: buildNameMap(type.interfaces || []),
74 fields: buildNameMap(
75 type.fields!.map((field: any) => ({
76 name: field.name,
77 type: field.type,
78 args: buildNameMap(field.args),
79 }))
80 ),
81 } as SchemaObject;
82 case 'UNION':
83 return {
84 name: type.name,
85 kind: type.kind as 'UNION',
86 types: buildNameMap(type.possibleTypes || []),
87 } as SchemaUnion;
88 }
89 };
90
91 const schema: SchemaIntrospector = {
92 query: __schema.queryType ? __schema.queryType.name : null,
93 mutation: __schema.mutationType ? __schema.mutationType.name : null,
94 subscription: __schema.subscriptionType
95 ? __schema.subscriptionType.name
96 : null,
97 types: undefined,
98 isSubType(abstract: string, possible: string) {
99 const abstractType = typemap.get(abstract);
100 const possibleType = typemap.get(possible);
101 if (!abstractType || !possibleType) {
102 return false;
103 } else if (abstractType.kind === 'UNION') {
104 return !!abstractType.types()[possible];
105 } else if (
106 abstractType.kind !== 'OBJECT' &&
107 possibleType.kind === 'OBJECT'
108 ) {
109 return !!possibleType.interfaces()[abstract];
110 } else {
111 return abstract === possible;
112 }
113 },
114 };
115
116 if (__schema.types) {
117 schema.types = typemap;
118 for (let i = 0; i < __schema.types.length; i++) {
119 const type = __schema.types[i];
120 if (type && type.name) {
121 const out = buildType(type);
122 if (out) typemap.set(type.name, out);
123 }
124 }
125 }
126
127 return schema;
128};