@warp-drive-mirror/json-api 5.8.0-alpha.5 → 5.8.0-alpha.7
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/README.md +2 -2
- package/declarations/-private/cache.d.ts +9 -77
- package/declarations/index.d.ts +4 -0
- package/dist/index.js +209 -228
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<br>
|
|
27
27
|
|
|
28
28
|
<p align="center">
|
|
29
|
-
A {
|
|
29
|
+
A {json:api} Cache Implementation for <em>Warp</em><strong>Drive</strong>.
|
|
30
30
|
</p>
|
|
31
31
|
|
|
32
32
|
<br>
|
|
@@ -41,7 +41,7 @@ pnpm add -E @warp-drive-mirror/json-api
|
|
|
41
41
|
|
|
42
42
|
## Documentation
|
|
43
43
|
|
|
44
|
-
*Get Started* → [Guides](https://
|
|
44
|
+
*Get Started* → [Guides](https://warp-drive.io)
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
<br>
|
|
@@ -11,82 +11,12 @@ import type { CollectionResourceDataDocument, ResourceDocument, ResourceErrorDoc
|
|
|
11
11
|
import type { ApiError } from "@warp-drive-mirror/core/types/spec/error";
|
|
12
12
|
import type { CollectionResourceDocument, ExistingResourceObject, ResourceObject, SingleResourceDocument } from "@warp-drive-mirror/core/types/spec/json-api-raw";
|
|
13
13
|
/**
|
|
14
|
-
* ### JSONAPICache
|
|
15
|
-
*
|
|
16
14
|
* ```ts
|
|
17
15
|
* import { JSONAPICache } from '@warp-drive-mirror/json-api';
|
|
18
16
|
* ```
|
|
19
17
|
*
|
|
20
|
-
A {@link Cache} implementation tuned for [{json:api}](https://jsonapi.org/)
|
|
21
|
-
|
|
22
|
-
This format excels at simiplifying common complex problems around cache
|
|
23
|
-
consistency and information density. Because most API responses can be quickly
|
|
24
|
-
transformed into {json:api} format without losing any information, WarpDrive
|
|
25
|
-
recommends that most apps use this Cache implementation.
|
|
26
|
-
|
|
27
|
-
If a cache built to understand another format would do better for your app then
|
|
28
|
-
it just needs to follow the same interface.
|
|
29
|
-
|
|
30
|
-
Do you really need a cache? Are sunsets beautiful? Caching is what powers features like
|
|
31
|
-
immutability, mutation management, and allows ***Warp*Drive** to understand your relational
|
|
32
|
-
data.
|
|
33
|
-
|
|
34
|
-
Some caches are simple request/response maps. ***Warp*Drive**'s is not. The Cache deeply
|
|
35
|
-
understands the structure of your data, ensuring your data remains consistent both within
|
|
36
|
-
and across requests.
|
|
37
|
-
|
|
38
|
-
### Installation
|
|
39
|
-
|
|
40
|
-
::: code-group
|
|
41
|
-
|
|
42
|
-
```sh [pnpm]
|
|
43
|
-
pnpm add -E @warp-drive-mirror/core@latest @warp-drive-mirror/json-api@latest
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
```sh [npm]
|
|
47
|
-
npm add -E @warp-drive-mirror/core@latest @warp-drive-mirror/json-api@latest
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
```sh [yarn]
|
|
51
|
-
yarn add -E @warp-drive-mirror/core@latest @warp-drive-mirror/json-api@latest
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
```sh [bun]
|
|
55
|
-
bun add --exact @warp-drive-mirror/core@latest @warp-drive-mirror/json-api@latest
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
:::
|
|
59
|
-
|
|
60
|
-
### Setup
|
|
61
|
-
|
|
62
|
-
```ts [services/store.ts]
|
|
63
|
-
import { Fetch, RequestManager, Store } from '@warp-drive-mirror/core';
|
|
64
|
-
import {
|
|
65
|
-
registerDerivations,
|
|
66
|
-
SchemaService,
|
|
67
|
-
} from '@warp-drive-mirror/core/reactive';
|
|
68
|
-
import { CacheHandler } from '@warp-drive-mirror/core/store';
|
|
69
|
-
import type { CacheCapabilitiesManager } from '@warp-drive-mirror/core/types'; // [!code focus:2]
|
|
70
|
-
import { JSONAPICache } from '@warp-drive-mirror/json-api';
|
|
71
|
-
|
|
72
|
-
export default class AppStore extends Store {
|
|
73
|
-
|
|
74
|
-
requestManager = new RequestManager()
|
|
75
|
-
.use([Fetch])
|
|
76
|
-
.useCache(CacheHandler);
|
|
77
|
-
|
|
78
|
-
createSchemaService() {
|
|
79
|
-
const schema = new SchemaService();
|
|
80
|
-
registerDerivations(schema);
|
|
81
|
-
return schema;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
createCache(capabilities: CacheCapabilitiesManager) { // [!code focus:3]
|
|
85
|
-
return new JSONAPICache(capabilities);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
18
|
+
* A {@link Cache} implementation tuned for [{json:api}](https://jsonapi.org/)
|
|
19
|
+
*
|
|
90
20
|
* @categoryDescription Cache Management
|
|
91
21
|
* APIs for primary cache management functionality
|
|
92
22
|
* @categoryDescription Cache Forking
|
|
@@ -99,8 +29,8 @@ return new JSONAPICache(capabilities);
|
|
|
99
29
|
* APIs that support granular field level management of resource data
|
|
100
30
|
* @categoryDescription Resource State
|
|
101
31
|
* APIs that support managing Resource states
|
|
102
|
-
|
|
103
|
-
@public
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
104
34
|
*/
|
|
105
35
|
export declare class JSONAPICache implements Cache {
|
|
106
36
|
/**
|
|
@@ -325,7 +255,7 @@ export declare class JSONAPICache implements Cache {
|
|
|
325
255
|
* @category Resource Lifecycle
|
|
326
256
|
* @public
|
|
327
257
|
*/
|
|
328
|
-
willCommit(identifier: ResourceKey, _context: RequestContext | null): void;
|
|
258
|
+
willCommit(identifier: ResourceKey | ResourceKey[], _context: RequestContext | null): void;
|
|
329
259
|
/**
|
|
330
260
|
* [LIFECYCLE] Signals to the cache that a resource
|
|
331
261
|
* was successfully updated as part of a save transaction.
|
|
@@ -333,7 +263,9 @@ export declare class JSONAPICache implements Cache {
|
|
|
333
263
|
* @category Resource Lifecycle
|
|
334
264
|
* @public
|
|
335
265
|
*/
|
|
336
|
-
didCommit(committedIdentifier: ResourceKey, result: StructuredDataDocument<
|
|
266
|
+
didCommit(committedIdentifier: ResourceKey, result: StructuredDataDocument<SingleResourceDataDocument> | null): SingleResourceDataDocument;
|
|
267
|
+
didCommit(committedIdentifier: ResourceKey[], result: StructuredDataDocument<SingleResourceDataDocument> | null): SingleResourceDataDocument;
|
|
268
|
+
didCommit(committedIdentifier: ResourceKey[], result: StructuredDataDocument<CollectionResourceDataDocument> | null): CollectionResourceDataDocument;
|
|
337
269
|
/**
|
|
338
270
|
* [LIFECYCLE] Signals to the cache that a resource
|
|
339
271
|
* was update via a save transaction failed.
|
|
@@ -341,7 +273,7 @@ export declare class JSONAPICache implements Cache {
|
|
|
341
273
|
* @category Resource Lifecycle
|
|
342
274
|
* @public
|
|
343
275
|
*/
|
|
344
|
-
commitWasRejected(identifier: ResourceKey, errors?: ApiError[]): void;
|
|
276
|
+
commitWasRejected(identifier: ResourceKey | ResourceKey[], errors?: ApiError[]): void;
|
|
345
277
|
/**
|
|
346
278
|
* [LIFECYCLE] Signals to the cache that all data for a resource
|
|
347
279
|
* should be cleared.
|
package/declarations/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -361,7 +361,7 @@ class Reporter {
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
|
-
const contextStr = `${counts.error} errors and ${counts.warning} warnings found in the {
|
|
364
|
+
const contextStr = `${counts.error} errors and ${counts.warning} warnings found in the {json:api} document returned by ${this.contextDocument.request?.method} ${this.contextDocument.request?.url}`;
|
|
365
365
|
const errorString = contextStr + `\n\n` + errorLines.join('\n');
|
|
366
366
|
|
|
367
367
|
// eslint-disable-next-line no-console, @typescript-eslint/no-unused-expressions
|
|
@@ -449,10 +449,10 @@ function validateTopLevelDocumentMembers(reporter, doc) {
|
|
|
449
449
|
const extension = reporter.getExtension(extensionName);
|
|
450
450
|
extension(reporter, [key]);
|
|
451
451
|
} else {
|
|
452
|
-
reporter.warn([key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {
|
|
452
|
+
reporter.warn([key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {json:api} member`);
|
|
453
453
|
}
|
|
454
454
|
} else {
|
|
455
|
-
reporter.error([key], `Unrecognized top-level member. The data it provides is ignored as it is not a valid {
|
|
455
|
+
reporter.error([key], `Unrecognized top-level member. The data it provides is ignored as it is not a valid {json:api} member`);
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
458
|
}
|
|
@@ -462,24 +462,24 @@ function validateTopLevelDocumentMembers(reporter, doc) {
|
|
|
462
462
|
|
|
463
463
|
// 1. MUST have either `data`, `errors`, or `meta`
|
|
464
464
|
if (!('data' in doc) && !('errors' in doc) && !('meta' in doc)) {
|
|
465
|
-
reporter.error([], 'A {
|
|
465
|
+
reporter.error([], 'A {json:api} Document must contain one-of `data` `errors` or `meta`');
|
|
466
466
|
}
|
|
467
467
|
|
|
468
468
|
// 2. MUST NOT have both `data` and `errors`
|
|
469
469
|
if ('data' in doc && 'errors' in doc) {
|
|
470
|
-
reporter.error(['data'], 'A {
|
|
470
|
+
reporter.error(['data'], 'A {json:api} Document MUST NOT contain both `data` and `errors` members');
|
|
471
471
|
}
|
|
472
472
|
|
|
473
473
|
// 3. MUST NOT have both `included` and `errors`
|
|
474
474
|
// while not explicitly stated in the spec, this is a logical extension of the above rule
|
|
475
475
|
// since `included` is only valid when `data` is present.
|
|
476
476
|
if ('included' in doc && 'errors' in doc) {
|
|
477
|
-
reporter.error(['included'], 'A {
|
|
477
|
+
reporter.error(['included'], 'A {json:api} Document MUST NOT contain both `included` and `errors` members');
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
// 4. MUST NOT have `included` if `data` is not present
|
|
481
481
|
if ('included' in doc && !('data' in doc)) {
|
|
482
|
-
reporter.error(['included'], 'A {
|
|
482
|
+
reporter.error(['included'], 'A {json:api} Document MUST NOT contain `included` if `data` is not present');
|
|
483
483
|
}
|
|
484
484
|
|
|
485
485
|
// 5. MUST NOT have `included` if `data` is null
|
|
@@ -489,7 +489,7 @@ function validateTopLevelDocumentMembers(reporter, doc) {
|
|
|
489
489
|
if ('included' in doc && doc.data === null) {
|
|
490
490
|
const isMaybeDelete = reporter.contextDocument.request?.method?.toUpperCase() === 'DELETE' || reporter.contextDocument.request?.op === 'deleteRecord';
|
|
491
491
|
const method = !reporter.strict.linkage && isMaybeDelete ? 'warn' : 'error';
|
|
492
|
-
reporter[method](['included'], 'A {
|
|
492
|
+
reporter[method](['included'], 'A {json:api} Document MUST NOT contain `included` if `data` is null');
|
|
493
493
|
}
|
|
494
494
|
|
|
495
495
|
// Simple Validation of Top-Level Members
|
|
@@ -576,7 +576,7 @@ function validateLinks(reporter, doc, type, path = ['links']) {
|
|
|
576
576
|
const keys = Object.keys(links);
|
|
577
577
|
for (const key of keys) {
|
|
578
578
|
if (!VALID_TOP_LEVEL_LINKS.includes(key)) {
|
|
579
|
-
reporter.warn([...path, key], `Unrecognized top-level link. The data it provides may be ignored as it is not a valid {
|
|
579
|
+
reporter.warn([...path, key], `Unrecognized top-level link. The data it provides may be ignored as it is not a valid {json:api} link for a ${type}`);
|
|
580
580
|
}
|
|
581
581
|
// links may be either a string or an object with an href property or null
|
|
582
582
|
if (links[key] === null) ; else if (typeof links[key] === 'string') {
|
|
@@ -738,7 +738,7 @@ function validateTopLevelResourceShape(reporter, resource, path) {
|
|
|
738
738
|
const extension = reporter.getExtension(extensionName);
|
|
739
739
|
extension(reporter, [...path, key]);
|
|
740
740
|
} else {
|
|
741
|
-
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {
|
|
741
|
+
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {json:api} ResourceObject member`);
|
|
742
742
|
}
|
|
743
743
|
} else {
|
|
744
744
|
// check if this is an attribute or relationship
|
|
@@ -767,7 +767,7 @@ function validateTopLevelResourceShape(reporter, resource, path) {
|
|
|
767
767
|
}
|
|
768
768
|
}
|
|
769
769
|
}
|
|
770
|
-
reporter.error([...path, key], `Unrecognized ResourceObject member. The data it provides is ignored as it is not a valid {
|
|
770
|
+
reporter.error([...path, key], `Unrecognized ResourceObject member. The data it provides is ignored as it is not a valid {json:api} ResourceObject member.${didYouMean}`);
|
|
771
771
|
}
|
|
772
772
|
}
|
|
773
773
|
}
|
|
@@ -801,7 +801,7 @@ function validateResourceAttributes(reporter, type, resource, path) {
|
|
|
801
801
|
const extension = reporter.getExtension(extensionName);
|
|
802
802
|
extension(reporter, [...path, key]);
|
|
803
803
|
} else {
|
|
804
|
-
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {
|
|
804
|
+
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {json:api} ResourceObject member`);
|
|
805
805
|
}
|
|
806
806
|
} else {
|
|
807
807
|
const method = reporter.strict.unknownAttribute ? 'error' : 'warn';
|
|
@@ -835,7 +835,7 @@ function validateResourceRelationships(reporter, type, resource, path) {
|
|
|
835
835
|
const extension = reporter.getExtension(extensionName);
|
|
836
836
|
extension(reporter, [...path, key]);
|
|
837
837
|
} else {
|
|
838
|
-
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {
|
|
838
|
+
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {json:api} ResourceObject member`);
|
|
839
839
|
}
|
|
840
840
|
} else {
|
|
841
841
|
const method = reporter.strict.unknownRelationship ? 'error' : 'warn';
|
|
@@ -955,82 +955,12 @@ function makeCache() {
|
|
|
955
955
|
}
|
|
956
956
|
|
|
957
957
|
/**
|
|
958
|
-
* ### JSONAPICache
|
|
959
|
-
*
|
|
960
958
|
* ```ts
|
|
961
959
|
* import { JSONAPICache } from '@warp-drive-mirror/json-api';
|
|
962
960
|
* ```
|
|
963
961
|
*
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
This format excels at simiplifying common complex problems around cache
|
|
967
|
-
consistency and information density. Because most API responses can be quickly
|
|
968
|
-
transformed into {json:api} format without losing any information, WarpDrive
|
|
969
|
-
recommends that most apps use this Cache implementation.
|
|
970
|
-
|
|
971
|
-
If a cache built to understand another format would do better for your app then
|
|
972
|
-
it just needs to follow the same interface.
|
|
973
|
-
|
|
974
|
-
Do you really need a cache? Are sunsets beautiful? Caching is what powers features like
|
|
975
|
-
immutability, mutation management, and allows ***Warp*Drive** to understand your relational
|
|
976
|
-
data.
|
|
977
|
-
|
|
978
|
-
Some caches are simple request/response maps. ***Warp*Drive**'s is not. The Cache deeply
|
|
979
|
-
understands the structure of your data, ensuring your data remains consistent both within
|
|
980
|
-
and across requests.
|
|
981
|
-
|
|
982
|
-
### Installation
|
|
983
|
-
|
|
984
|
-
::: code-group
|
|
985
|
-
|
|
986
|
-
```sh [pnpm]
|
|
987
|
-
pnpm add -E @warp-drive-mirror/core@latest @warp-drive-mirror/json-api@latest
|
|
988
|
-
```
|
|
989
|
-
|
|
990
|
-
```sh [npm]
|
|
991
|
-
npm add -E @warp-drive-mirror/core@latest @warp-drive-mirror/json-api@latest
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
```sh [yarn]
|
|
995
|
-
yarn add -E @warp-drive-mirror/core@latest @warp-drive-mirror/json-api@latest
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
```sh [bun]
|
|
999
|
-
bun add --exact @warp-drive-mirror/core@latest @warp-drive-mirror/json-api@latest
|
|
1000
|
-
```
|
|
1001
|
-
|
|
1002
|
-
:::
|
|
1003
|
-
|
|
1004
|
-
### Setup
|
|
1005
|
-
|
|
1006
|
-
```ts [services/store.ts]
|
|
1007
|
-
import { Fetch, RequestManager, Store } from '@warp-drive-mirror/core';
|
|
1008
|
-
import {
|
|
1009
|
-
registerDerivations,
|
|
1010
|
-
SchemaService,
|
|
1011
|
-
} from '@warp-drive-mirror/core/reactive';
|
|
1012
|
-
import { CacheHandler } from '@warp-drive-mirror/core/store';
|
|
1013
|
-
import type { CacheCapabilitiesManager } from '@warp-drive-mirror/core/types'; // [!code focus:2]
|
|
1014
|
-
import { JSONAPICache } from '@warp-drive-mirror/json-api';
|
|
1015
|
-
|
|
1016
|
-
export default class AppStore extends Store {
|
|
1017
|
-
|
|
1018
|
-
requestManager = new RequestManager()
|
|
1019
|
-
.use([Fetch])
|
|
1020
|
-
.useCache(CacheHandler);
|
|
1021
|
-
|
|
1022
|
-
createSchemaService() {
|
|
1023
|
-
const schema = new SchemaService();
|
|
1024
|
-
registerDerivations(schema);
|
|
1025
|
-
return schema;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
createCache(capabilities: CacheCapabilitiesManager) { // [!code focus:3]
|
|
1029
|
-
return new JSONAPICache(capabilities);
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
```
|
|
1033
|
-
|
|
962
|
+
* A {@link Cache} implementation tuned for [{json:api}](https://jsonapi.org/)
|
|
963
|
+
*
|
|
1034
964
|
* @categoryDescription Cache Management
|
|
1035
965
|
* APIs for primary cache management functionality
|
|
1036
966
|
* @categoryDescription Cache Forking
|
|
@@ -1043,10 +973,9 @@ function makeCache() {
|
|
|
1043
973
|
* APIs that support granular field level management of resource data
|
|
1044
974
|
* @categoryDescription Resource State
|
|
1045
975
|
* APIs that support managing Resource states
|
|
1046
|
-
|
|
1047
|
-
|
|
976
|
+
*
|
|
977
|
+
* @public
|
|
1048
978
|
*/
|
|
1049
|
-
|
|
1050
979
|
class JSONAPICache {
|
|
1051
980
|
/**
|
|
1052
981
|
* The Cache Version that this implementation implements.
|
|
@@ -1689,50 +1618,12 @@ class JSONAPICache {
|
|
|
1689
1618
|
* @public
|
|
1690
1619
|
*/
|
|
1691
1620
|
willCommit(identifier, _context) {
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
if we have multiple saves in flight at once then
|
|
1696
|
-
we have information loss no matter what. This
|
|
1697
|
-
attempts to lose the least information.
|
|
1698
|
-
If we were to clear inflightAttrs, previous requests
|
|
1699
|
-
would not be able to use it during their didCommit.
|
|
1700
|
-
If we upsert inflightattrs, previous requests incorrectly
|
|
1701
|
-
see more recent inflight changes as part of their own and
|
|
1702
|
-
will incorrectly mark the new state as the correct remote state.
|
|
1703
|
-
We choose this latter behavior to avoid accidentally removing
|
|
1704
|
-
earlier changes.
|
|
1705
|
-
If apps do not want this behavior they can either
|
|
1706
|
-
- chain save requests serially vs allowing concurrent saves
|
|
1707
|
-
- move to using a request handler that caches the inflight state
|
|
1708
|
-
on a per-request basis
|
|
1709
|
-
- change their save requests to only send a "PATCH" instead of a "PUT"
|
|
1710
|
-
so that only latest changes are involved in each request, and then also
|
|
1711
|
-
ensure that the API or their handler reflects only those changes back
|
|
1712
|
-
for upsert into the cache.
|
|
1713
|
-
*/
|
|
1714
|
-
if (cached.inflightAttrs) {
|
|
1715
|
-
if (cached.localAttrs) {
|
|
1716
|
-
Object.assign(cached.inflightAttrs, cached.localAttrs);
|
|
1621
|
+
if (Array.isArray(identifier)) {
|
|
1622
|
+
for (const key of identifier) {
|
|
1623
|
+
willCommit(this, key);
|
|
1717
1624
|
}
|
|
1718
1625
|
} else {
|
|
1719
|
-
|
|
1720
|
-
}
|
|
1721
|
-
cached.localAttrs = null;
|
|
1722
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
1723
|
-
if (macroCondition(!getGlobalConfig().WarpDriveMirror.deprecations.DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE)) {
|
|
1724
|
-
// save off info about saved relationships
|
|
1725
|
-
const fields = getCacheFields(this, identifier);
|
|
1726
|
-
fields.forEach((schema, name) => {
|
|
1727
|
-
if (schema.kind === 'belongsTo') {
|
|
1728
|
-
if (this.__graph._isDirty(identifier, name)) {
|
|
1729
|
-
const relationshipData = this.__graph.getData(identifier, name);
|
|
1730
|
-
const inFlight = cached.inflightRelationships = cached.inflightRelationships || Object.create(null);
|
|
1731
|
-
inFlight[name] = relationshipData;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
});
|
|
1735
|
-
}
|
|
1626
|
+
willCommit(this, identifier);
|
|
1736
1627
|
}
|
|
1737
1628
|
}
|
|
1738
1629
|
|
|
@@ -1743,9 +1634,10 @@ class JSONAPICache {
|
|
|
1743
1634
|
* @category Resource Lifecycle
|
|
1744
1635
|
* @public
|
|
1745
1636
|
*/
|
|
1637
|
+
|
|
1746
1638
|
didCommit(committedIdentifier, result) {
|
|
1747
1639
|
const payload = result ? result.content : null;
|
|
1748
|
-
const operation = result
|
|
1640
|
+
const operation = result?.request?.op ?? null;
|
|
1749
1641
|
const data = payload && payload.data;
|
|
1750
1642
|
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
1751
1643
|
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
@@ -1759,101 +1651,45 @@ class JSONAPICache {
|
|
|
1759
1651
|
}
|
|
1760
1652
|
}
|
|
1761
1653
|
}
|
|
1762
|
-
|
|
1654
|
+
const responseIsCollection = Array.isArray(data);
|
|
1655
|
+
const hasMultipleIdentifiers = Array.isArray(committedIdentifier) && committedIdentifier.length > 1;
|
|
1656
|
+
if (Array.isArray(committedIdentifier)) {
|
|
1657
|
+
// if we get back an array of primary data, we treat each
|
|
1658
|
+
// entry as a separate commit for each identifier
|
|
1763
1659
|
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1764
1660
|
if (!test) {
|
|
1765
|
-
throw new Error(`
|
|
1661
|
+
throw new Error(`Expected the array of primary data to match the array of committed identifiers`);
|
|
1766
1662
|
}
|
|
1767
|
-
})(committedIdentifier.
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
const existingId = committedIdentifier.id;
|
|
1773
|
-
const identifier = operation !== 'deleteRecord' && data ? cacheKeyManager.updateRecordIdentifier(committedIdentifier, data) : committedIdentifier;
|
|
1774
|
-
const cached = this.__peek(identifier, false);
|
|
1775
|
-
if (cached.isDeleted) {
|
|
1776
|
-
this.__graph.push({
|
|
1777
|
-
op: 'deleteRecord',
|
|
1778
|
-
record: identifier,
|
|
1779
|
-
isNew: false
|
|
1780
|
-
});
|
|
1781
|
-
cached.isDeletionCommitted = true;
|
|
1782
|
-
this._capabilities.notifyChange(identifier, 'removed', null);
|
|
1783
|
-
// TODO @runspired should we early exit here?
|
|
1784
|
-
}
|
|
1785
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
1786
|
-
if (cached.isNew && !identifier.id && (typeof data?.id !== 'string' || data.id.length > 0)) {
|
|
1787
|
-
const error = new Error(`Expected an id ${String(identifier)} in response ${JSON.stringify(data)}`);
|
|
1788
|
-
//@ts-expect-error
|
|
1789
|
-
error.isAdapterError = true;
|
|
1790
|
-
//@ts-expect-error
|
|
1791
|
-
error.code = 'InvalidError';
|
|
1792
|
-
throw error;
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
const fields = getCacheFields(this, identifier);
|
|
1796
|
-
cached.isNew = false;
|
|
1797
|
-
let newCanonicalAttributes;
|
|
1798
|
-
if (data) {
|
|
1799
|
-
if (data.id && !cached.id) {
|
|
1800
|
-
cached.id = data.id;
|
|
1801
|
-
}
|
|
1802
|
-
if (identifier === committedIdentifier && identifier.id !== existingId) {
|
|
1803
|
-
this._capabilities.notifyChange(identifier, 'identity', null);
|
|
1804
|
-
}
|
|
1805
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1806
|
-
if (!test) {
|
|
1807
|
-
throw new Error(`Expected the ID received for the primary '${identifier.type}' resource being saved to match the current id '${cached.id}' but received '${identifier.id}'.`);
|
|
1663
|
+
})(!hasMultipleIdentifiers || !responseIsCollection || data.length === committedIdentifier.length) : {};
|
|
1664
|
+
if (responseIsCollection) {
|
|
1665
|
+
for (let i = 0; i < committedIdentifier.length; i++) {
|
|
1666
|
+
const identifier = committedIdentifier[i];
|
|
1667
|
+
didCommit(this, identifier, data[i] ?? null, operation);
|
|
1808
1668
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
fields.forEach((field, name) => {
|
|
1816
|
-
if (field.kind === 'belongsTo') {
|
|
1817
|
-
const relationshipData = data.relationships[name]?.data;
|
|
1818
|
-
if (relationshipData !== undefined) {
|
|
1819
|
-
const inFlightData = cached.inflightRelationships?.[name];
|
|
1820
|
-
if (!inFlightData || !('data' in inFlightData)) {
|
|
1821
|
-
return;
|
|
1822
|
-
}
|
|
1823
|
-
const actualData = relationshipData ? this._capabilities.cacheKeyManager.getOrCreateRecordIdentifier(relationshipData) : null;
|
|
1824
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1825
|
-
if (!test) {
|
|
1826
|
-
throw new Error(`Expected the resource relationship '<${identifier.type}>.${name}' on ${identifier.lid} to be saved as ${inFlightData.data ? inFlightData.data.lid : '<null>'} but it was saved as ${actualData ? actualData.lid : '<null>'}`);
|
|
1827
|
-
}
|
|
1828
|
-
})(inFlightData.data === actualData) : {};
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
});
|
|
1832
|
-
cached.inflightRelationships = null;
|
|
1833
|
-
}
|
|
1669
|
+
// but if we get back no data or a single entry, we apply
|
|
1670
|
+
// the change back to the original identifier
|
|
1671
|
+
} else {
|
|
1672
|
+
for (let i = 0; i < committedIdentifier.length; i++) {
|
|
1673
|
+
const identifier = committedIdentifier[i];
|
|
1674
|
+
didCommit(this, identifier, i === 0 ? data : null, operation);
|
|
1834
1675
|
}
|
|
1835
|
-
setupRelationships(this.__graph, fields, identifier, data);
|
|
1836
1676
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
const changedKeys = newCanonicalAttributes && calculateChangedKeys(cached, newCanonicalAttributes, fields);
|
|
1840
|
-
cached.remoteAttrs = Object.assign(cached.remoteAttrs || Object.create(null), cached.inflightAttrs, newCanonicalAttributes);
|
|
1841
|
-
cached.inflightAttrs = null;
|
|
1842
|
-
patchLocalAttributes(cached, changedKeys);
|
|
1843
|
-
if (cached.errors) {
|
|
1844
|
-
cached.errors = null;
|
|
1845
|
-
this._capabilities.notifyChange(identifier, 'errors', null);
|
|
1677
|
+
} else {
|
|
1678
|
+
didCommit(this, committedIdentifier, data, operation);
|
|
1846
1679
|
}
|
|
1847
|
-
if (changedKeys?.size) notifyAttributes(this._capabilities, identifier, changedKeys);
|
|
1848
|
-
this._capabilities.notifyChange(identifier, 'state', null);
|
|
1849
1680
|
const included = payload && payload.included;
|
|
1681
|
+
const {
|
|
1682
|
+
cacheKeyManager
|
|
1683
|
+
} = this._capabilities;
|
|
1850
1684
|
if (included) {
|
|
1851
1685
|
for (let i = 0, length = included.length; i < length; i++) {
|
|
1852
1686
|
putOne(this, cacheKeyManager, included[i]);
|
|
1853
1687
|
}
|
|
1854
1688
|
}
|
|
1855
|
-
return {
|
|
1856
|
-
data:
|
|
1689
|
+
return hasMultipleIdentifiers && responseIsCollection ? {
|
|
1690
|
+
data: committedIdentifier
|
|
1691
|
+
} : {
|
|
1692
|
+
data: Array.isArray(committedIdentifier) ? committedIdentifier[0] : committedIdentifier
|
|
1857
1693
|
};
|
|
1858
1694
|
}
|
|
1859
1695
|
|
|
@@ -1865,23 +1701,13 @@ class JSONAPICache {
|
|
|
1865
1701
|
* @public
|
|
1866
1702
|
*/
|
|
1867
1703
|
commitWasRejected(identifier, errors) {
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
if (keys.length > 0) {
|
|
1872
|
-
const attrs = cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
1873
|
-
for (let i = 0; i < keys.length; i++) {
|
|
1874
|
-
if (attrs[keys[i]] === undefined) {
|
|
1875
|
-
attrs[keys[i]] = cached.inflightAttrs[keys[i]];
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1704
|
+
if (Array.isArray(identifier)) {
|
|
1705
|
+
for (let i = 0; i < identifier.length; i++) {
|
|
1706
|
+
commitDidError(this, identifier[i], errors && i === 0 ? errors : null);
|
|
1878
1707
|
}
|
|
1879
|
-
|
|
1880
|
-
}
|
|
1881
|
-
if (errors) {
|
|
1882
|
-
cached.errors = errors;
|
|
1708
|
+
return;
|
|
1883
1709
|
}
|
|
1884
|
-
this
|
|
1710
|
+
return commitDidError(this, identifier, errors || null);
|
|
1885
1711
|
}
|
|
1886
1712
|
|
|
1887
1713
|
/**
|
|
@@ -3233,5 +3059,160 @@ function getCacheFields(cache, identifier) {
|
|
|
3233
3059
|
// the model schema service cannot process fields that are not cache fields
|
|
3234
3060
|
return cache._capabilities.schema.fields(identifier);
|
|
3235
3061
|
}
|
|
3062
|
+
function commitDidError(cache, identifier, errors) {
|
|
3063
|
+
const cached = cache.__peek(identifier, false);
|
|
3064
|
+
if (cached.inflightAttrs) {
|
|
3065
|
+
const keys = Object.keys(cached.inflightAttrs);
|
|
3066
|
+
if (keys.length > 0) {
|
|
3067
|
+
const attrs = cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
3068
|
+
for (let i = 0; i < keys.length; i++) {
|
|
3069
|
+
if (attrs[keys[i]] === undefined) {
|
|
3070
|
+
attrs[keys[i]] = cached.inflightAttrs[keys[i]];
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
cached.inflightAttrs = null;
|
|
3075
|
+
}
|
|
3076
|
+
if (errors) {
|
|
3077
|
+
cached.errors = errors;
|
|
3078
|
+
}
|
|
3079
|
+
cache._capabilities.notifyChange(identifier, 'errors', null);
|
|
3080
|
+
}
|
|
3081
|
+
function didCommit(cache, committedIdentifier, data, op) {
|
|
3082
|
+
if (!data) {
|
|
3083
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3084
|
+
if (!test) {
|
|
3085
|
+
throw new Error(`Your ${committedIdentifier.type} record was saved to the server, but the response does not have an id and no id has been set client side. Records must have ids. Please update the server response to provide an id in the response or generate the id on the client side either before saving the record or while normalizing the response.`);
|
|
3086
|
+
}
|
|
3087
|
+
})(committedIdentifier.id) : {};
|
|
3088
|
+
}
|
|
3089
|
+
const {
|
|
3090
|
+
cacheKeyManager
|
|
3091
|
+
} = cache._capabilities;
|
|
3092
|
+
const existingId = committedIdentifier.id;
|
|
3093
|
+
const identifier = op !== 'deleteRecord' && data ? cacheKeyManager.updateRecordIdentifier(committedIdentifier, data) : committedIdentifier;
|
|
3094
|
+
const cached = cache.__peek(identifier, false);
|
|
3095
|
+
if (cached.isDeleted) {
|
|
3096
|
+
cache.__graph.push({
|
|
3097
|
+
op: 'deleteRecord',
|
|
3098
|
+
record: identifier,
|
|
3099
|
+
isNew: false
|
|
3100
|
+
});
|
|
3101
|
+
cached.isDeletionCommitted = true;
|
|
3102
|
+
cache._capabilities.notifyChange(identifier, 'removed', null);
|
|
3103
|
+
// TODO @runspired should we early exit here?
|
|
3104
|
+
}
|
|
3105
|
+
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3106
|
+
if (cached.isNew && !identifier.id && (typeof data?.id !== 'string' || data.id.length > 0)) {
|
|
3107
|
+
const error = new Error(`Expected an id ${String(identifier)} in response ${JSON.stringify(data)}`);
|
|
3108
|
+
//@ts-expect-error
|
|
3109
|
+
error.isAdapterError = true;
|
|
3110
|
+
//@ts-expect-error
|
|
3111
|
+
error.code = 'InvalidError';
|
|
3112
|
+
throw error;
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
const fields = getCacheFields(cache, identifier);
|
|
3116
|
+
cached.isNew = false;
|
|
3117
|
+
let newCanonicalAttributes;
|
|
3118
|
+
if (data) {
|
|
3119
|
+
if (data.id && !cached.id) {
|
|
3120
|
+
cached.id = data.id;
|
|
3121
|
+
}
|
|
3122
|
+
if (identifier === committedIdentifier && identifier.id !== existingId) {
|
|
3123
|
+
cache._capabilities.notifyChange(identifier, 'identity', null);
|
|
3124
|
+
}
|
|
3125
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3126
|
+
if (!test) {
|
|
3127
|
+
throw new Error(`Expected the ID received for the primary '${identifier.type}' resource being saved to match the current id '${cached.id}' but received '${identifier.id}'.`);
|
|
3128
|
+
}
|
|
3129
|
+
})(identifier.id === cached.id) : {};
|
|
3130
|
+
if (data.relationships) {
|
|
3131
|
+
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3132
|
+
if (macroCondition(!getGlobalConfig().WarpDriveMirror.deprecations.DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE)) {
|
|
3133
|
+
// assert against bad API behavior where a belongsTo relationship
|
|
3134
|
+
// is saved but the return payload indicates a different final state.
|
|
3135
|
+
fields.forEach((field, name) => {
|
|
3136
|
+
if (field.kind === 'belongsTo') {
|
|
3137
|
+
const relationshipData = data.relationships[name]?.data;
|
|
3138
|
+
if (relationshipData !== undefined) {
|
|
3139
|
+
const inFlightData = cached.inflightRelationships?.[name];
|
|
3140
|
+
if (!inFlightData || !('data' in inFlightData)) {
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
const actualData = relationshipData ? cache._capabilities.cacheKeyManager.getOrCreateRecordIdentifier(relationshipData) : null;
|
|
3144
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3145
|
+
if (!test) {
|
|
3146
|
+
throw new Error(`Expected the resource relationship '<${identifier.type}>.${name}' on ${identifier.lid} to be saved as ${inFlightData.data ? inFlightData.data.lid : '<null>'} but it was saved as ${actualData ? actualData.lid : '<null>'}`);
|
|
3147
|
+
}
|
|
3148
|
+
})(inFlightData.data === actualData) : {};
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
});
|
|
3152
|
+
cached.inflightRelationships = null;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
setupRelationships(cache.__graph, fields, identifier, data);
|
|
3156
|
+
}
|
|
3157
|
+
newCanonicalAttributes = data.attributes;
|
|
3158
|
+
}
|
|
3159
|
+
const changedKeys = newCanonicalAttributes && calculateChangedKeys(cached, newCanonicalAttributes, fields);
|
|
3160
|
+
cached.remoteAttrs = Object.assign(cached.remoteAttrs || Object.create(null), cached.inflightAttrs, newCanonicalAttributes);
|
|
3161
|
+
cached.inflightAttrs = null;
|
|
3162
|
+
patchLocalAttributes(cached, changedKeys);
|
|
3163
|
+
if (cached.errors) {
|
|
3164
|
+
cached.errors = null;
|
|
3165
|
+
cache._capabilities.notifyChange(identifier, 'errors', null);
|
|
3166
|
+
}
|
|
3167
|
+
if (changedKeys?.size) notifyAttributes(cache._capabilities, identifier, changedKeys);
|
|
3168
|
+
cache._capabilities.notifyChange(identifier, 'state', null);
|
|
3169
|
+
}
|
|
3170
|
+
function willCommit(cache, identifier) {
|
|
3171
|
+
const cached = cache.__peek(identifier, false);
|
|
3172
|
+
|
|
3173
|
+
/*
|
|
3174
|
+
if we have multiple saves in flight at once then
|
|
3175
|
+
we have information loss no matter what. This
|
|
3176
|
+
attempts to lose the least information.
|
|
3177
|
+
If we were to clear inflightAttrs, previous requests
|
|
3178
|
+
would not be able to use it during their didCommit.
|
|
3179
|
+
If we upsert inflightattrs, previous requests incorrectly
|
|
3180
|
+
see more recent inflight changes as part of their own and
|
|
3181
|
+
will incorrectly mark the new state as the correct remote state.
|
|
3182
|
+
We choose this latter behavior to avoid accidentally removing
|
|
3183
|
+
earlier changes.
|
|
3184
|
+
If apps do not want this behavior they can either
|
|
3185
|
+
- chain save requests serially vs allowing concurrent saves
|
|
3186
|
+
- move to using a request handler that caches the inflight state
|
|
3187
|
+
on a per-request basis
|
|
3188
|
+
- change their save requests to only send a "PATCH" instead of a "PUT"
|
|
3189
|
+
so that only latest changes are involved in each request, and then also
|
|
3190
|
+
ensure that the API or their handler reflects only those changes back
|
|
3191
|
+
for upsert into the cache.
|
|
3192
|
+
*/
|
|
3193
|
+
if (cached.inflightAttrs) {
|
|
3194
|
+
if (cached.localAttrs) {
|
|
3195
|
+
Object.assign(cached.inflightAttrs, cached.localAttrs);
|
|
3196
|
+
}
|
|
3197
|
+
} else {
|
|
3198
|
+
cached.inflightAttrs = cached.localAttrs;
|
|
3199
|
+
}
|
|
3200
|
+
cached.localAttrs = null;
|
|
3201
|
+
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3202
|
+
if (macroCondition(!getGlobalConfig().WarpDriveMirror.deprecations.DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE)) {
|
|
3203
|
+
// save off info about saved relationships
|
|
3204
|
+
const fields = getCacheFields(cache, identifier);
|
|
3205
|
+
fields.forEach((schema, name) => {
|
|
3206
|
+
if (schema.kind === 'belongsTo') {
|
|
3207
|
+
if (cache.__graph._isDirty(identifier, name)) {
|
|
3208
|
+
const relationshipData = cache.__graph.getData(identifier, name);
|
|
3209
|
+
const inFlight = cached.inflightRelationships = cached.inflightRelationships || Object.create(null);
|
|
3210
|
+
inFlight[name] = relationshipData;
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
});
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3236
3217
|
|
|
3237
3218
|
export { JSONAPICache };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warp-drive-mirror/json-api",
|
|
3
|
-
"version": "5.8.0-alpha.
|
|
4
|
-
"description": "A {
|
|
3
|
+
"version": "5.8.0-alpha.7",
|
|
4
|
+
"description": "A {json:api} Cache Implementation for WarpDrive",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ember-addon"
|
|
7
7
|
],
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@warp-drive-mirror/core": "5.8.0-alpha.
|
|
35
|
+
"@warp-drive-mirror/core": "5.8.0-alpha.7"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@embroider/macros": "^1.18.1",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"@babel/plugin-transform-typescript": "^7.28.0",
|
|
45
45
|
"@babel/preset-typescript": "^7.27.1",
|
|
46
46
|
"@types/json-to-ast": "^2.1.4",
|
|
47
|
-
"@warp-drive/internal-config": "5.8.0-alpha.
|
|
48
|
-
"@warp-drive-mirror/core": "5.8.0-alpha.
|
|
47
|
+
"@warp-drive/internal-config": "5.8.0-alpha.7",
|
|
48
|
+
"@warp-drive-mirror/core": "5.8.0-alpha.7",
|
|
49
49
|
"decorator-transforms": "^2.3.0",
|
|
50
50
|
"expect-type": "^1.2.2",
|
|
51
51
|
"typescript": "^5.9.2",
|