Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 8.0 kB view raw
1import type { TypedDocumentNode } from '@urql/core'; 2import { formatDocument, createRequest } from '@urql/core'; 3 4import type { 5 Cache, 6 FieldInfo, 7 ResolverConfig, 8 DataField, 9 Variables, 10 FieldArgs, 11 Link, 12 Data, 13 QueryInput, 14 UpdatesConfig, 15 OptimisticMutationConfig, 16 KeyingConfig, 17 Entity, 18 CacheExchangeOpts, 19 DirectivesConfig, 20 Logger, 21} from '../types'; 22 23import { invariant } from '../helpers/help'; 24import { contextRef, ensureLink } from '../operations/shared'; 25import { _query, _queryFragment } from '../operations/query'; 26import { _write, _writeFragment } from '../operations/write'; 27import { invalidateEntity, invalidateType } from '../operations/invalidate'; 28import { keyOfField } from './keys'; 29import * as InMemoryData from './data'; 30 31import type { SchemaIntrospector } from '../ast'; 32import { 33 buildClientSchema, 34 expectValidKeyingConfig, 35 expectValidUpdatesConfig, 36 expectValidResolversConfig, 37 expectValidOptimisticMutationsConfig, 38} from '../ast'; 39 40type DocumentNode = TypedDocumentNode<any, any>; 41type RootField = 'query' | 'mutation' | 'subscription'; 42 43/** Implementation of the {@link Cache} interface as created internally by the {@link cacheExchange}. 44 * @internal 45 */ 46export class Store< 47 C extends Partial<CacheExchangeOpts> = Partial<CacheExchangeOpts>, 48> implements Cache 49{ 50 data: InMemoryData.InMemoryData; 51 52 logger?: Logger; 53 directives: DirectivesConfig; 54 resolvers: ResolverConfig; 55 updates: UpdatesConfig; 56 optimisticMutations: OptimisticMutationConfig; 57 keys: KeyingConfig; 58 globalIDs: Set<string> | boolean; 59 schema?: SchemaIntrospector; 60 possibleTypeMap?: Map<string, Set<string>>; 61 62 rootFields: { query: string; mutation: string; subscription: string }; 63 rootNames: { [name: string]: RootField | void }; 64 65 constructor(opts?: C) { 66 if (!opts) opts = {} as C; 67 68 this.logger = opts.logger; 69 this.resolvers = opts.resolvers || {}; 70 this.directives = opts.directives || {}; 71 this.optimisticMutations = opts.optimistic || {}; 72 this.keys = opts.keys || {}; 73 74 this.globalIDs = Array.isArray(opts.globalIDs) 75 ? new Set(opts.globalIDs) 76 : !!opts.globalIDs; 77 78 let queryName = 'Query'; 79 let mutationName = 'Mutation'; 80 let subscriptionName = 'Subscription'; 81 if (opts.schema) { 82 const schema = buildClientSchema(opts.schema); 83 queryName = schema.query || queryName; 84 mutationName = schema.mutation || mutationName; 85 subscriptionName = schema.subscription || subscriptionName; 86 // Only add schema introspector if it has types info 87 if (schema.types) this.schema = schema; 88 } 89 90 if (!this.schema && opts.possibleTypes) { 91 this.possibleTypeMap = new Map(); 92 for (const entry of Object.entries(opts.possibleTypes)) { 93 const [abstractType, concreteTypes] = entry; 94 this.possibleTypeMap.set(abstractType, new Set(concreteTypes)); 95 } 96 } 97 98 this.updates = opts.updates || {}; 99 100 this.rootFields = { 101 query: queryName, 102 mutation: mutationName, 103 subscription: subscriptionName, 104 }; 105 106 this.rootNames = { 107 [queryName]: 'query', 108 [mutationName]: 'mutation', 109 [subscriptionName]: 'subscription', 110 }; 111 112 this.data = InMemoryData.make(queryName); 113 114 if (this.schema && process.env.NODE_ENV !== 'production') { 115 expectValidKeyingConfig(this.schema, this.keys, this.logger); 116 expectValidUpdatesConfig(this.schema, this.updates, this.logger); 117 expectValidResolversConfig(this.schema, this.resolvers, this.logger); 118 expectValidOptimisticMutationsConfig( 119 this.schema, 120 this.optimisticMutations, 121 this.logger 122 ); 123 } 124 } 125 126 keyOfField(fieldName: string, fieldArgs?: FieldArgs) { 127 return keyOfField(fieldName, fieldArgs); 128 } 129 130 keyOfEntity(data: Entity) { 131 // In resolvers and updaters we may have a specific parent 132 // object available that can be used to skip to a specific parent 133 // key directly without looking at its incomplete properties 134 if (contextRef && data === contextRef.parent) { 135 return contextRef.parentKey; 136 } else if (data == null || typeof data === 'string') { 137 return data || null; 138 } else if (!data.__typename) { 139 return null; 140 } else if (this.rootNames[data.__typename]) { 141 return data.__typename; 142 } 143 144 let key: string | null = null; 145 if (this.keys[data.__typename]) { 146 key = this.keys[data.__typename](data) || null; 147 } else if (data.id != null) { 148 key = `${data.id}`; 149 } else if (data._id != null) { 150 key = `${data._id}`; 151 } 152 153 const typename = data.__typename; 154 const globalID = 155 this.globalIDs === true || 156 (this.globalIDs && this.globalIDs.has(typename)); 157 return globalID || !key ? key : `${typename}:${key}`; 158 } 159 160 resolve( 161 entity: Entity, 162 field: string, 163 args?: FieldArgs 164 ): DataField | undefined { 165 const entityKey = this.keyOfEntity(entity); 166 if (entityKey) { 167 const fieldKey = keyOfField(field, args); 168 const fieldValue = InMemoryData.readRecord(entityKey, fieldKey); 169 if (fieldValue !== undefined) return fieldValue; 170 let fieldLink = InMemoryData.readLink(entityKey, fieldKey); 171 if (fieldLink !== undefined) fieldLink = ensureLink(this, fieldLink); 172 return fieldLink; 173 } 174 } 175 176 invalidate(entity: Entity, field?: string, args?: FieldArgs) { 177 const entityKey = this.keyOfEntity(entity); 178 const shouldInvalidateType = 179 entity && 180 typeof entity === 'string' && 181 !field && 182 !args && 183 !this.resolve(entity, '__typename'); 184 185 if (shouldInvalidateType) { 186 invalidateType(entity, []); 187 } else { 188 invariant( 189 entityKey, 190 "Can't generate a key for invalidate(...).\n" + 191 'You have to pass an id or _id field or create a custom `keys` field for `' + 192 (typeof entity === 'object' 193 ? (entity as Data).__typename 194 : entity + '`.'), 195 19 196 ); 197 198 invalidateEntity(entityKey, field, args); 199 } 200 } 201 202 inspectFields(entity: Entity): FieldInfo[] { 203 const entityKey = this.keyOfEntity(entity); 204 return entityKey ? InMemoryData.inspectFields(entityKey) : []; 205 } 206 207 updateQuery<T = Data, V = Variables>( 208 input: QueryInput<T, V>, 209 updater: (data: T | null) => T | null 210 ): void { 211 const request = createRequest(input.query, input.variables!); 212 const output = updater(this.readQuery(request)); 213 if (output !== null) { 214 _write(this, request, output as any, undefined); 215 } 216 } 217 218 readQuery<T = Data, V = Variables>(input: QueryInput<T, V>): T | null { 219 const request = createRequest(input.query, input.variables!); 220 return _query(this, request, undefined, undefined).data as T | null; 221 } 222 223 readFragment<T = Data, V = Variables>( 224 fragment: DocumentNode | TypedDocumentNode<T, V>, 225 entity: string | Data | T, 226 variables?: V, 227 fragmentName?: string 228 ): T | null { 229 return _queryFragment( 230 this, 231 formatDocument(fragment), 232 entity as Data, 233 variables as any, 234 fragmentName 235 ) as T | null; 236 } 237 238 writeFragment<T = Data, V = Variables>( 239 fragment: DocumentNode | TypedDocumentNode<T, V>, 240 data: T, 241 variables?: V, 242 fragmentName?: string 243 ): void { 244 _writeFragment( 245 this, 246 formatDocument(fragment), 247 data as Data, 248 variables as any, 249 fragmentName 250 ); 251 } 252 253 link( 254 entity: Entity, 255 field: string, 256 args: FieldArgs, 257 link: Link<Entity> 258 ): void; 259 260 link(entity: Entity, field: string, link: Link<Entity>): void; 261 262 link( 263 entity: Entity, 264 field: string, 265 ...rest: [FieldArgs, Link<Entity>] | [Link<Entity>] 266 ): void { 267 const args = rest.length === 2 ? rest[0] : null; 268 const link = rest.length === 2 ? rest[1] : rest[0]; 269 const entityKey = this.keyOfEntity(entity); 270 if (entityKey) { 271 InMemoryData.writeLink( 272 entityKey, 273 keyOfField(field, args), 274 ensureLink(this, link) 275 ); 276 } 277 } 278}