1import type { CombinedError, ErrorLike, FormattedNode } from '@urql/core';
2
3import type {
4 InlineFragmentNode,
5 FragmentDefinitionNode,
6 FieldNode,
7} from '@0no-co/graphql.web';
8import { Kind } from '@0no-co/graphql.web';
9
10import type { SelectionSet } from '../ast';
11import {
12 isDeferred,
13 getTypeCondition,
14 getSelectionSet,
15 getName,
16 isOptional,
17} from '../ast';
18
19import { warn, pushDebugNode, popDebugNode } from '../helpers/help';
20import {
21 hasField,
22 currentOperation,
23 currentOptimistic,
24 writeConcreteType,
25 getConcreteTypes,
26 isSeenConcreteType,
27} from '../store/data';
28import { keyOfField } from '../store/keys';
29import type { Store } from '../store/store';
30
31import { getFieldArguments, shouldInclude, isInterfaceOfType } from '../ast';
32
33import type {
34 Fragments,
35 Variables,
36 DataField,
37 NullArray,
38 Link,
39 Entity,
40 Data,
41 Logger,
42} from '../types';
43
44export interface Context {
45 store: Store;
46 variables: Variables;
47 fragments: Fragments;
48 parentTypeName: string;
49 parentKey: string;
50 parentFieldKey: string;
51 parent: Data;
52 fieldName: string;
53 error: ErrorLike | undefined;
54 partial: boolean;
55 hasNext: boolean;
56 optimistic: boolean;
57 __internal: {
58 path: Array<string | number>;
59 errorMap: { [path: string]: ErrorLike } | undefined;
60 };
61}
62
63export let contextRef: Context | null = null;
64export let deferRef = false;
65export let optionalRef: boolean | undefined = undefined;
66
67// Checks whether the current data field is a cache miss because of a GraphQLError
68export const getFieldError = (ctx: Context): ErrorLike | undefined =>
69 ctx.__internal.path.length > 0 && ctx.__internal.errorMap
70 ? ctx.__internal.errorMap[ctx.__internal.path.join('.')]
71 : undefined;
72
73export const makeContext = (
74 store: Store,
75 variables: Variables,
76 fragments: Fragments,
77 typename: string,
78 entityKey: string,
79 error: CombinedError | undefined
80): Context => {
81 const ctx: Context = {
82 store,
83 variables,
84 fragments,
85 parent: { __typename: typename },
86 parentTypeName: typename,
87 parentKey: entityKey,
88 parentFieldKey: '',
89 fieldName: '',
90 error: undefined,
91 partial: false,
92 hasNext: false,
93 optimistic: currentOptimistic,
94 __internal: {
95 path: [],
96 errorMap: undefined,
97 },
98 };
99
100 if (error && error.graphQLErrors) {
101 for (let i = 0; i < error.graphQLErrors.length; i++) {
102 const graphQLError = error.graphQLErrors[i];
103 if (graphQLError.path && graphQLError.path.length) {
104 if (!ctx.__internal.errorMap)
105 ctx.__internal.errorMap = Object.create(null);
106 ctx.__internal.errorMap![graphQLError.path.join('.')] = graphQLError;
107 }
108 }
109 }
110
111 return ctx;
112};
113
114export const updateContext = (
115 ctx: Context,
116 data: Data,
117 typename: string,
118 entityKey: string,
119 fieldKey: string,
120 fieldName: string
121) => {
122 contextRef = ctx;
123 ctx.parent = data;
124 ctx.parentTypeName = typename;
125 ctx.parentKey = entityKey;
126 ctx.parentFieldKey = fieldKey;
127 ctx.fieldName = fieldName;
128 ctx.error = getFieldError(ctx);
129};
130
131const isFragmentHeuristicallyMatching = (
132 node: FormattedNode<InlineFragmentNode | FragmentDefinitionNode>,
133 typename: void | string,
134 entityKey: string,
135 vars: Variables,
136 logger?: Logger
137) => {
138 if (!typename) return false;
139 const typeCondition = getTypeCondition(node);
140 if (!typeCondition || typename === typeCondition) return true;
141
142 warn(
143 'Heuristic Fragment Matching: A fragment is trying to match against the `' +
144 typename +
145 '` type, ' +
146 'but the type condition is `' +
147 typeCondition +
148 '`. Since GraphQL allows for interfaces `' +
149 typeCondition +
150 '` may be an ' +
151 'interface.\nA schema needs to be defined for this match to be deterministic, ' +
152 'otherwise the fragment will be matched heuristically!',
153 16,
154 logger
155 );
156
157 return !getSelectionSet(node).some(node => {
158 if (node.kind !== Kind.FIELD) return false;
159 const fieldKey = keyOfField(getName(node), getFieldArguments(node, vars));
160 return !hasField(entityKey, fieldKey);
161 });
162};
163
164export class SelectionIterator {
165 typename: undefined | string;
166 entityKey: string;
167 ctx: Context;
168 stack: {
169 selectionSet: FormattedNode<SelectionSet>;
170 index: number;
171 defer: boolean;
172 optional: boolean | undefined;
173 }[];
174
175 // NOTE: Outside of this file, we expect `_defer` to always be reset to `false`
176 constructor(
177 typename: undefined | string,
178 entityKey: string,
179 _defer: false,
180 _optional: undefined,
181 selectionSet: FormattedNode<SelectionSet>,
182 ctx: Context
183 );
184 // NOTE: Inside this file we expect the state to be recursively passed on
185 constructor(
186 typename: undefined | string,
187 entityKey: string,
188 _defer: boolean,
189 _optional: undefined | boolean,
190 selectionSet: FormattedNode<SelectionSet>,
191 ctx: Context
192 );
193
194 constructor(
195 typename: undefined | string,
196 entityKey: string,
197 _defer: boolean,
198 _optional: boolean | undefined,
199 selectionSet: FormattedNode<SelectionSet>,
200 ctx: Context
201 ) {
202 this.typename = typename;
203 this.entityKey = entityKey;
204 this.ctx = ctx;
205 this.stack = [
206 {
207 selectionSet,
208 index: 0,
209 defer: _defer,
210 optional: _optional,
211 },
212 ];
213 }
214
215 next(): FormattedNode<FieldNode> | undefined {
216 while (this.stack.length > 0) {
217 let state = this.stack[this.stack.length - 1];
218 while (state.index < state.selectionSet.length) {
219 const select = state.selectionSet[state.index++];
220 if (!shouldInclude(select, this.ctx.variables)) {
221 /*noop*/
222 } else if (select.kind !== Kind.FIELD) {
223 // A fragment is either referred to by FragmentSpread or inline
224 const fragment =
225 select.kind !== Kind.INLINE_FRAGMENT
226 ? this.ctx.fragments[getName(select)]
227 : select;
228 if (fragment) {
229 const isMatching =
230 !fragment.typeCondition ||
231 (this.ctx.store.schema
232 ? isInterfaceOfType(
233 this.ctx.store.schema,
234 fragment,
235 this.typename
236 )
237 : this.ctx.store.possibleTypeMap
238 ? isSuperType(
239 this.ctx.store.possibleTypeMap,
240 fragment.typeCondition.name.value,
241 this.typename
242 )
243 : (currentOperation === 'read' &&
244 isFragmentMatching(
245 fragment.typeCondition.name.value,
246 this.typename
247 )) ||
248 isFragmentHeuristicallyMatching(
249 fragment,
250 this.typename,
251 this.entityKey,
252 this.ctx.variables,
253 this.ctx.store.logger
254 ));
255 if (
256 isMatching ||
257 (currentOperation === 'write' && !this.ctx.store.schema)
258 ) {
259 if (process.env.NODE_ENV !== 'production')
260 pushDebugNode(this.typename, fragment);
261 const isFragmentOptional = isOptional(select);
262 if (
263 isMatching &&
264 fragment.typeCondition &&
265 this.typename !== fragment.typeCondition.name.value
266 ) {
267 writeConcreteType(
268 fragment.typeCondition.name.value,
269 this.typename!
270 );
271 }
272
273 this.stack.push(
274 (state = {
275 selectionSet: getSelectionSet(fragment),
276 index: 0,
277 defer: state.defer || isDeferred(select, this.ctx.variables),
278 optional:
279 isFragmentOptional !== undefined
280 ? isFragmentOptional
281 : state.optional,
282 })
283 );
284 }
285 }
286 } else if (currentOperation === 'write' || !select._generated) {
287 deferRef = state.defer;
288 optionalRef = state.optional;
289 return select;
290 }
291 }
292 this.stack.pop();
293 if (process.env.NODE_ENV !== 'production') popDebugNode();
294 }
295 return undefined;
296 }
297}
298
299const isSuperType = (
300 possibleTypeMap: Map<string, Set<string>>,
301 typeCondition: string,
302 typename: string | void
303) => {
304 if (!typename) return false;
305 if (typeCondition === typename) return true;
306
307 const concreteTypes = possibleTypeMap.get(typeCondition);
308
309 return concreteTypes && concreteTypes.has(typename);
310};
311
312const isFragmentMatching = (typeCondition: string, typename: string | void) => {
313 if (!typename) return false;
314 if (typeCondition === typename) return true;
315
316 const isProbableAbstractType = !isSeenConcreteType(typeCondition);
317 if (!isProbableAbstractType) return false;
318
319 const types = getConcreteTypes(typeCondition);
320 return types.size && types.has(typename);
321};
322
323export const ensureData = (x: DataField): Data | NullArray<Data> | null =>
324 x == null ? null : (x as Data | NullArray<Data>);
325
326export const ensureLink = (store: Store, ref: Link<Entity>): Link => {
327 if (!ref) {
328 return ref || null;
329 } else if (Array.isArray(ref)) {
330 const link = new Array(ref.length);
331 for (let i = 0, l = link.length; i < l; i++)
332 link[i] = ensureLink(store, ref[i]);
333 return link;
334 }
335
336 const link = store.keyOfEntity(ref);
337 if (!link && ref && typeof ref === 'object') {
338 warn(
339 "Can't generate a key for link(...) item." +
340 '\nYou have to pass an `id` or `_id` field or create a custom `keys` config for `' +
341 ref.__typename +
342 '`.',
343 12,
344 store.logger
345 );
346 }
347
348 return link;
349};