1import type {
2 InlineFragmentNode,
3 FragmentDefinitionNode,
4} from '@0no-co/graphql.web';
5
6import { warn, invariant } from '../helpers/help';
7import { getTypeCondition } from './node';
8import type { SchemaIntrospector, SchemaObject } from './schema';
9
10import type {
11 KeyingConfig,
12 UpdatesConfig,
13 ResolverConfig,
14 OptimisticMutationConfig,
15 Logger,
16} from '../types';
17
18const BUILTIN_NAME = '__';
19
20export const isFieldNullable = (
21 schema: SchemaIntrospector,
22 typename: string,
23 fieldName: string,
24 logger: Logger | undefined
25): boolean => {
26 const field = getField(schema, typename, fieldName, logger);
27 return !!field && field.type.kind !== 'NON_NULL';
28};
29
30export const isListNullable = (
31 schema: SchemaIntrospector,
32 typename: string,
33 fieldName: string,
34 logger: Logger | undefined
35): boolean => {
36 const field = getField(schema, typename, fieldName, logger);
37 if (!field) return false;
38 const ofType =
39 field.type.kind === 'NON_NULL' ? field.type.ofType : field.type;
40 return ofType.kind === 'LIST' && ofType.ofType.kind !== 'NON_NULL';
41};
42
43export const isFieldAvailableOnType = (
44 schema: SchemaIntrospector,
45 typename: string,
46 fieldName: string,
47 logger: Logger | undefined
48): boolean =>
49 fieldName.indexOf(BUILTIN_NAME) === 0 ||
50 typename.indexOf(BUILTIN_NAME) === 0 ||
51 !!getField(schema, typename, fieldName, logger);
52
53export const isInterfaceOfType = (
54 schema: SchemaIntrospector,
55 node: InlineFragmentNode | FragmentDefinitionNode,
56 typename: string | void
57): boolean => {
58 if (!typename) return false;
59 const typeCondition = getTypeCondition(node);
60 if (!typeCondition || typename === typeCondition) {
61 return true;
62 } else if (
63 schema.types!.has(typeCondition) &&
64 schema.types!.get(typeCondition)!.kind === 'OBJECT'
65 ) {
66 return typeCondition === typename;
67 }
68
69 expectAbstractType(schema, typeCondition!);
70 expectObjectType(schema, typename!);
71 return schema.isSubType(typeCondition, typename);
72};
73
74const getField = (
75 schema: SchemaIntrospector,
76 typename: string,
77 fieldName: string,
78 logger: Logger | undefined
79) => {
80 if (
81 fieldName.indexOf(BUILTIN_NAME) === 0 ||
82 typename.indexOf(BUILTIN_NAME) === 0
83 )
84 return;
85
86 expectObjectType(schema, typename);
87 const object = schema.types!.get(typename) as SchemaObject;
88 const field = object.fields()[fieldName];
89 if (!field) {
90 warn(
91 'Invalid field: The field `' +
92 fieldName +
93 '` does not exist on `' +
94 typename +
95 '`, ' +
96 'but the GraphQL document expects it to exist.\n' +
97 'Traversal will continue, however this may lead to undefined behavior!',
98 4,
99 logger
100 );
101 }
102
103 return field;
104};
105
106function expectObjectType(schema: SchemaIntrospector, typename: string) {
107 invariant(
108 schema.types!.has(typename) &&
109 schema.types!.get(typename)!.kind === 'OBJECT',
110 'Invalid Object type: The type `' +
111 typename +
112 '` is not an object in the defined schema, ' +
113 'but the GraphQL document is traversing it.',
114 3
115 );
116}
117
118function expectAbstractType(schema: SchemaIntrospector, typename: string) {
119 invariant(
120 schema.types!.has(typename) &&
121 (schema.types!.get(typename)!.kind === 'INTERFACE' ||
122 schema.types!.get(typename)!.kind === 'UNION'),
123 'Invalid Abstract type: The type `' +
124 typename +
125 '` is not an Interface or Union type in the defined schema, ' +
126 'but a fragment in the GraphQL document is using it as a type condition.',
127 5
128 );
129}
130
131export function expectValidKeyingConfig(
132 schema: SchemaIntrospector,
133 keys: KeyingConfig,
134 logger: Logger | undefined
135): void {
136 if (process.env.NODE_ENV !== 'production') {
137 for (const key in keys) {
138 if (!schema.types!.has(key)) {
139 warn(
140 'Invalid Object type: The type `' +
141 key +
142 '` is not an object in the defined schema, but the `keys` option is referencing it.',
143 20,
144 logger
145 );
146 }
147 }
148 }
149}
150
151export function expectValidUpdatesConfig(
152 schema: SchemaIntrospector,
153 updates: UpdatesConfig,
154 logger: Logger | undefined
155): void {
156 if (process.env.NODE_ENV === 'production') {
157 return;
158 }
159
160 for (const typename in updates) {
161 if (!updates[typename]) {
162 continue;
163 } else if (!schema.types!.has(typename)) {
164 let addition = '';
165
166 if (
167 typename === 'Mutation' &&
168 schema.mutation &&
169 schema.mutation !== 'Mutation'
170 ) {
171 addition +=
172 '\nMaybe your config should reference `' + schema.mutation + '`?';
173 } else if (
174 typename === 'Subscription' &&
175 schema.subscription &&
176 schema.subscription !== 'Subscription'
177 ) {
178 addition +=
179 '\nMaybe your config should reference `' + schema.subscription + '`?';
180 }
181
182 return warn(
183 'Invalid updates type: The type `' +
184 typename +
185 '` is not an object in the defined schema, but the `updates` config is referencing it.' +
186 addition,
187 21,
188 logger
189 );
190 }
191
192 const fields = (schema.types!.get(typename)! as SchemaObject).fields();
193 for (const fieldName in updates[typename]!) {
194 if (!fields[fieldName]) {
195 warn(
196 'Invalid updates field: `' +
197 fieldName +
198 '` on `' +
199 typename +
200 '` is not in the defined schema, but the `updates` config is referencing it.',
201 22,
202 logger
203 );
204 }
205 }
206 }
207}
208
209function warnAboutResolver(name: string, logger: Logger | undefined): void {
210 warn(
211 `Invalid resolver: \`${name}\` is not in the defined schema, but the \`resolvers\` option is referencing it.`,
212 23,
213 logger
214 );
215}
216
217function warnAboutAbstractResolver(
218 name: string,
219 kind: 'UNION' | 'INTERFACE',
220 logger: Logger | undefined
221): void {
222 warn(
223 `Invalid resolver: \`${name}\` does not match to a concrete type in the schema, but the \`resolvers\` option is referencing it. Implement the resolver for the types that ${
224 kind === 'UNION' ? 'make up the union' : 'implement the interface'
225 } instead.`,
226 26,
227 logger
228 );
229}
230
231export function expectValidResolversConfig(
232 schema: SchemaIntrospector,
233 resolvers: ResolverConfig,
234 logger: Logger | undefined
235): void {
236 if (process.env.NODE_ENV === 'production') {
237 return;
238 }
239
240 for (const key in resolvers) {
241 if (key === 'Query') {
242 if (schema.query) {
243 const validQueries = (
244 schema.types!.get(schema.query) as SchemaObject
245 ).fields();
246 for (const resolverQuery in resolvers.Query || {}) {
247 if (!validQueries[resolverQuery]) {
248 warnAboutResolver('Query.' + resolverQuery, logger);
249 }
250 }
251 } else {
252 warnAboutResolver('Query', logger);
253 }
254 } else {
255 if (!schema.types!.has(key)) {
256 warnAboutResolver(key, logger);
257 } else if (
258 schema.types!.get(key)!.kind === 'INTERFACE' ||
259 schema.types!.get(key)!.kind === 'UNION'
260 ) {
261 warnAboutAbstractResolver(
262 key,
263 schema.types!.get(key)!.kind as 'INTERFACE' | 'UNION',
264 logger
265 );
266 } else {
267 const validTypeProperties = (
268 schema.types!.get(key) as SchemaObject
269 ).fields();
270 for (const resolverProperty in resolvers[key] || {}) {
271 if (!validTypeProperties[resolverProperty]) {
272 warnAboutResolver(key + '.' + resolverProperty, logger);
273 }
274 }
275 }
276 }
277 }
278}
279
280export function expectValidOptimisticMutationsConfig(
281 schema: SchemaIntrospector,
282 optimisticMutations: OptimisticMutationConfig,
283 logger: Logger | undefined
284): void {
285 if (process.env.NODE_ENV === 'production') {
286 return;
287 }
288
289 if (schema.mutation) {
290 const validMutations = (
291 schema.types!.get(schema.mutation) as SchemaObject
292 ).fields();
293 for (const mutation in optimisticMutations) {
294 if (!validMutations[mutation]) {
295 warn(
296 `Invalid optimistic mutation field: \`${mutation}\` is not a mutation field in the defined schema, but the \`optimistic\` option is referencing it.`,
297 24,
298 logger
299 );
300 }
301 }
302 }
303}