@khanacademy/wonder-blocks-data 7.0.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/es/index.js +284 -100
  3. package/dist/index.js +1180 -800
  4. package/package.json +1 -1
  5. package/src/__docs__/_overview_ssr_.stories.mdx +13 -13
  6. package/src/__docs__/exports.abort-inflight-requests.stories.mdx +20 -0
  7. package/src/__docs__/exports.data.stories.mdx +3 -3
  8. package/src/__docs__/{exports.fulfill-all-data-requests.stories.mdx → exports.fetch-tracked-requests.stories.mdx} +5 -5
  9. package/src/__docs__/exports.get-gql-request-id.stories.mdx +24 -0
  10. package/src/__docs__/{exports.has-unfulfilled-requests.stories.mdx → exports.has-tracked-requests-to-be-fetched.stories.mdx} +4 -4
  11. package/src/__docs__/exports.intialize-hydration-cache.stories.mdx +29 -0
  12. package/src/__docs__/exports.purge-caches.stories.mdx +23 -0
  13. package/src/__docs__/{exports.remove-all-from-cache.stories.mdx → exports.purge-hydration-cache.stories.mdx} +4 -4
  14. package/src/__docs__/{exports.clear-shared-cache.stories.mdx → exports.purge-shared-cache.stories.mdx} +4 -4
  15. package/src/__docs__/exports.track-data.stories.mdx +4 -4
  16. package/src/__docs__/exports.use-cached-effect.stories.mdx +7 -4
  17. package/src/__docs__/exports.use-gql.stories.mdx +1 -33
  18. package/src/__docs__/exports.use-server-effect.stories.mdx +1 -1
  19. package/src/__docs__/exports.use-shared-cache.stories.mdx +2 -2
  20. package/src/__docs__/types.fetch-policy.stories.mdx +44 -0
  21. package/src/__docs__/types.response-cache.stories.mdx +1 -1
  22. package/src/__tests__/generated-snapshot.test.js +5 -5
  23. package/src/components/__tests__/data.test.js +2 -6
  24. package/src/hooks/__tests__/use-cached-effect.test.js +341 -100
  25. package/src/hooks/__tests__/use-hydratable-effect.test.js +15 -9
  26. package/src/hooks/__tests__/use-shared-cache.test.js +6 -6
  27. package/src/hooks/use-cached-effect.js +169 -93
  28. package/src/hooks/use-hydratable-effect.js +8 -1
  29. package/src/hooks/use-shared-cache.js +2 -2
  30. package/src/index.js +14 -78
  31. package/src/util/__tests__/get-gql-request-id.test.js +74 -0
  32. package/src/util/__tests__/graphql-document-node-parser.test.js +542 -0
  33. package/src/util/__tests__/hydration-cache-api.test.js +35 -0
  34. package/src/util/__tests__/purge-caches.test.js +29 -0
  35. package/src/util/__tests__/request-api.test.js +188 -0
  36. package/src/util/__tests__/request-fulfillment.test.js +42 -0
  37. package/src/util/__tests__/ssr-cache.test.js +10 -60
  38. package/src/util/__tests__/to-gql-operation.test.js +42 -0
  39. package/src/util/data-error.js +6 -0
  40. package/src/util/get-gql-request-id.js +50 -0
  41. package/src/util/graphql-document-node-parser.js +133 -0
  42. package/src/util/graphql-types.js +30 -0
  43. package/src/util/hydration-cache-api.js +28 -0
  44. package/src/util/purge-caches.js +15 -0
  45. package/src/util/request-api.js +66 -0
  46. package/src/util/request-fulfillment.js +32 -12
  47. package/src/util/request-tracking.js +1 -1
  48. package/src/util/ssr-cache.js +1 -21
  49. package/src/util/to-gql-operation.js +44 -0
  50. package/src/util/types.js +31 -0
  51. package/src/__docs__/exports.intialize-cache.stories.mdx +0 -29
  52. package/src/__docs__/exports.remove-from-cache.stories.mdx +0 -25
  53. package/src/__docs__/exports.request-fulfillment.stories.mdx +0 -36
@@ -0,0 +1,542 @@
1
+ // @flow
2
+ import {graphQLDocumentNodeParser} from "../graphql-document-node-parser.js";
3
+
4
+ describe("#graphQLDocumentNodeParser", () => {
5
+ describe("in production - shorter error messages", () => {
6
+ const NODE_ENV = process.env.NODE_ENV;
7
+ beforeEach(() => {
8
+ process.env.NODE_ENV = "production";
9
+ });
10
+
11
+ afterEach(() => {
12
+ if (NODE_ENV != null) {
13
+ process.env.NODE_ENV = NODE_ENV;
14
+ } else {
15
+ delete process.env.NODE_ENV;
16
+ }
17
+ });
18
+
19
+ it("should throw if the document lacks the kind property", () => {
20
+ // Arrange
21
+ const documentNode = ({}: any);
22
+
23
+ // Act
24
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
25
+
26
+ // Assert
27
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
28
+ `"Bad DocumentNode"`,
29
+ );
30
+ });
31
+
32
+ it("should throw if the document contains only fragments", () => {
33
+ // Arrange
34
+ const documentNode = {
35
+ kind: "Document",
36
+ definitions: [
37
+ {
38
+ kind: "FragmentDefinition",
39
+ name: {
40
+ kind: "Name",
41
+ value: "fragment",
42
+ },
43
+ typeCondition: {
44
+ kind: "NamedType",
45
+ name: {
46
+ kind: "Name",
47
+ value: "Query",
48
+ },
49
+ },
50
+ selectionSet: {
51
+ kind: "SelectionSet",
52
+ selections: [
53
+ {
54
+ kind: "Field",
55
+ name: {
56
+ kind: "Name",
57
+ value: "test",
58
+ },
59
+ },
60
+ ],
61
+ },
62
+ },
63
+ ],
64
+ };
65
+
66
+ // Act
67
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
68
+
69
+ // Assert
70
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
71
+ `"Fragment only"`,
72
+ );
73
+ });
74
+
75
+ it("should throw if the document contains subscriptions", () => {
76
+ // Arrange
77
+ const documentNode = {
78
+ kind: "Document",
79
+ definitions: [
80
+ {
81
+ kind: "OperationDefinition",
82
+ operation: "subscription",
83
+ variableDefinitions: [],
84
+ name: {
85
+ kind: "Name",
86
+ value: "subscription",
87
+ },
88
+ },
89
+ ],
90
+ };
91
+
92
+ // Act
93
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
94
+
95
+ // Assert
96
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
97
+ `"No subscriptions"`,
98
+ );
99
+ });
100
+
101
+ it("should throw if the document contains more than one query", () => {
102
+ // Arrange
103
+ const documentNode = {
104
+ kind: "Document",
105
+ definitions: [
106
+ {
107
+ kind: "OperationDefinition",
108
+ operation: "query",
109
+ variableDefinitions: [],
110
+ name: {
111
+ kind: "Name",
112
+ value: "query",
113
+ },
114
+ },
115
+ {
116
+ kind: "OperationDefinition",
117
+ operation: "query",
118
+ variableDefinitions: [],
119
+ name: {
120
+ kind: "Name",
121
+ value: "query",
122
+ },
123
+ },
124
+ ],
125
+ };
126
+
127
+ // Act
128
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
129
+
130
+ // Assert
131
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
132
+ `"Too many ops"`,
133
+ );
134
+ });
135
+
136
+ it("should throw if the document contains more than one mutation", () => {
137
+ // Arrange
138
+ const documentNode = {
139
+ kind: "Document",
140
+ definitions: [
141
+ {
142
+ kind: "OperationDefinition",
143
+ operation: "mutation",
144
+ variableDefinitions: [],
145
+ name: {
146
+ kind: "Name",
147
+ value: "mutation",
148
+ },
149
+ },
150
+ {
151
+ kind: "OperationDefinition",
152
+ operation: "mutation",
153
+ variableDefinitions: [],
154
+ name: {
155
+ kind: "Name",
156
+ value: "mutation",
157
+ },
158
+ },
159
+ ],
160
+ };
161
+
162
+ // Act
163
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
164
+
165
+ // Assert
166
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
167
+ `"Too many ops"`,
168
+ );
169
+ });
170
+
171
+ it("should throw if the document is a combination of query and mutation", () => {
172
+ // Arrange
173
+ const documentNode = {
174
+ kind: "Document",
175
+ definitions: [
176
+ {
177
+ kind: "OperationDefinition",
178
+ operation: "query",
179
+ variableDefinitions: [],
180
+ name: {
181
+ kind: "Name",
182
+ value: "query",
183
+ },
184
+ },
185
+ {
186
+ kind: "OperationDefinition",
187
+ operation: "mutation",
188
+ variableDefinitions: [],
189
+ name: {
190
+ kind: "Name",
191
+ value: "mutation",
192
+ },
193
+ },
194
+ ],
195
+ };
196
+
197
+ // Act
198
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
199
+
200
+ // Assert
201
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
202
+ `"Too many ops"`,
203
+ );
204
+ });
205
+ });
206
+
207
+ describe("not in production - more informative error messages", () => {
208
+ it("should throw if the document lacks the kind property", () => {
209
+ // Arrange
210
+ const documentNode = ({}: any);
211
+
212
+ // Act
213
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
214
+
215
+ // Assert
216
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
217
+ `"Argument of {} passed to parser was not a valid GraphQL DocumentNode. You may need to use 'graphql-tag' or another method to convert your operation into a document"`,
218
+ );
219
+ });
220
+
221
+ it("should throw if the document contains only fragments", () => {
222
+ // Arrange
223
+ const documentNode = {
224
+ kind: "Document",
225
+ definitions: [
226
+ {
227
+ kind: "FragmentDefinition",
228
+ name: {
229
+ kind: "Name",
230
+ value: "fragment",
231
+ },
232
+ typeCondition: {
233
+ kind: "NamedType",
234
+ name: {
235
+ kind: "Name",
236
+ value: "Query",
237
+ },
238
+ },
239
+ selectionSet: {
240
+ kind: "SelectionSet",
241
+ selections: [
242
+ {
243
+ kind: "Field",
244
+ name: {
245
+ kind: "Name",
246
+ value: "test",
247
+ },
248
+ },
249
+ ],
250
+ },
251
+ },
252
+ ],
253
+ };
254
+
255
+ // Act
256
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
257
+
258
+ // Assert
259
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
260
+ `"Passing only a fragment to 'graphql' is not supported. You must include a query or mutation as well"`,
261
+ );
262
+ });
263
+
264
+ it("should throw if the document contains subscriptions", () => {
265
+ // Arrange
266
+ const documentNode = {
267
+ kind: "Document",
268
+ definitions: [
269
+ {
270
+ kind: "OperationDefinition",
271
+ operation: "subscription",
272
+ variableDefinitions: [],
273
+ name: {
274
+ kind: "Name",
275
+ value: "subscription",
276
+ },
277
+ },
278
+ ],
279
+ };
280
+
281
+ // Act
282
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
283
+
284
+ // Assert
285
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
286
+ `"We do not support subscriptions. {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"subscription\\",\\"variableDefinitions\\":[],\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"subscription\\"}}]} had 1 subscriptions"`,
287
+ );
288
+ });
289
+
290
+ it("should throw if the document contains more than one query", () => {
291
+ // Arrange
292
+ const documentNode = {
293
+ kind: "Document",
294
+ definitions: [
295
+ {
296
+ kind: "OperationDefinition",
297
+ operation: "query",
298
+ variableDefinitions: [],
299
+ name: {
300
+ kind: "Name",
301
+ value: "query",
302
+ },
303
+ },
304
+ {
305
+ kind: "OperationDefinition",
306
+ operation: "query",
307
+ variableDefinitions: [],
308
+ name: {
309
+ kind: "Name",
310
+ value: "query",
311
+ },
312
+ },
313
+ ],
314
+ };
315
+
316
+ // Act
317
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
318
+
319
+ // Assert
320
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
321
+ `"We only support one query or mutation per component. {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"variableDefinitions\\":[],\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"query\\"}},{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"variableDefinitions\\":[],\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"query\\"}}]} had 2 queries and 0 mutations. "`,
322
+ );
323
+ });
324
+
325
+ it("should throw if the document contains more than one mutation", () => {
326
+ // Arrange
327
+ const documentNode = {
328
+ kind: "Document",
329
+ definitions: [
330
+ {
331
+ kind: "OperationDefinition",
332
+ operation: "mutation",
333
+ variableDefinitions: [],
334
+ name: {
335
+ kind: "Name",
336
+ value: "mutation",
337
+ },
338
+ },
339
+ {
340
+ kind: "OperationDefinition",
341
+ operation: "mutation",
342
+ variableDefinitions: [],
343
+ name: {
344
+ kind: "Name",
345
+ value: "mutation",
346
+ },
347
+ },
348
+ ],
349
+ };
350
+
351
+ // Act
352
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
353
+
354
+ // Assert
355
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
356
+ `"We only support one query or mutation per component. {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"mutation\\",\\"variableDefinitions\\":[],\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"mutation\\"}},{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"mutation\\",\\"variableDefinitions\\":[],\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"mutation\\"}}]} had 0 queries and 2 mutations. "`,
357
+ );
358
+ });
359
+
360
+ it("should throw if the document is a combination of query and mutation", () => {
361
+ // Arrange
362
+ const documentNode = {
363
+ kind: "Document",
364
+ definitions: [
365
+ {
366
+ kind: "OperationDefinition",
367
+ operation: "query",
368
+ variableDefinitions: [],
369
+ name: {
370
+ kind: "Name",
371
+ value: "query",
372
+ },
373
+ },
374
+ {
375
+ kind: "OperationDefinition",
376
+ operation: "mutation",
377
+ variableDefinitions: [],
378
+ name: {
379
+ kind: "Name",
380
+ value: "mutation",
381
+ },
382
+ },
383
+ ],
384
+ };
385
+
386
+ // Act
387
+ const underTest = () => graphQLDocumentNodeParser(documentNode);
388
+
389
+ // Assert
390
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
391
+ `"We only support one query or mutation per component. {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"variableDefinitions\\":[],\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"query\\"}},{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"mutation\\",\\"variableDefinitions\\":[],\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"mutation\\"}}]} had 1 queries and 1 mutations. "`,
392
+ );
393
+ });
394
+ });
395
+
396
+ it("should return the operation name, type, and variables for a query", () => {
397
+ // Arrange
398
+ const documentNode: any = {
399
+ kind: "Document",
400
+ definitions: [
401
+ {
402
+ kind: "OperationDefinition",
403
+ operation: "query",
404
+ variableDefinitions: [
405
+ {
406
+ kind: "VariableDefinition",
407
+ variable: {
408
+ kind: "Variable",
409
+ name: {
410
+ kind: "Name",
411
+ value: "variable",
412
+ },
413
+ },
414
+ type: {
415
+ kind: "NamedType",
416
+ name: {
417
+ kind: "Name",
418
+ value: "String",
419
+ },
420
+ },
421
+ defaultValue: {
422
+ kind: "StringValue",
423
+ value: "defaultValue",
424
+ },
425
+ },
426
+ ],
427
+ name: {
428
+ kind: "Name",
429
+ value: "myQuery",
430
+ },
431
+ },
432
+ ],
433
+ };
434
+
435
+ // Act
436
+ const result = graphQLDocumentNodeParser(documentNode);
437
+
438
+ // Assert
439
+ expect(result).toEqual({
440
+ name: "myQuery",
441
+ type: "query",
442
+ variables: documentNode.definitions[0].variableDefinitions,
443
+ });
444
+ });
445
+
446
+ it("should return the operation name, type, and variables for a mutation", () => {
447
+ // Arrange
448
+ const documentNode: any = {
449
+ kind: "Document",
450
+ definitions: [
451
+ {
452
+ kind: "OperationDefinition",
453
+ operation: "mutation",
454
+ variableDefinitions: [
455
+ {
456
+ kind: "VariableDefinition",
457
+ variable: {
458
+ kind: "Variable",
459
+ name: {
460
+ kind: "Name",
461
+ value: "variable",
462
+ },
463
+ },
464
+ type: {
465
+ kind: "NamedType",
466
+ name: {
467
+ kind: "Name",
468
+ value: "String",
469
+ },
470
+ },
471
+ defaultValue: {
472
+ kind: "StringValue",
473
+ value: "defaultValue",
474
+ },
475
+ },
476
+ ],
477
+ name: {
478
+ kind: "Name",
479
+ value: "myMutation",
480
+ },
481
+ },
482
+ ],
483
+ };
484
+
485
+ // Act
486
+ const result = graphQLDocumentNodeParser(documentNode);
487
+
488
+ // Assert
489
+ expect(result).toEqual({
490
+ name: "myMutation",
491
+ type: "mutation",
492
+ variables: documentNode.definitions[0].variableDefinitions,
493
+ });
494
+ });
495
+
496
+ it("should use name of data if no name in document", () => {
497
+ // Arrange
498
+ const documentNode: any = {
499
+ kind: "Document",
500
+ definitions: [
501
+ {
502
+ kind: "OperationDefinition",
503
+ operation: "query",
504
+ },
505
+ ],
506
+ };
507
+
508
+ // Act
509
+ const result = graphQLDocumentNodeParser(documentNode);
510
+
511
+ // Assert
512
+ expect(result).toEqual({
513
+ name: "data",
514
+ type: "query",
515
+ variables: [],
516
+ });
517
+ });
518
+
519
+ it("should cache the output", () => {
520
+ // Arrange
521
+ const documentNode: any = {
522
+ kind: "Document",
523
+ definitions: [
524
+ {
525
+ kind: "OperationDefinition",
526
+ operation: "query",
527
+ name: {
528
+ kind: "Name",
529
+ value: "myQuery",
530
+ },
531
+ },
532
+ ],
533
+ };
534
+
535
+ // Act
536
+ const initialResult = graphQLDocumentNodeParser(documentNode);
537
+ const secondResult = graphQLDocumentNodeParser(documentNode);
538
+
539
+ // Assert
540
+ expect(initialResult).toBe(secondResult);
541
+ });
542
+ });
@@ -0,0 +1,35 @@
1
+ // @flow
2
+ import {SsrCache} from "../ssr-cache.js";
3
+
4
+ import {
5
+ initializeHydrationCache,
6
+ purgeHydrationCache,
7
+ } from "../hydration-cache-api.js";
8
+
9
+ describe("#initializeHydrationCache", () => {
10
+ it("should call SsrCache.Default.initialize", () => {
11
+ // Arrange
12
+ const sourceCache = {};
13
+ const initSpy = jest.spyOn(SsrCache.Default, "initialize");
14
+
15
+ // Act
16
+ initializeHydrationCache(sourceCache);
17
+
18
+ // Assert
19
+ expect(initSpy).toHaveBeenCalledWith(sourceCache);
20
+ });
21
+ });
22
+
23
+ describe("#purgeHydrationCache", () => {
24
+ it("should call SsrCache.Default.purgeData", () => {
25
+ // Arrange
26
+ const predicate = jest.fn();
27
+ const purgeDataSpy = jest.spyOn(SsrCache.Default, "purgeData");
28
+
29
+ // Act
30
+ purgeHydrationCache(predicate);
31
+
32
+ // Assert
33
+ expect(purgeDataSpy).toHaveBeenCalledWith(predicate);
34
+ });
35
+ });
@@ -0,0 +1,29 @@
1
+ // @flow
2
+ import * as UseSharedCache from "../../hooks/use-shared-cache.js";
3
+ import * as HydrationCacheApi from "../hydration-cache-api.js";
4
+
5
+ import {purgeCaches} from "../purge-caches.js";
6
+
7
+ describe("#purgeCaches", () => {
8
+ it("should purge the shared cache", () => {
9
+ // Arrange
10
+ const spy = jest.spyOn(UseSharedCache, "purgeSharedCache");
11
+
12
+ // Act
13
+ purgeCaches();
14
+
15
+ // Assert
16
+ expect(spy).toHaveBeenCalled();
17
+ });
18
+
19
+ it("should purge the hydration cache", () => {
20
+ // Arrange
21
+ const spy = jest.spyOn(HydrationCacheApi, "purgeHydrationCache");
22
+
23
+ // Act
24
+ purgeCaches();
25
+
26
+ // Assert
27
+ expect(spy).toHaveBeenCalled();
28
+ });
29
+ });