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}