Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 9.8 kB view raw
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};