@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.
- package/CHANGELOG.md +32 -0
- package/dist/es/index.js +321 -759
- package/dist/index.js +1188 -802
- package/package.json +3 -3
- package/src/__docs__/_overview_ssr_.stories.mdx +13 -13
- package/src/__docs__/exports.abort-inflight-requests.stories.mdx +20 -0
- package/src/__docs__/exports.data.stories.mdx +3 -3
- package/src/__docs__/{exports.fulfill-all-data-requests.stories.mdx → exports.fetch-tracked-requests.stories.mdx} +5 -5
- package/src/__docs__/exports.get-gql-request-id.stories.mdx +24 -0
- package/src/__docs__/{exports.has-unfulfilled-requests.stories.mdx → exports.has-tracked-requests-to-be-fetched.stories.mdx} +4 -4
- package/src/__docs__/exports.intialize-hydration-cache.stories.mdx +29 -0
- package/src/__docs__/exports.purge-caches.stories.mdx +23 -0
- package/src/__docs__/{exports.remove-all-from-cache.stories.mdx → exports.purge-hydration-cache.stories.mdx} +4 -4
- package/src/__docs__/{exports.clear-shared-cache.stories.mdx → exports.purge-shared-cache.stories.mdx} +4 -4
- package/src/__docs__/exports.track-data.stories.mdx +4 -4
- package/src/__docs__/exports.use-cached-effect.stories.mdx +7 -4
- package/src/__docs__/exports.use-gql.stories.mdx +1 -33
- package/src/__docs__/exports.use-server-effect.stories.mdx +1 -1
- package/src/__docs__/exports.use-shared-cache.stories.mdx +2 -2
- package/src/__docs__/types.fetch-policy.stories.mdx +44 -0
- package/src/__docs__/types.response-cache.stories.mdx +1 -1
- package/src/__tests__/generated-snapshot.test.js +5 -5
- package/src/components/__tests__/data.test.js +2 -6
- package/src/hooks/__tests__/use-cached-effect.test.js +341 -100
- package/src/hooks/__tests__/use-hydratable-effect.test.js +15 -9
- package/src/hooks/__tests__/use-shared-cache.test.js +6 -6
- package/src/hooks/use-cached-effect.js +169 -93
- package/src/hooks/use-hydratable-effect.js +8 -1
- package/src/hooks/use-shared-cache.js +2 -2
- package/src/index.js +14 -78
- package/src/util/__tests__/get-gql-request-id.test.js +74 -0
- package/src/util/__tests__/graphql-document-node-parser.test.js +542 -0
- package/src/util/__tests__/hydration-cache-api.test.js +35 -0
- package/src/util/__tests__/purge-caches.test.js +29 -0
- package/src/util/__tests__/request-api.test.js +188 -0
- package/src/util/__tests__/request-fulfillment.test.js +42 -0
- package/src/util/__tests__/ssr-cache.test.js +68 -60
- package/src/util/__tests__/to-gql-operation.test.js +42 -0
- package/src/util/data-error.js +6 -0
- package/src/util/get-gql-request-id.js +50 -0
- package/src/util/graphql-document-node-parser.js +133 -0
- package/src/util/graphql-types.js +30 -0
- package/src/util/hydration-cache-api.js +28 -0
- package/src/util/purge-caches.js +15 -0
- package/src/util/request-api.js +66 -0
- package/src/util/request-fulfillment.js +32 -12
- package/src/util/request-tracking.js +1 -1
- package/src/util/ssr-cache.js +11 -24
- package/src/util/to-gql-operation.js +44 -0
- package/src/util/types.js +31 -0
- package/src/__docs__/exports.intialize-cache.stories.mdx +0 -29
- package/src/__docs__/exports.remove-from-cache.stories.mdx +0 -25
- 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("#
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
});
|
package/src/util/data-error.js
CHANGED
|
@@ -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
|
+
}
|