@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.
- package/CHANGELOG.md +32 -0
- package/dist/es/index.js +286 -107
- package/dist/index.js +1089 -713
- package/package.json +1 -1
- 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 +58 -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 +13 -31
- 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,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("#
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
});
|
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
|
+
}
|