@khanacademy/wonder-blocks-data 7.0.0 → 8.0.1

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 +32 -0
  2. package/dist/es/index.js +321 -759
  3. package/dist/index.js +1188 -802
  4. package/package.json +3 -3
  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 +68 -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 +11 -24
  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,188 @@
1
+ // @flow
2
+ import {Server} from "@khanacademy/wonder-blocks-core";
3
+ import {RequestFulfillment} from "../request-fulfillment.js";
4
+ import {RequestTracker} from "../request-tracking.js";
5
+
6
+ import {
7
+ abortInflightRequests,
8
+ fetchTrackedRequests,
9
+ hasTrackedRequestsToBeFetched,
10
+ } from "../request-api.js";
11
+
12
+ describe("#fetchTrackedRequests", () => {
13
+ describe("when server-side", () => {
14
+ beforeEach(() => {
15
+ jest.spyOn(Server, "isServerSide").mockReturnValue(true);
16
+ });
17
+
18
+ it("should call RequestTracker.Default.fulfillTrackedRequests", () => {
19
+ // Arrange
20
+ const fulfillTrackedRequestsSpy = jest.spyOn(
21
+ RequestTracker.Default,
22
+ "fulfillTrackedRequests",
23
+ );
24
+
25
+ // Act
26
+ fetchTrackedRequests();
27
+
28
+ // Assert
29
+ expect(fulfillTrackedRequestsSpy).toHaveBeenCalled();
30
+ });
31
+
32
+ it("should return the response cache", async () => {
33
+ // Arrange
34
+ const responseCache = {};
35
+ jest.spyOn(
36
+ RequestTracker.Default,
37
+ "fulfillTrackedRequests",
38
+ ).mockResolvedValue(responseCache);
39
+
40
+ // Act
41
+ const result = await fetchTrackedRequests();
42
+
43
+ // Assert
44
+ expect(result).toBe(responseCache);
45
+ });
46
+ });
47
+
48
+ describe("when client-side", () => {
49
+ const NODE_ENV = process.env.NODE_ENV;
50
+ beforeEach(() => {
51
+ jest.spyOn(Server, "isServerSide").mockReturnValue(false);
52
+ });
53
+
54
+ afterEach(() => {
55
+ if (NODE_ENV === undefined) {
56
+ delete process.env.NODE_ENV;
57
+ } else {
58
+ process.env.NODE_ENV = NODE_ENV;
59
+ }
60
+ });
61
+
62
+ describe("in production", () => {
63
+ it("should reject with error", async () => {
64
+ // Arrange
65
+ process.env.NODE_ENV = "production";
66
+
67
+ // Act
68
+ const result = fetchTrackedRequests();
69
+
70
+ // Assert
71
+ await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(
72
+ `"No CSR tracking"`,
73
+ );
74
+ });
75
+ });
76
+
77
+ describe("not in production", () => {
78
+ it("should reject with error", async () => {
79
+ // Arrange
80
+ process.env.NODE_ENV = "test";
81
+
82
+ // Act
83
+ const result = fetchTrackedRequests();
84
+
85
+ // Assert
86
+ await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(
87
+ `"Data requests are not tracked for fulfillment when when client-side"`,
88
+ );
89
+ });
90
+ });
91
+ });
92
+ });
93
+
94
+ describe("#hasTrackedRequestsToBeFetched", () => {
95
+ describe("when server-side", () => {
96
+ beforeEach(() => {
97
+ jest.spyOn(Server, "isServerSide").mockReturnValue(true);
98
+ });
99
+
100
+ it("should call RequestTracker.Default.hasUnfulfilledRequests", () => {
101
+ // Arrange
102
+ const hasUnfulfilledRequestsSpy = jest.spyOn(
103
+ RequestTracker.Default,
104
+ "hasUnfulfilledRequests",
105
+ "get",
106
+ );
107
+
108
+ // Act
109
+ hasTrackedRequestsToBeFetched();
110
+
111
+ // Assert
112
+ expect(hasUnfulfilledRequestsSpy).toHaveBeenCalled();
113
+ });
114
+
115
+ it("should return the boolean value from RequestTracker.Default.hasUnfulfilledRequests", () => {
116
+ // Arrange
117
+ jest.spyOn(
118
+ RequestTracker.Default,
119
+ "hasUnfulfilledRequests",
120
+ "get",
121
+ ).mockReturnValue(true);
122
+
123
+ // Act
124
+ const result = hasTrackedRequestsToBeFetched();
125
+
126
+ // Assert
127
+ expect(result).toBeTrue();
128
+ });
129
+ });
130
+
131
+ describe("when client-side", () => {
132
+ const NODE_ENV = process.env.NODE_ENV;
133
+ beforeEach(() => {
134
+ jest.spyOn(Server, "isServerSide").mockReturnValue(false);
135
+ });
136
+
137
+ afterEach(() => {
138
+ if (NODE_ENV === undefined) {
139
+ delete process.env.NODE_ENV;
140
+ } else {
141
+ process.env.NODE_ENV = NODE_ENV;
142
+ }
143
+ });
144
+
145
+ describe("in production", () => {
146
+ it("should reject with error", () => {
147
+ // Arrange
148
+ process.env.NODE_ENV = "production";
149
+
150
+ // Act
151
+ const underTest = () => hasTrackedRequestsToBeFetched();
152
+
153
+ // Assert
154
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
155
+ `"No CSR tracking"`,
156
+ );
157
+ });
158
+ });
159
+
160
+ describe("not in production", () => {
161
+ it("should reject with error", () => {
162
+ // Arrange
163
+ process.env.NODE_ENV = "test";
164
+
165
+ // Act
166
+ const underTest = () => hasTrackedRequestsToBeFetched();
167
+
168
+ // Assert
169
+ expect(underTest).toThrowErrorMatchingInlineSnapshot(
170
+ `"Data requests are not tracked for fulfillment when when client-side"`,
171
+ );
172
+ });
173
+ });
174
+ });
175
+ });
176
+
177
+ describe("#abortInflightRequests", () => {
178
+ it("should call RequestFulfillment.Default.abortAll", () => {
179
+ // Arrange
180
+ const abortAllSpy = jest.spyOn(RequestFulfillment.Default, "abortAll");
181
+
182
+ // Act
183
+ abortInflightRequests();
184
+
185
+ // Assert
186
+ expect(abortAllSpy).toHaveBeenCalled();
187
+ });
188
+ });
@@ -102,4 +102,46 @@ describe("RequestFulfillment", () => {
102
102
  expect(result).not.toBe(promise);
103
103
  });
104
104
  });
105
+
106
+ describe("#abort", () => {
107
+ it("should delete the given request from the inflight requests", () => {
108
+ // Arrange
109
+ const requestFulfillment = new RequestFulfillment();
110
+ const fakeRequestHandler = () => Promise.resolve("DATA!");
111
+ const promise = requestFulfillment.fulfill("ID", {
112
+ handler: fakeRequestHandler,
113
+ });
114
+
115
+ // Act
116
+ requestFulfillment.abort("ID");
117
+ const result = requestFulfillment.fulfill("ID", {
118
+ handler: fakeRequestHandler,
119
+ });
120
+
121
+ // Assert
122
+ expect(result).not.toBe(promise);
123
+ });
124
+ });
125
+
126
+ describe("#abortAll", () => {
127
+ it("should abort all inflight requests", () => {
128
+ // Arrange
129
+ const requestFulfillment = new RequestFulfillment();
130
+ const abortSpy = jest.spyOn(requestFulfillment, "abort");
131
+ const fakeRequestHandler = () => Promise.resolve("DATA!");
132
+ requestFulfillment.fulfill("ID1", {
133
+ handler: fakeRequestHandler,
134
+ });
135
+ requestFulfillment.fulfill("ID2", {
136
+ handler: fakeRequestHandler,
137
+ });
138
+
139
+ // Act
140
+ requestFulfillment.abortAll();
141
+
142
+ // Assert
143
+ expect(abortSpy).toHaveBeenCalledWith("ID1");
144
+ expect(abortSpy).toHaveBeenCalledWith("ID2");
145
+ });
146
+ });
105
147
  });
@@ -16,6 +16,64 @@ describe("../ssr-cache.js", () => {
16
16
  jest.restoreAllMocks();
17
17
  });
18
18
 
19
+ describe("#constructor", () => {
20
+ const NODE_ENV = process.env.NODE_ENV;
21
+
22
+ afterEach(() => {
23
+ if (NODE_ENV == null) {
24
+ delete process.env.NODE_ENV;
25
+ } else {
26
+ process.env.NODE_ENV = NODE_ENV;
27
+ }
28
+ });
29
+
30
+ it.each(["development", "production"])(
31
+ "should default the ssr-only cache to a undefined when client-side in %s",
32
+ (nodeEnv) => {
33
+ // Arrange
34
+ jest.spyOn(Server, "isServerSide").mockReturnValue(false);
35
+ process.env.NODE_ENV = nodeEnv;
36
+
37
+ // Act
38
+ const cache = new SsrCache();
39
+
40
+ // Assert
41
+ expect(cache._ssrOnlyCache).toBeUndefined();
42
+ },
43
+ );
44
+
45
+ it("should default the ssr-only cache to a cache instance when client-side in test", () => {
46
+ // Arrange
47
+ jest.spyOn(Server, "isServerSide").mockReturnValue(false);
48
+ process.env.NODE_ENV = "test";
49
+
50
+ // Act
51
+ const cache = new SsrCache();
52
+
53
+ // Assert
54
+ expect(cache._ssrOnlyCache).toBeInstanceOf(
55
+ SerializableInMemoryCache,
56
+ );
57
+ });
58
+
59
+ it.each(["development", "production"])(
60
+ "should default the ssr-only cache to a cache instance when server-side in %s",
61
+ (nodeEnv) => {
62
+ // Arrange
63
+ jest.spyOn(Server, "isServerSide").mockReturnValue(true);
64
+ process.env.NODE_ENV = nodeEnv;
65
+
66
+ // Act
67
+ const cache = new SsrCache();
68
+
69
+ // Assert
70
+ expect(cache._ssrOnlyCache).toBeInstanceOf(
71
+ SerializableInMemoryCache,
72
+ );
73
+ },
74
+ );
75
+ });
76
+
19
77
  describe("@Default", () => {
20
78
  it("should return an instance of SsrCache", () => {
21
79
  // Arrange
@@ -393,56 +451,6 @@ describe("../ssr-cache.js", () => {
393
451
  });
394
452
  });
395
453
 
396
- describe("#remove", () => {
397
- it("should return false if nothing was removed", () => {
398
- // Arrange
399
- const hydrationCache = new SerializableInMemoryCache();
400
- const ssrOnlycache = new SerializableInMemoryCache();
401
- jest.spyOn(hydrationCache, "purge").mockReturnValue(false);
402
- jest.spyOn(ssrOnlycache, "purge").mockReturnValue(false);
403
- const cache = new SsrCache(hydrationCache, ssrOnlycache);
404
-
405
- // Act
406
- const result = cache.remove("A");
407
-
408
- // Assert
409
- expect(result).toBeFalsy();
410
- });
411
-
412
- it("should return true if something was removed from hydration cache", () => {
413
- // Arrange
414
- const hydrationCache = new SerializableInMemoryCache();
415
- jest.spyOn(hydrationCache, "purge").mockReturnValue(true);
416
- const cache = new SsrCache(hydrationCache);
417
-
418
- // Act
419
- const result = cache.remove("A");
420
-
421
- // Assert
422
- expect(result).toBeTruthy();
423
- });
424
-
425
- describe("when server-side", () => {
426
- beforeEach(() => {
427
- jest.spyOn(Server, "isServerSide").mockReturnValue(true);
428
- });
429
-
430
- it("should return true if something was removed from ssr-only cache", () => {
431
- // Arrange
432
- const hydrationCache = new SerializableInMemoryCache();
433
- const ssrOnlyCache = new SerializableInMemoryCache();
434
- jest.spyOn(ssrOnlyCache, "purge").mockReturnValue(true);
435
- const cache = new SsrCache(hydrationCache, ssrOnlyCache);
436
-
437
- // Act
438
- const result = cache.remove("A");
439
-
440
- // Assert
441
- expect(result).toBeTruthy();
442
- });
443
- });
444
- });
445
-
446
454
  describe("#cloneHydratableData", () => {
447
455
  it("should clone the hydration cache", () => {
448
456
  // Arrange
@@ -467,7 +475,7 @@ describe("../ssr-cache.js", () => {
467
475
  });
468
476
  });
469
477
 
470
- describe("#removeAll", () => {
478
+ describe("#purgeData", () => {
471
479
  describe("when client-side", () => {
472
480
  beforeEach(() => {
473
481
  jest.spyOn(Server, "isServerSide").mockReturnValue(false);
@@ -480,7 +488,7 @@ describe("../ssr-cache.js", () => {
480
488
  const cache = new SsrCache(hydrationCache);
481
489
 
482
490
  // Act
483
- cache.removeAll();
491
+ cache.purgeData();
484
492
 
485
493
  // Assert
486
494
  expect(purgeAllSpy).toHaveBeenCalledWith(undefined);
@@ -496,7 +504,7 @@ describe("../ssr-cache.js", () => {
496
504
  );
497
505
 
498
506
  // Act
499
- cache.removeAll(() => true);
507
+ cache.purgeData(() => true);
500
508
 
501
509
  // Assert
502
510
  expect(purgeAllSpy).toHaveBeenCalledWith(expect.any(Function));
@@ -518,7 +526,7 @@ describe("../ssr-cache.js", () => {
518
526
  const predicate = jest.fn().mockReturnValue(false);
519
527
 
520
528
  // Act
521
- cache.removeAll(predicate);
529
+ cache.purgeData(predicate);
522
530
 
523
531
  // Assert
524
532
  expect(predicate).toHaveBeenCalledWith("KEY1", {data: "DATA"});
@@ -543,7 +551,7 @@ describe("../ssr-cache.js", () => {
543
551
  );
544
552
 
545
553
  // Act
546
- cache.removeAll();
554
+ cache.purgeData();
547
555
 
548
556
  // Assert
549
557
  expect(hydrationPurgeAllSpy).toHaveBeenCalledWith(undefined);
@@ -559,7 +567,7 @@ describe("../ssr-cache.js", () => {
559
567
  );
560
568
 
561
569
  // Act
562
- cache.removeAll();
570
+ cache.purgeData();
563
571
 
564
572
  // Assert
565
573
  expect(ssrPurgeAllSpy).toHaveBeenCalledWith(undefined);
@@ -575,7 +583,7 @@ describe("../ssr-cache.js", () => {
575
583
  );
576
584
 
577
585
  // Act
578
- cache.removeAll(() => true);
586
+ cache.purgeData(() => true);
579
587
 
580
588
  // Assert
581
589
  expect(purgeAllSpy).toHaveBeenCalledWith(expect.any(Function));
@@ -591,7 +599,7 @@ describe("../ssr-cache.js", () => {
591
599
  );
592
600
 
593
601
  // Act
594
- cache.removeAll(() => true);
602
+ cache.purgeData(() => true);
595
603
 
596
604
  // Assert
597
605
  expect(purgeAllSpy).toHaveBeenCalledWith(expect.any(Function));
@@ -608,7 +616,7 @@ describe("../ssr-cache.js", () => {
608
616
  const predicate = jest.fn().mockReturnValue(false);
609
617
 
610
618
  // Act
611
- cache.removeAll(predicate);
619
+ cache.purgeData(predicate);
612
620
 
613
621
  // Assert
614
622
  expect(predicate).toHaveBeenCalledWith("KEY1", {data: "DATA"});
@@ -629,7 +637,7 @@ describe("../ssr-cache.js", () => {
629
637
  const predicate = jest.fn().mockReturnValue(false);
630
638
 
631
639
  // Act
632
- cache.removeAll(predicate);
640
+ cache.purgeData(predicate);
633
641
 
634
642
  // Assert
635
643
  expect(predicate).toHaveBeenCalledWith("KEY1", {data: "DATA"});
@@ -0,0 +1,42 @@
1
+ // @flow
2
+ import {toGqlOperation} from "../to-gql-operation.js";
3
+ import * as GDNP from "../graphql-document-node-parser.js";
4
+
5
+ jest.mock("../graphql-document-node-parser.js");
6
+
7
+ describe("#toGqlOperation", () => {
8
+ it("should parse the document node", () => {
9
+ // Arrange
10
+ const documentNode: any = {};
11
+ const parserSpy = jest
12
+ .spyOn(GDNP, "graphQLDocumentNodeParser")
13
+ .mockReturnValue({
14
+ name: "operationName",
15
+ type: "query",
16
+ });
17
+
18
+ // Act
19
+ toGqlOperation(documentNode);
20
+
21
+ // Assert
22
+ expect(parserSpy).toHaveBeenCalledWith(documentNode);
23
+ });
24
+
25
+ it("should return the Wonder Blocks Data representation of the given document node", () => {
26
+ // Arrange
27
+ const documentNode: any = {};
28
+ jest.spyOn(GDNP, "graphQLDocumentNodeParser").mockReturnValue({
29
+ name: "operationName",
30
+ type: "mutation",
31
+ });
32
+
33
+ // Act
34
+ const result = toGqlOperation(documentNode);
35
+
36
+ // Assert
37
+ expect(result).toStrictEqual({
38
+ id: "operationName",
39
+ type: "mutation",
40
+ });
41
+ });
42
+ });
@@ -26,6 +26,12 @@ export const DataErrors = Object.freeze({
26
26
  */
27
27
  Network: "Network",
28
28
 
29
+ /**
30
+ * There was a problem due to the state of the system not matching the
31
+ * requested operation or input.
32
+ */
33
+ NotAllowed: "NotAllowed",
34
+
29
35
  /**
30
36
  * Response could not be parsed.
31
37
  */
@@ -0,0 +1,50 @@
1
+ // @flow
2
+ import type {GqlOperation, GqlContext} from "./gql-types.js";
3
+
4
+ const toString = (valid: mixed): string => {
5
+ if (typeof valid === "string") {
6
+ return valid;
7
+ }
8
+ return JSON.stringify(valid) ?? "";
9
+ };
10
+
11
+ /**
12
+ * Get an identifier for a given request.
13
+ */
14
+ export const getGqlRequestId = <TData, TVariables: {...}>(
15
+ operation: GqlOperation<TData, TVariables>,
16
+ variables: ?TVariables,
17
+ context: GqlContext,
18
+ ): string => {
19
+ // We add all the bits for this into an array and then join them with
20
+ // a chosen separator.
21
+ const parts = [];
22
+
23
+ // First, we push the context values.
24
+ const sortableContext = new URLSearchParams(context);
25
+ // $FlowIgnore[prop-missing] Flow has incomplete support for URLSearchParams
26
+ sortableContext.sort();
27
+ parts.push(sortableContext.toString());
28
+
29
+ // Now we add the operation identifier.
30
+ parts.push(operation.id);
31
+
32
+ // Finally, if we have variables, we add those too.
33
+ if (variables != null) {
34
+ // We need to turn each variable into a string.
35
+ const stringifiedVariables = Object.keys(variables).reduce(
36
+ (acc, key) => {
37
+ acc[key] = toString(variables[key]);
38
+ return acc;
39
+ },
40
+ {},
41
+ );
42
+ // We use the same mechanism as context to sort and arrange the
43
+ // variables.
44
+ const sortableVariables = new URLSearchParams(stringifiedVariables);
45
+ // $FlowIgnore[prop-missing] Flow has incomplete support for URLSearchParams
46
+ sortableVariables.sort();
47
+ parts.push(sortableVariables.toString());
48
+ }
49
+ return parts.join("|");
50
+ };
@@ -0,0 +1,133 @@
1
+ // @flow
2
+ import type {
3
+ DocumentNode,
4
+ DefinitionNode,
5
+ VariableDefinitionNode,
6
+ OperationDefinitionNode,
7
+ } from "./graphql-types.js";
8
+ import {DataError, DataErrors} from "./data-error.js";
9
+
10
+ export const DocumentTypes = Object.freeze({
11
+ query: "query",
12
+ mutation: "mutation",
13
+ });
14
+
15
+ export type DocumentType = $Values<typeof DocumentTypes>;
16
+
17
+ export interface IDocumentDefinition {
18
+ type: DocumentType;
19
+ name: string;
20
+ variables: $ReadOnlyArray<VariableDefinitionNode>;
21
+ }
22
+
23
+ const cache = new Map<DocumentNode, IDocumentDefinition>();
24
+
25
+ /**
26
+ * Parse a GraphQL document node to determine some info about it.
27
+ *
28
+ * This is based on:
29
+ * https://github.com/apollographql/react-apollo/blob/3bc993b2ea91704bd6a2667f42d1940656c071ff/src/parser.ts
30
+ */
31
+ export function graphQLDocumentNodeParser(
32
+ document: DocumentNode,
33
+ ): IDocumentDefinition {
34
+ const cached = cache.get(document);
35
+ if (cached) {
36
+ return cached;
37
+ }
38
+
39
+ /**
40
+ * Saftey check for proper usage.
41
+ */
42
+ if (!document?.kind) {
43
+ if (process.env.NODE_ENV === "production") {
44
+ throw new DataError("Bad DocumentNode", DataErrors.InvalidInput);
45
+ } else {
46
+ throw new DataError(
47
+ `Argument of ${JSON.stringify(
48
+ document,
49
+ )} passed to parser was not a valid GraphQL ` +
50
+ `DocumentNode. You may need to use 'graphql-tag' or another method ` +
51
+ `to convert your operation into a document`,
52
+ DataErrors.InvalidInput,
53
+ );
54
+ }
55
+ }
56
+
57
+ const fragments = document.definitions.filter(
58
+ (x: DefinitionNode) => x.kind === "FragmentDefinition",
59
+ );
60
+
61
+ const queries = document.definitions.filter(
62
+ (x: DefinitionNode) =>
63
+ // $FlowIgnore[prop-missing]
64
+ x.kind === "OperationDefinition" && x.operation === "query",
65
+ );
66
+
67
+ const mutations = document.definitions.filter(
68
+ (x: DefinitionNode) =>
69
+ // $FlowIgnore[prop-missing]
70
+ x.kind === "OperationDefinition" && x.operation === "mutation",
71
+ );
72
+
73
+ const subscriptions = document.definitions.filter(
74
+ (x: DefinitionNode) =>
75
+ // $FlowIgnore[prop-missing]
76
+ x.kind === "OperationDefinition" && x.operation === "subscription",
77
+ );
78
+
79
+ if (fragments.length && !queries.length && !mutations.length) {
80
+ if (process.env.NODE_ENV === "production") {
81
+ throw new DataError("Fragment only", DataErrors.InvalidInput);
82
+ } else {
83
+ throw new DataError(
84
+ `Passing only a fragment to 'graphql' is not supported. ` +
85
+ `You must include a query or mutation as well`,
86
+ DataErrors.InvalidInput,
87
+ );
88
+ }
89
+ }
90
+
91
+ if (subscriptions.length) {
92
+ if (process.env.NODE_ENV === "production") {
93
+ throw new DataError("No subscriptions", DataErrors.InvalidInput);
94
+ } else {
95
+ throw new DataError(
96
+ `We do not support subscriptions. ` +
97
+ `${JSON.stringify(document)} had ${
98
+ subscriptions.length
99
+ } subscriptions`,
100
+ DataErrors.InvalidInput,
101
+ );
102
+ }
103
+ }
104
+
105
+ if (queries.length + mutations.length > 1) {
106
+ if (process.env.NODE_ENV === "production") {
107
+ throw new DataError("Too many ops", DataErrors.InvalidInput);
108
+ } else {
109
+ throw new DataError(
110
+ `We only support one query or mutation per component. ` +
111
+ `${JSON.stringify(document)} had ${
112
+ queries.length
113
+ } queries and ` +
114
+ `${mutations.length} mutations. `,
115
+ DataErrors.InvalidInput,
116
+ );
117
+ }
118
+ }
119
+
120
+ const type = queries.length ? DocumentTypes.query : DocumentTypes.mutation;
121
+ const definitions = queries.length ? queries : mutations;
122
+
123
+ const definition: OperationDefinitionNode = (definitions[0]: any);
124
+ const variables = definition.variableDefinitions || [];
125
+
126
+ // fallback to using data if no name
127
+ const name =
128
+ definition.name?.kind === "Name" ? definition.name.value : "data";
129
+
130
+ const payload: IDocumentDefinition = {name, type, variables};
131
+ cache.set(document, payload);
132
+ return payload;
133
+ }