@khanacademy/wonder-blocks-data 7.0.1 → 8.0.2

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 +286 -107
  3. package/dist/index.js +1089 -713
  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 +58 -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 +13 -31
  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,54 @@ describe("../ssr-cache.js", () => {
16
16
  jest.restoreAllMocks();
17
17
  });
18
18
 
19
+ describe("#constructor", () => {
20
+ it("should default the ssr-only cache to a cache instance", () => {
21
+ // Arrange
22
+
23
+ // Act
24
+ const cache = new SsrCache();
25
+
26
+ // Assert
27
+ expect(cache._ssrOnlyCache).toBeInstanceOf(
28
+ SerializableInMemoryCache,
29
+ );
30
+ });
31
+
32
+ it("should set the hydration cache to the passed instance if there is one", () => {
33
+ // Arrange
34
+ const passedInstance = new SerializableInMemoryCache();
35
+
36
+ // Act
37
+ const cache = new SsrCache(null, passedInstance);
38
+
39
+ // Assert
40
+ expect(cache._ssrOnlyCache).toBe(passedInstance);
41
+ });
42
+
43
+ it("should default the hydration cache to a cache instance", () => {
44
+ // Arrange
45
+
46
+ // Act
47
+ const cache = new SsrCache();
48
+
49
+ // Assert
50
+ expect(cache._hydrationCache).toBeInstanceOf(
51
+ SerializableInMemoryCache,
52
+ );
53
+ });
54
+
55
+ it("should set the hydration cache to the passed instance if there is one", () => {
56
+ // Arrange
57
+ const passedInstance = new SerializableInMemoryCache();
58
+
59
+ // Act
60
+ const cache = new SsrCache(passedInstance);
61
+
62
+ // Assert
63
+ expect(cache._hydrationCache).toBe(passedInstance);
64
+ });
65
+ });
66
+
19
67
  describe("@Default", () => {
20
68
  it("should return an instance of SsrCache", () => {
21
69
  // Arrange
@@ -393,56 +441,6 @@ describe("../ssr-cache.js", () => {
393
441
  });
394
442
  });
395
443
 
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
444
  describe("#cloneHydratableData", () => {
447
445
  it("should clone the hydration cache", () => {
448
446
  // Arrange
@@ -467,7 +465,7 @@ describe("../ssr-cache.js", () => {
467
465
  });
468
466
  });
469
467
 
470
- describe("#removeAll", () => {
468
+ describe("#purgeData", () => {
471
469
  describe("when client-side", () => {
472
470
  beforeEach(() => {
473
471
  jest.spyOn(Server, "isServerSide").mockReturnValue(false);
@@ -480,7 +478,7 @@ describe("../ssr-cache.js", () => {
480
478
  const cache = new SsrCache(hydrationCache);
481
479
 
482
480
  // Act
483
- cache.removeAll();
481
+ cache.purgeData();
484
482
 
485
483
  // Assert
486
484
  expect(purgeAllSpy).toHaveBeenCalledWith(undefined);
@@ -496,7 +494,7 @@ describe("../ssr-cache.js", () => {
496
494
  );
497
495
 
498
496
  // Act
499
- cache.removeAll(() => true);
497
+ cache.purgeData(() => true);
500
498
 
501
499
  // Assert
502
500
  expect(purgeAllSpy).toHaveBeenCalledWith(expect.any(Function));
@@ -518,7 +516,7 @@ describe("../ssr-cache.js", () => {
518
516
  const predicate = jest.fn().mockReturnValue(false);
519
517
 
520
518
  // Act
521
- cache.removeAll(predicate);
519
+ cache.purgeData(predicate);
522
520
 
523
521
  // Assert
524
522
  expect(predicate).toHaveBeenCalledWith("KEY1", {data: "DATA"});
@@ -543,7 +541,7 @@ describe("../ssr-cache.js", () => {
543
541
  );
544
542
 
545
543
  // Act
546
- cache.removeAll();
544
+ cache.purgeData();
547
545
 
548
546
  // Assert
549
547
  expect(hydrationPurgeAllSpy).toHaveBeenCalledWith(undefined);
@@ -559,7 +557,7 @@ describe("../ssr-cache.js", () => {
559
557
  );
560
558
 
561
559
  // Act
562
- cache.removeAll();
560
+ cache.purgeData();
563
561
 
564
562
  // Assert
565
563
  expect(ssrPurgeAllSpy).toHaveBeenCalledWith(undefined);
@@ -575,7 +573,7 @@ describe("../ssr-cache.js", () => {
575
573
  );
576
574
 
577
575
  // Act
578
- cache.removeAll(() => true);
576
+ cache.purgeData(() => true);
579
577
 
580
578
  // Assert
581
579
  expect(purgeAllSpy).toHaveBeenCalledWith(expect.any(Function));
@@ -591,7 +589,7 @@ describe("../ssr-cache.js", () => {
591
589
  );
592
590
 
593
591
  // Act
594
- cache.removeAll(() => true);
592
+ cache.purgeData(() => true);
595
593
 
596
594
  // Assert
597
595
  expect(purgeAllSpy).toHaveBeenCalledWith(expect.any(Function));
@@ -608,7 +606,7 @@ describe("../ssr-cache.js", () => {
608
606
  const predicate = jest.fn().mockReturnValue(false);
609
607
 
610
608
  // Act
611
- cache.removeAll(predicate);
609
+ cache.purgeData(predicate);
612
610
 
613
611
  // Assert
614
612
  expect(predicate).toHaveBeenCalledWith("KEY1", {data: "DATA"});
@@ -629,7 +627,7 @@ describe("../ssr-cache.js", () => {
629
627
  const predicate = jest.fn().mockReturnValue(false);
630
628
 
631
629
  // Act
632
- cache.removeAll(predicate);
630
+ cache.purgeData(predicate);
633
631
 
634
632
  // Assert
635
633
  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
+ }