···11+---
22+'@urql/exchange-graphcache': minor
33+---
44+55+Allow `updates` config to react to arbitrary type updates other than just `Mutation` and `Subscription` fields.
66+You’ll now be able to write updaters that react to any entity field being written to the cache,
77+which allows for more granular invalidations. **Note:** If you’ve previously used `updates.Mutation`
88+and `updated.Subscription` with a custom schema with custom root names, you‘ll get a warning since
99+you’ll have to update your `updates` config to reflect this. This was a prior implementation
1010+mistake!
+12-10
docs/graphcache/errors.md
···308308Check whether your schema is up-to-date, or whether you're using an invalid
309309typename in `opts.keys`, maybe due to a typo.
310310311311-## (21) Invalid mutation
311311+## (21) Invalid updates type
312312313313-> Invalid mutation field `???` is not in the defined schema, but the `updates` option is referencing it.
313313+> Invalid updates field: The type `???` is not an object in the defined schema,
314314+> but the `updates` config is referencing it.
314315315316When you're passing an introspected schema to the cache exchange, it is
316316-able to check whether your `opts.updates.Mutation` is valid.
317317-This error occurs when an unknown mutation field is found in `opts.updates.Mutation`.
317317+able to check whether your `opts.updates` config is valid.
318318+This error occurs when an unknown type is found in the `opts.updates` config.
318319319319-Check whether your schema is up-to-date, or whether you've got a typo in `opts.updates.Mutation`.
320320+Check whether your schema is up-to-date, or whether you've got a typo in `opts.updates`.
320321321321-## (22) Invalid subscription
322322+## (22) Invalid updates field
322323323323-> Invalid subscription field: `???` is not in the defined schema, but the `updates` option is referencing it.
324324+> Invalid updates field: `???` on `???` is not in the defined schema,
325325+> but the `updates` config is referencing it.
324326325327When you're passing an introspected schema to the cache exchange, it is
326326-able to check whether your `opts.updates.Subscription` is valid.
327327-This error occurs when an unknown subscription field is found in `opts.updates.Subscription`.
328328+able to check whether your `opts.updates` config is valid.
329329+This error occurs when an unknown field is found in `opts.updates[typename]`.
328330329331Check whether your schema is up-to-date, or whether you're using an invalid
330330-subscription name in `opts.updates.Subscription`, maybe due to a typo.
332332+field name in `opts.updates`, maybe due to a typo.
331333332334## (23) Invalid resolver
333335
+39-27
exchanges/graphcache/src/ast/schemaPredicates.ts
···6677import {
88 KeyingConfig,
99- UpdateResolver,
99+ UpdatesConfig,
1010 ResolverConfig,
1111 OptimisticMutationConfig,
1212} from '../types';
···139139140140export function expectValidUpdatesConfig(
141141 schema: SchemaIntrospector,
142142- updates: Record<string, Record<string, UpdateResolver | undefined>>
142142+ updates: UpdatesConfig
143143): void {
144144 if (process.env.NODE_ENV === 'production') {
145145 return;
146146 }
147147148148- if (schema.mutation) {
149149- const mutationFields = (schema.types!.get(
150150- schema.mutation
151151- ) as SchemaObject).fields();
152152- const givenMutations = updates[schema.mutation] || {};
153153- for (const fieldName in givenMutations) {
154154- if (mutationFields[fieldName] === undefined) {
155155- warn(
156156- 'Invalid mutation field: `' +
157157- fieldName +
158158- '` is not in the defined schema, but the `updates.Mutation` option is referencing it.',
159159- 21
160160- );
148148+ for (const typename in updates) {
149149+ if (!updates[typename]) {
150150+ continue;
151151+ } else if (!schema.types!.has(typename)) {
152152+ let addition = '';
153153+154154+ if (
155155+ typename === 'Mutation' &&
156156+ schema.mutation &&
157157+ schema.mutation !== 'Mutation'
158158+ ) {
159159+ addition +=
160160+ '\nMaybe your config should reference `' + schema.mutation + '`?';
161161+ } else if (
162162+ typename === 'Subscription' &&
163163+ schema.subscription &&
164164+ schema.subscription !== 'Subscription'
165165+ ) {
166166+ addition +=
167167+ '\nMaybe your config should reference `' + schema.subscription + '`?';
161168 }
169169+170170+ return warn(
171171+ 'Invalid updates type: The type `' +
172172+ typename +
173173+ '` is not an object in the defined schema, but the `updates` config is referencing it.' +
174174+ addition,
175175+ 21
176176+ );
162177 }
163163- }
164178165165- if (schema.subscription) {
166166- const subscriptionFields = (schema.types!.get(
167167- schema.subscription
168168- ) as SchemaObject).fields();
169169- const givenSubscription = updates[schema.subscription] || {};
170170- for (const fieldName in givenSubscription) {
171171- if (subscriptionFields[fieldName] === undefined) {
179179+ const fields = (schema.types!.get(typename)! as SchemaObject).fields();
180180+ for (const fieldName in updates[typename]!) {
181181+ if (!fields[fieldName]) {
172182 warn(
173173- 'Invalid subscription field: `' +
183183+ 'Invalid updates field: `' +
174184 fieldName +
175175- '` is not in the defined schema, but the `updates.Subscription` option is referencing it.',
185185+ '` on `' +
186186+ typename +
187187+ '` is not in the defined schema, but the `updates` config is referencing it.',
176188 22
177189 );
178190 }
···213225 const validQueries = (schema.types!.get(
214226 schema.query
215227 ) as SchemaObject).fields();
216216- for (const resolverQuery in resolvers.Query) {
228228+ for (const resolverQuery in resolvers.Query || {}) {
217229 if (!validQueries[resolverQuery]) {
218230 warnAboutResolver('Query.' + resolverQuery);
219231 }
···236248 const validTypeProperties = (schema.types!.get(
237249 key
238250 ) as SchemaObject).fields();
239239- for (const resolverProperty in resolvers[key]) {
251251+ for (const resolverProperty in resolvers[key] || {}) {
240252 if (!validTypeProperties[resolverProperty]) {
241253 warnAboutResolver(key + '.' + resolverProperty);
242254 }
+4-4
exchanges/graphcache/src/operations/query.ts
···284284 result?: Data
285285): Data | undefined => {
286286 const { store } = ctx;
287287- const isQuery = key === store.rootFields['query'];
287287+ const isQuery = key === store.rootFields.query;
288288289289 const entityKey = (result && store.keyOfEntity(result)) || key;
290290 if (!isQuery && !!ctx.store.rootNames[entityKey]) {
···321321 return;
322322 }
323323324324+ const resolvers = store.resolvers[typename];
324325 const iterate = makeSelectionIterator(typename, entityKey, select, ctx);
325326326327 let hasFields = false;
···337338 const key = joinKeys(entityKey, fieldKey);
338339 const fieldValue = InMemoryData.readRecord(entityKey, fieldKey);
339340 const resultValue = result ? result[fieldName] : undefined;
340340- const resolvers = store.resolvers[typename];
341341342342 if (process.env.NODE_ENV !== 'production' && store.schema && typename) {
343343 isFieldAvailableOnType(store.schema, typename, fieldName);
···358358 } else if (
359359 getCurrentOperation() === 'read' &&
360360 resolvers &&
361361- typeof resolvers[fieldName] === 'function'
361361+ resolvers[fieldName]
362362 ) {
363363 // We have to update the information in context to reflect the info
364364 // that the resolver will receive
···370370 output[fieldAlias] = fieldValue;
371371 }
372372373373- dataFieldValue = resolvers[fieldName](
373373+ dataFieldValue = resolvers[fieldName]!(
374374 output,
375375 fieldArgs || ({} as Variables),
376376 store,
+30-26
exchanges/graphcache/src/operations/write.ts
···198198 select: SelectionSet,
199199 data: Data
200200) => {
201201- const isQuery = entityKey === ctx.store.rootFields['query'];
202202- const isRoot = !isQuery && !!ctx.store.rootNames[entityKey!];
203203- const typename = isRoot || isQuery ? entityKey : data.__typename;
201201+ // These fields determine how we write. The `Query` root type is written
202202+ // like a normal entity, hence, we use `rootField` with a default to determine
203203+ // this. All other root names (Subscription & Mutation) are in a different
204204+ // write mode
205205+ const rootField = ctx.store.rootNames[entityKey!] || 'query';
206206+ const isRoot = !!ctx.store.rootNames[entityKey!];
207207+208208+ const typename = isRoot ? entityKey : data.__typename;
204209 if (!typename) {
205210 warn(
206211 "Couldn't find __typename when writing.\n" +
···208213 14
209214 );
210215 return;
211211- } else if (!isRoot && !isQuery && entityKey) {
216216+ } else if (!isRoot && entityKey) {
212217 InMemoryData.writeRecord(entityKey, '__typename', typename);
213218 }
214219220220+ const updates = ctx.store.updates[typename];
215221 const iterate = makeSelectionIterator(
216222 typename,
217223 entityKey || typename,
···230236 // Development check of undefined fields
231237 if (process.env.NODE_ENV !== 'production') {
232238 if (
233233- !isRoot &&
239239+ rootField === 'query' &&
234240 fieldValue === undefined &&
235241 !deferRef.current &&
236242 !ctx.optimistic
···261267 // Fields marked as deferred that aren't defined must be skipped
262268 // Otherwise, we also ignore undefined values in optimistic updaters
263269 (fieldValue === undefined &&
264264- (deferRef.current || (ctx.optimistic && !isRoot)))
270270+ (deferRef.current || (ctx.optimistic && rootField === 'query')))
265271 ) {
266272 continue;
267273 }
···272278 // Execute optimistic mutation functions on root fields, or execute recursive functions
273279 // that have been returned on optimistic objects
274280 let resolver: OptimisticMutationResolver | void;
275275- if (ctx.optimistic && isRoot) {
281281+ if (ctx.optimistic && rootField === 'mutation') {
276282 resolver = ctx.store.optimisticMutations[fieldName];
277283 if (!resolver) continue;
278284 } else if (ctx.optimistic && typeof fieldValue === 'function') {
···288294289295 if (node.selectionSet) {
290296 // Process the field and write links for the child entities that have been written
291291- if (entityKey && !isRoot) {
297297+ if (entityKey && rootField === 'query') {
292298 const key = joinKeys(entityKey, fieldKey);
293299 const link = writeField(
294300 ctx,
···300306 } else {
301307 writeField(ctx, getSelectionSet(node), ensureData(fieldValue));
302308 }
303303- } else if (entityKey && !isRoot) {
309309+ } else if (entityKey && rootField === 'query') {
304310 // This is a leaf node, so we're setting the field's value directly
305311 InMemoryData.writeRecord(
306312 entityKey || typename,
···311317 );
312318 }
313319314314- if (isRoot) {
315315- // We run side-effect updates after the default, normalized updates
316316- // so that the data is already available in-store if necessary
317317- const updater = ctx.store.updates[typename][fieldName];
318318- if (updater) {
319319- // We have to update the context to reflect up-to-date ResolveInfo
320320- updateContext(
321321- ctx,
322322- data,
323323- typename,
324324- typename,
325325- joinKeys(typename, fieldKey),
326326- fieldName
327327- );
320320+ // We run side-effect updates after the default, normalized updates
321321+ // so that the data is already available in-store if necessary
322322+ const updater = updates && updates[fieldName];
323323+ if (updater) {
324324+ // We have to update the context to reflect up-to-date ResolveInfo
325325+ updateContext(
326326+ ctx,
327327+ data,
328328+ typename,
329329+ typename,
330330+ joinKeys(typename, fieldKey),
331331+ fieldName
332332+ );
328333329329- data[fieldName] = fieldValue;
330330- updater(data, fieldArgs || {}, ctx.store, ctx);
331331- }
334334+ data[fieldName] = fieldValue;
335335+ updater(data, fieldArgs || {}, ctx.store, ctx);
332336 }
333337334338 // After processing the field, remove the current alias from the path again
+6-14
exchanges/graphcache/src/store/store.test.ts
···123123 expect(store.updates.Subscription).toBe(updatesOption.Subscription);
124124 });
125125126126- it("sets the store's updates field to an empty default if not provided", () => {
127127- const store = new Store({});
128128-129129- expect(store.updates.Mutation).toEqual({});
130130- expect(store.updates.Subscription).toEqual({});
131131- });
132132-133126 it('should not warn if Mutation/Subscription operations do exist in the schema', function () {
134127 new Store({
135128 schema: minifyIntrospectionQuery(
···163156 expect(console.warn).toBeCalledTimes(1);
164157 const warnMessage = mocked(console.warn).mock.calls[0][0];
165158 expect(warnMessage).toContain(
166166- 'Invalid mutation field: `doTheChaChaSlide` is not in the defined schema, but the `updates.Mutation` option is referencing it.'
159159+ 'Invalid updates field: `doTheChaChaSlide` on `Mutation` is not in the defined schema'
167160 );
168168- expect(warnMessage).toContain('https://bit.ly/2XbVrpR#21');
161161+ expect(warnMessage).toContain('https://bit.ly/2XbVrpR#22');
169162 });
170163171164 it("should warn if Subscription operations don't exist in the schema", function () {
···183176 expect(console.warn).toBeCalledTimes(1);
184177 const warnMessage = mocked(console.warn).mock.calls[0][0];
185178 expect(warnMessage).toContain(
186186- 'Invalid subscription field: `someoneDidTheChaChaSlide` is not in the defined schema, but the `updates.Subscription` option is referencing it.'
179179+ 'Invalid updates field: `someoneDidTheChaChaSlide` on `Subscription` is not in the defined schema'
187180 );
188181 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#22');
189182 });
···1006999 expect(warnMessage).toContain('https://bit.ly/2XbVrpR#24');
10071000 });
1008100110091009- it('should use different rootConfigs', function () {
10021002+ it('should use different rootConfigs', () => {
10101003 const fakeUpdater = vi.fn();
1011100410121005 const store = new Store({
···10241017 },
10251018 },
10261019 updates: {
10271027- Mutation: {
10201020+ mutation_root: {
10281021 toggleTodo: fakeUpdater,
10291022 },
10301023 },
10311024 });
1032102510331026 const mutationData = {
10341034- __typename: 'mutation_root',
10351027 toggleTodo: {
10361028 __typename: 'Todo',
10371029 id: 1,
···10491041 }
10501042 `,
10511043 },
10521052- mutationData
10441044+ mutationData as any
10531045 );
1054104610551047 expect(fakeUpdater).toBeCalledTimes(1);