@warp-drive-mirror/json-api 5.8.0-alpha.4 → 5.8.0-alpha.40
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 +17 -30
- package/declarations/-private/cache.d.ts +9 -77
- package/declarations/index.d.ts +4 -0
- package/dist/index.js +225 -237
- package/dist/unpkg/dev/index.js +3338 -0
- package/dist/unpkg/dev-deprecated/index.js +3299 -0
- package/dist/unpkg/prod/index.js +1872 -0
- package/dist/unpkg/prod-deprecated/index.js +1872 -0
- package/logos/README.md +2 -2
- package/logos/logo-yellow-slab.svg +1 -0
- package/logos/word-mark-black.svg +1 -0
- package/logos/word-mark-white.svg +1 -0
- package/package.json +13 -5
- package/logos/NCC-1701-a-blue.svg +0 -4
- package/logos/NCC-1701-a-gold.svg +0 -4
- package/logos/NCC-1701-a-gold_100.svg +0 -1
- package/logos/NCC-1701-a-gold_base-64.txt +0 -1
- package/logos/NCC-1701-a.svg +0 -4
- package/logos/docs-badge.svg +0 -2
- package/logos/ember-data-logo-dark.svg +0 -12
- package/logos/ember-data-logo-light.svg +0 -12
- package/logos/social1.png +0 -0
- package/logos/social2.png +0 -0
- package/logos/warp-drive-logo-dark.svg +0 -4
- package/logos/warp-drive-logo-gold.svg +0 -4
package/dist/index.js
CHANGED
|
@@ -196,7 +196,10 @@ class Reporter {
|
|
|
196
196
|
const allFields = this.schema.fields({
|
|
197
197
|
type
|
|
198
198
|
});
|
|
199
|
-
const
|
|
199
|
+
const allCacheFields = this.schema.cacheFields?.({
|
|
200
|
+
type
|
|
201
|
+
}) ?? allFields;
|
|
202
|
+
const attrs = Array.from(allCacheFields.values()).filter(isRemoteField).map(v => v.name);
|
|
200
203
|
this._fieldFilters.set(type, new Fuse(attrs));
|
|
201
204
|
}
|
|
202
205
|
const result = this._fieldFilters.get(type).search(field);
|
|
@@ -361,7 +364,7 @@ class Reporter {
|
|
|
361
364
|
}
|
|
362
365
|
}
|
|
363
366
|
}
|
|
364
|
-
const contextStr = `${counts.error} errors and ${counts.warning} warnings found in the {
|
|
367
|
+
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
368
|
const errorString = contextStr + `\n\n` + errorLines.join('\n');
|
|
366
369
|
|
|
367
370
|
// eslint-disable-next-line no-console, @typescript-eslint/no-unused-expressions
|
|
@@ -449,10 +452,10 @@ function validateTopLevelDocumentMembers(reporter, doc) {
|
|
|
449
452
|
const extension = reporter.getExtension(extensionName);
|
|
450
453
|
extension(reporter, [key]);
|
|
451
454
|
} else {
|
|
452
|
-
reporter.warn([key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {
|
|
455
|
+
reporter.warn([key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {json:api} member`);
|
|
453
456
|
}
|
|
454
457
|
} else {
|
|
455
|
-
reporter.error([key], `Unrecognized top-level member. The data it provides is ignored as it is not a valid {
|
|
458
|
+
reporter.error([key], `Unrecognized top-level member. The data it provides is ignored as it is not a valid {json:api} member`);
|
|
456
459
|
}
|
|
457
460
|
}
|
|
458
461
|
}
|
|
@@ -462,24 +465,24 @@ function validateTopLevelDocumentMembers(reporter, doc) {
|
|
|
462
465
|
|
|
463
466
|
// 1. MUST have either `data`, `errors`, or `meta`
|
|
464
467
|
if (!('data' in doc) && !('errors' in doc) && !('meta' in doc)) {
|
|
465
|
-
reporter.error([], 'A {
|
|
468
|
+
reporter.error([], 'A {json:api} Document must contain one-of `data` `errors` or `meta`');
|
|
466
469
|
}
|
|
467
470
|
|
|
468
471
|
// 2. MUST NOT have both `data` and `errors`
|
|
469
472
|
if ('data' in doc && 'errors' in doc) {
|
|
470
|
-
reporter.error(['data'], 'A {
|
|
473
|
+
reporter.error(['data'], 'A {json:api} Document MUST NOT contain both `data` and `errors` members');
|
|
471
474
|
}
|
|
472
475
|
|
|
473
476
|
// 3. MUST NOT have both `included` and `errors`
|
|
474
477
|
// while not explicitly stated in the spec, this is a logical extension of the above rule
|
|
475
478
|
// since `included` is only valid when `data` is present.
|
|
476
479
|
if ('included' in doc && 'errors' in doc) {
|
|
477
|
-
reporter.error(['included'], 'A {
|
|
480
|
+
reporter.error(['included'], 'A {json:api} Document MUST NOT contain both `included` and `errors` members');
|
|
478
481
|
}
|
|
479
482
|
|
|
480
483
|
// 4. MUST NOT have `included` if `data` is not present
|
|
481
484
|
if ('included' in doc && !('data' in doc)) {
|
|
482
|
-
reporter.error(['included'], 'A {
|
|
485
|
+
reporter.error(['included'], 'A {json:api} Document MUST NOT contain `included` if `data` is not present');
|
|
483
486
|
}
|
|
484
487
|
|
|
485
488
|
// 5. MUST NOT have `included` if `data` is null
|
|
@@ -489,7 +492,7 @@ function validateTopLevelDocumentMembers(reporter, doc) {
|
|
|
489
492
|
if ('included' in doc && doc.data === null) {
|
|
490
493
|
const isMaybeDelete = reporter.contextDocument.request?.method?.toUpperCase() === 'DELETE' || reporter.contextDocument.request?.op === 'deleteRecord';
|
|
491
494
|
const method = !reporter.strict.linkage && isMaybeDelete ? 'warn' : 'error';
|
|
492
|
-
reporter[method](['included'], 'A {
|
|
495
|
+
reporter[method](['included'], 'A {json:api} Document MUST NOT contain `included` if `data` is null');
|
|
493
496
|
}
|
|
494
497
|
|
|
495
498
|
// Simple Validation of Top-Level Members
|
|
@@ -576,7 +579,7 @@ function validateLinks(reporter, doc, type, path = ['links']) {
|
|
|
576
579
|
const keys = Object.keys(links);
|
|
577
580
|
for (const key of keys) {
|
|
578
581
|
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 {
|
|
582
|
+
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
583
|
}
|
|
581
584
|
// links may be either a string or an object with an href property or null
|
|
582
585
|
if (links[key] === null) ; else if (typeof links[key] === 'string') {
|
|
@@ -738,7 +741,7 @@ function validateTopLevelResourceShape(reporter, resource, path) {
|
|
|
738
741
|
const extension = reporter.getExtension(extensionName);
|
|
739
742
|
extension(reporter, [...path, key]);
|
|
740
743
|
} else {
|
|
741
|
-
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {
|
|
744
|
+
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
745
|
}
|
|
743
746
|
} else {
|
|
744
747
|
// check if this is an attribute or relationship
|
|
@@ -767,7 +770,7 @@ function validateTopLevelResourceShape(reporter, resource, path) {
|
|
|
767
770
|
}
|
|
768
771
|
}
|
|
769
772
|
}
|
|
770
|
-
reporter.error([...path, key], `Unrecognized ResourceObject member. The data it provides is ignored as it is not a valid {
|
|
773
|
+
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
774
|
}
|
|
772
775
|
}
|
|
773
776
|
}
|
|
@@ -786,12 +789,15 @@ function validateTopLevelResourceShape(reporter, resource, path) {
|
|
|
786
789
|
}
|
|
787
790
|
}
|
|
788
791
|
function validateResourceAttributes(reporter, type, resource, path) {
|
|
789
|
-
const
|
|
792
|
+
const fields = reporter.schema.fields({
|
|
790
793
|
type
|
|
791
794
|
});
|
|
795
|
+
const cacheFields = reporter.schema.cacheFields?.({
|
|
796
|
+
type
|
|
797
|
+
}) ?? fields;
|
|
792
798
|
for (const [key] of Object.entries(resource)) {
|
|
793
|
-
const field = getRemoteField(
|
|
794
|
-
const actualField =
|
|
799
|
+
const field = getRemoteField(cacheFields, key);
|
|
800
|
+
const actualField = cacheFields.get(key);
|
|
795
801
|
if (!field && actualField) {
|
|
796
802
|
reporter.warn([...path, key], `Expected the ${actualField.kind} field "${key}" to not have its own data in the ResourceObject's attributes. Likely this field should either not be returned in this payload or the field definition should be updated in the schema.`);
|
|
797
803
|
} else if (!field) {
|
|
@@ -801,7 +807,7 @@ function validateResourceAttributes(reporter, type, resource, path) {
|
|
|
801
807
|
const extension = reporter.getExtension(extensionName);
|
|
802
808
|
extension(reporter, [...path, key]);
|
|
803
809
|
} else {
|
|
804
|
-
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {
|
|
810
|
+
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
811
|
}
|
|
806
812
|
} else {
|
|
807
813
|
const method = reporter.strict.unknownAttribute ? 'error' : 'warn';
|
|
@@ -835,7 +841,7 @@ function validateResourceRelationships(reporter, type, resource, path) {
|
|
|
835
841
|
const extension = reporter.getExtension(extensionName);
|
|
836
842
|
extension(reporter, [...path, key]);
|
|
837
843
|
} else {
|
|
838
|
-
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {
|
|
844
|
+
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
845
|
}
|
|
840
846
|
} else {
|
|
841
847
|
const method = reporter.strict.unknownRelationship ? 'error' : 'warn';
|
|
@@ -955,82 +961,12 @@ function makeCache() {
|
|
|
955
961
|
}
|
|
956
962
|
|
|
957
963
|
/**
|
|
958
|
-
* ### JSONAPICache
|
|
959
|
-
*
|
|
960
964
|
* ```ts
|
|
961
965
|
* import { JSONAPICache } from '@warp-drive-mirror/json-api';
|
|
962
966
|
* ```
|
|
963
967
|
*
|
|
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
|
-
|
|
968
|
+
* A {@link Cache} implementation tuned for [{json:api}](https://jsonapi.org/)
|
|
969
|
+
*
|
|
1034
970
|
* @categoryDescription Cache Management
|
|
1035
971
|
* APIs for primary cache management functionality
|
|
1036
972
|
* @categoryDescription Cache Forking
|
|
@@ -1043,10 +979,9 @@ function makeCache() {
|
|
|
1043
979
|
* APIs that support granular field level management of resource data
|
|
1044
980
|
* @categoryDescription Resource State
|
|
1045
981
|
* APIs that support managing Resource states
|
|
1046
|
-
|
|
1047
|
-
|
|
982
|
+
*
|
|
983
|
+
* @public
|
|
1048
984
|
*/
|
|
1049
|
-
|
|
1050
985
|
class JSONAPICache {
|
|
1051
986
|
/**
|
|
1052
987
|
* The Cache Version that this implementation implements.
|
|
@@ -1365,7 +1300,7 @@ class JSONAPICache {
|
|
|
1365
1300
|
id,
|
|
1366
1301
|
lid
|
|
1367
1302
|
} = identifier;
|
|
1368
|
-
const attributes = Object.assign({}, peeked.remoteAttrs, peeked.inflightAttrs, peeked.localAttrs);
|
|
1303
|
+
const attributes = structuredClone(Object.assign({}, peeked.remoteAttrs, peeked.inflightAttrs, peeked.localAttrs));
|
|
1369
1304
|
const relationships = {};
|
|
1370
1305
|
const rels = this.__graph.identifiers.get(identifier);
|
|
1371
1306
|
if (rels) {
|
|
@@ -1374,7 +1309,7 @@ class JSONAPICache {
|
|
|
1374
1309
|
if (rel.definition.isImplicit) {
|
|
1375
1310
|
return;
|
|
1376
1311
|
} else {
|
|
1377
|
-
relationships[key] = this.__graph.getData(identifier, key);
|
|
1312
|
+
relationships[key] = structuredClone(this.__graph.getData(identifier, key));
|
|
1378
1313
|
}
|
|
1379
1314
|
});
|
|
1380
1315
|
}
|
|
@@ -1423,7 +1358,7 @@ class JSONAPICache {
|
|
|
1423
1358
|
id,
|
|
1424
1359
|
lid
|
|
1425
1360
|
} = identifier;
|
|
1426
|
-
const attributes =
|
|
1361
|
+
const attributes = structuredClone(peeked.remoteAttrs);
|
|
1427
1362
|
const relationships = {};
|
|
1428
1363
|
const rels = this.__graph.identifiers.get(identifier);
|
|
1429
1364
|
if (rels) {
|
|
@@ -1432,7 +1367,7 @@ class JSONAPICache {
|
|
|
1432
1367
|
if (rel.definition.isImplicit) {
|
|
1433
1368
|
return;
|
|
1434
1369
|
} else {
|
|
1435
|
-
relationships[key] = this.__graph.getData(identifier, key);
|
|
1370
|
+
relationships[key] = structuredClone(this.__graph.getData(identifier, key));
|
|
1436
1371
|
}
|
|
1437
1372
|
});
|
|
1438
1373
|
}
|
|
@@ -1689,50 +1624,12 @@ class JSONAPICache {
|
|
|
1689
1624
|
* @public
|
|
1690
1625
|
*/
|
|
1691
1626
|
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);
|
|
1627
|
+
if (Array.isArray(identifier)) {
|
|
1628
|
+
for (const key of identifier) {
|
|
1629
|
+
willCommit(this, key);
|
|
1717
1630
|
}
|
|
1718
1631
|
} 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
|
-
}
|
|
1632
|
+
willCommit(this, identifier);
|
|
1736
1633
|
}
|
|
1737
1634
|
}
|
|
1738
1635
|
|
|
@@ -1743,9 +1640,10 @@ class JSONAPICache {
|
|
|
1743
1640
|
* @category Resource Lifecycle
|
|
1744
1641
|
* @public
|
|
1745
1642
|
*/
|
|
1643
|
+
|
|
1746
1644
|
didCommit(committedIdentifier, result) {
|
|
1747
1645
|
const payload = result ? result.content : null;
|
|
1748
|
-
const operation = result
|
|
1646
|
+
const operation = result?.request?.op ?? null;
|
|
1749
1647
|
const data = payload && payload.data;
|
|
1750
1648
|
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
1751
1649
|
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
@@ -1759,101 +1657,45 @@ class JSONAPICache {
|
|
|
1759
1657
|
}
|
|
1760
1658
|
}
|
|
1761
1659
|
}
|
|
1762
|
-
|
|
1660
|
+
const responseIsCollection = Array.isArray(data);
|
|
1661
|
+
const hasMultipleIdentifiers = Array.isArray(committedIdentifier) && committedIdentifier.length > 1;
|
|
1662
|
+
if (Array.isArray(committedIdentifier)) {
|
|
1663
|
+
// if we get back an array of primary data, we treat each
|
|
1664
|
+
// entry as a separate commit for each identifier
|
|
1763
1665
|
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1764
1666
|
if (!test) {
|
|
1765
|
-
throw new Error(`
|
|
1667
|
+
throw new Error(`Expected the array of primary data to match the array of committed identifiers`);
|
|
1766
1668
|
}
|
|
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}'.`);
|
|
1669
|
+
})(!hasMultipleIdentifiers || !responseIsCollection || data.length === committedIdentifier.length) : {};
|
|
1670
|
+
if (responseIsCollection) {
|
|
1671
|
+
for (let i = 0; i < committedIdentifier.length; i++) {
|
|
1672
|
+
const identifier = committedIdentifier[i];
|
|
1673
|
+
didCommit(this, identifier, data[i] ?? null, operation);
|
|
1808
1674
|
}
|
|
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
|
-
}
|
|
1675
|
+
// but if we get back no data or a single entry, we apply
|
|
1676
|
+
// the change back to the original identifier
|
|
1677
|
+
} else {
|
|
1678
|
+
for (let i = 0; i < committedIdentifier.length; i++) {
|
|
1679
|
+
const identifier = committedIdentifier[i];
|
|
1680
|
+
didCommit(this, identifier, i === 0 ? data : null, operation);
|
|
1834
1681
|
}
|
|
1835
|
-
setupRelationships(this.__graph, fields, identifier, data);
|
|
1836
1682
|
}
|
|
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);
|
|
1683
|
+
} else {
|
|
1684
|
+
didCommit(this, committedIdentifier, data, operation);
|
|
1846
1685
|
}
|
|
1847
|
-
if (changedKeys?.size) notifyAttributes(this._capabilities, identifier, changedKeys);
|
|
1848
|
-
this._capabilities.notifyChange(identifier, 'state', null);
|
|
1849
1686
|
const included = payload && payload.included;
|
|
1687
|
+
const {
|
|
1688
|
+
cacheKeyManager
|
|
1689
|
+
} = this._capabilities;
|
|
1850
1690
|
if (included) {
|
|
1851
1691
|
for (let i = 0, length = included.length; i < length; i++) {
|
|
1852
1692
|
putOne(this, cacheKeyManager, included[i]);
|
|
1853
1693
|
}
|
|
1854
1694
|
}
|
|
1855
|
-
return {
|
|
1856
|
-
data:
|
|
1695
|
+
return hasMultipleIdentifiers && responseIsCollection ? {
|
|
1696
|
+
data: committedIdentifier
|
|
1697
|
+
} : {
|
|
1698
|
+
data: Array.isArray(committedIdentifier) ? committedIdentifier[0] : committedIdentifier
|
|
1857
1699
|
};
|
|
1858
1700
|
}
|
|
1859
1701
|
|
|
@@ -1865,23 +1707,13 @@ class JSONAPICache {
|
|
|
1865
1707
|
* @public
|
|
1866
1708
|
*/
|
|
1867
1709
|
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
|
-
}
|
|
1710
|
+
if (Array.isArray(identifier)) {
|
|
1711
|
+
for (let i = 0; i < identifier.length; i++) {
|
|
1712
|
+
commitDidError(this, identifier[i], errors && i === 0 ? errors : null);
|
|
1878
1713
|
}
|
|
1879
|
-
|
|
1880
|
-
}
|
|
1881
|
-
if (errors) {
|
|
1882
|
-
cached.errors = errors;
|
|
1714
|
+
return;
|
|
1883
1715
|
}
|
|
1884
|
-
this
|
|
1716
|
+
return commitDidError(this, identifier, errors || null);
|
|
1885
1717
|
}
|
|
1886
1718
|
|
|
1887
1719
|
/**
|
|
@@ -2901,7 +2733,7 @@ function putOne(cache, identifiers, resource) {
|
|
|
2901
2733
|
throw new Error(`Missing Resource Type: received resource data with a type '${resource.type}' but no schema could be found with that name.`);
|
|
2902
2734
|
}
|
|
2903
2735
|
})(cache._capabilities.schema.hasResource(resource)) : {};
|
|
2904
|
-
let identifier = identifiers.
|
|
2736
|
+
let identifier = identifiers.peekResourceKey(resource);
|
|
2905
2737
|
if (identifier) {
|
|
2906
2738
|
identifier = identifiers.updateRecordIdentifier(identifier, resource);
|
|
2907
2739
|
} else {
|
|
@@ -3233,5 +3065,161 @@ function getCacheFields(cache, identifier) {
|
|
|
3233
3065
|
// the model schema service cannot process fields that are not cache fields
|
|
3234
3066
|
return cache._capabilities.schema.fields(identifier);
|
|
3235
3067
|
}
|
|
3068
|
+
function commitDidError(cache, identifier, errors) {
|
|
3069
|
+
const cached = cache.__peek(identifier, false);
|
|
3070
|
+
if (cached.inflightAttrs) {
|
|
3071
|
+
const keys = Object.keys(cached.inflightAttrs);
|
|
3072
|
+
if (keys.length > 0) {
|
|
3073
|
+
const attrs = cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
3074
|
+
for (let i = 0; i < keys.length; i++) {
|
|
3075
|
+
if (attrs[keys[i]] === undefined) {
|
|
3076
|
+
attrs[keys[i]] = cached.inflightAttrs[keys[i]];
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
cached.inflightAttrs = null;
|
|
3081
|
+
}
|
|
3082
|
+
if (errors) {
|
|
3083
|
+
cached.errors = errors;
|
|
3084
|
+
}
|
|
3085
|
+
cache._capabilities.notifyChange(identifier, 'errors', null);
|
|
3086
|
+
}
|
|
3087
|
+
function didCommit(cache, committedIdentifier, data, op) {
|
|
3088
|
+
if (!data) {
|
|
3089
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3090
|
+
if (!test) {
|
|
3091
|
+
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.`);
|
|
3092
|
+
}
|
|
3093
|
+
})(committedIdentifier.id) : {};
|
|
3094
|
+
}
|
|
3095
|
+
const {
|
|
3096
|
+
cacheKeyManager
|
|
3097
|
+
} = cache._capabilities;
|
|
3098
|
+
const existingId = committedIdentifier.id;
|
|
3099
|
+
const identifier = op !== 'deleteRecord' && data ? cacheKeyManager.updateRecordIdentifier(committedIdentifier, data) : committedIdentifier;
|
|
3100
|
+
const cached = cache.__peek(identifier, false);
|
|
3101
|
+
if (cached.isDeleted || op === 'deleteRecord') {
|
|
3102
|
+
cache.__graph.push({
|
|
3103
|
+
op: 'deleteRecord',
|
|
3104
|
+
record: identifier,
|
|
3105
|
+
isNew: false
|
|
3106
|
+
});
|
|
3107
|
+
cached.isDeleted = true;
|
|
3108
|
+
cached.isDeletionCommitted = true;
|
|
3109
|
+
cache._capabilities.notifyChange(identifier, 'removed', null);
|
|
3110
|
+
// TODO @runspired should we early exit here?
|
|
3111
|
+
}
|
|
3112
|
+
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3113
|
+
if (cached.isNew && !identifier.id && (typeof data?.id !== 'string' || data.id.length > 0)) {
|
|
3114
|
+
const error = new Error(`Expected an id ${String(identifier)} in response ${JSON.stringify(data)}`);
|
|
3115
|
+
//@ts-expect-error
|
|
3116
|
+
error.isAdapterError = true;
|
|
3117
|
+
//@ts-expect-error
|
|
3118
|
+
error.code = 'InvalidError';
|
|
3119
|
+
throw error;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
const fields = getCacheFields(cache, identifier);
|
|
3123
|
+
cached.isNew = false;
|
|
3124
|
+
let newCanonicalAttributes;
|
|
3125
|
+
if (data) {
|
|
3126
|
+
if (data.id && !cached.id) {
|
|
3127
|
+
cached.id = data.id;
|
|
3128
|
+
}
|
|
3129
|
+
if (identifier === committedIdentifier && identifier.id !== existingId) {
|
|
3130
|
+
cache._capabilities.notifyChange(identifier, 'identity', null);
|
|
3131
|
+
}
|
|
3132
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3133
|
+
if (!test) {
|
|
3134
|
+
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}'.`);
|
|
3135
|
+
}
|
|
3136
|
+
})(identifier.id === cached.id) : {};
|
|
3137
|
+
if (data.relationships) {
|
|
3138
|
+
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3139
|
+
if (macroCondition(!getGlobalConfig().WarpDriveMirror.deprecations.DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE)) {
|
|
3140
|
+
// assert against bad API behavior where a belongsTo relationship
|
|
3141
|
+
// is saved but the return payload indicates a different final state.
|
|
3142
|
+
fields.forEach((field, name) => {
|
|
3143
|
+
if (field.kind === 'belongsTo') {
|
|
3144
|
+
const relationshipData = data.relationships[name]?.data;
|
|
3145
|
+
if (relationshipData !== undefined) {
|
|
3146
|
+
const inFlightData = cached.inflightRelationships?.[name];
|
|
3147
|
+
if (!inFlightData || !('data' in inFlightData)) {
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
const actualData = relationshipData ? cache._capabilities.cacheKeyManager.getOrCreateRecordIdentifier(relationshipData) : null;
|
|
3151
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3152
|
+
if (!test) {
|
|
3153
|
+
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>'}`);
|
|
3154
|
+
}
|
|
3155
|
+
})(inFlightData.data === actualData) : {};
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
});
|
|
3159
|
+
cached.inflightRelationships = null;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
setupRelationships(cache.__graph, fields, identifier, data);
|
|
3163
|
+
}
|
|
3164
|
+
newCanonicalAttributes = data.attributes;
|
|
3165
|
+
}
|
|
3166
|
+
const changedKeys = newCanonicalAttributes && calculateChangedKeys(cached, newCanonicalAttributes, fields);
|
|
3167
|
+
cached.remoteAttrs = Object.assign(cached.remoteAttrs || Object.create(null), cached.inflightAttrs, newCanonicalAttributes);
|
|
3168
|
+
cached.inflightAttrs = null;
|
|
3169
|
+
patchLocalAttributes(cached, changedKeys);
|
|
3170
|
+
if (cached.errors) {
|
|
3171
|
+
cached.errors = null;
|
|
3172
|
+
cache._capabilities.notifyChange(identifier, 'errors', null);
|
|
3173
|
+
}
|
|
3174
|
+
if (changedKeys?.size) notifyAttributes(cache._capabilities, identifier, changedKeys);
|
|
3175
|
+
cache._capabilities.notifyChange(identifier, 'state', null);
|
|
3176
|
+
}
|
|
3177
|
+
function willCommit(cache, identifier) {
|
|
3178
|
+
const cached = cache.__peek(identifier, false);
|
|
3179
|
+
|
|
3180
|
+
/*
|
|
3181
|
+
if we have multiple saves in flight at once then
|
|
3182
|
+
we have information loss no matter what. This
|
|
3183
|
+
attempts to lose the least information.
|
|
3184
|
+
If we were to clear inflightAttrs, previous requests
|
|
3185
|
+
would not be able to use it during their didCommit.
|
|
3186
|
+
If we upsert inflightattrs, previous requests incorrectly
|
|
3187
|
+
see more recent inflight changes as part of their own and
|
|
3188
|
+
will incorrectly mark the new state as the correct remote state.
|
|
3189
|
+
We choose this latter behavior to avoid accidentally removing
|
|
3190
|
+
earlier changes.
|
|
3191
|
+
If apps do not want this behavior they can either
|
|
3192
|
+
- chain save requests serially vs allowing concurrent saves
|
|
3193
|
+
- move to using a request handler that caches the inflight state
|
|
3194
|
+
on a per-request basis
|
|
3195
|
+
- change their save requests to only send a "PATCH" instead of a "PUT"
|
|
3196
|
+
so that only latest changes are involved in each request, and then also
|
|
3197
|
+
ensure that the API or their handler reflects only those changes back
|
|
3198
|
+
for upsert into the cache.
|
|
3199
|
+
*/
|
|
3200
|
+
if (cached.inflightAttrs) {
|
|
3201
|
+
if (cached.localAttrs) {
|
|
3202
|
+
Object.assign(cached.inflightAttrs, cached.localAttrs);
|
|
3203
|
+
}
|
|
3204
|
+
} else {
|
|
3205
|
+
cached.inflightAttrs = cached.localAttrs;
|
|
3206
|
+
}
|
|
3207
|
+
cached.localAttrs = null;
|
|
3208
|
+
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3209
|
+
if (macroCondition(!getGlobalConfig().WarpDriveMirror.deprecations.DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE)) {
|
|
3210
|
+
// save off info about saved relationships
|
|
3211
|
+
const fields = getCacheFields(cache, identifier);
|
|
3212
|
+
fields.forEach((schema, name) => {
|
|
3213
|
+
if (schema.kind === 'belongsTo') {
|
|
3214
|
+
if (cache.__graph._isDirty(identifier, name)) {
|
|
3215
|
+
const relationshipData = cache.__graph.getData(identifier, name);
|
|
3216
|
+
const inFlight = cached.inflightRelationships = cached.inflightRelationships || Object.create(null);
|
|
3217
|
+
inFlight[name] = relationshipData;
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3236
3224
|
|
|
3237
3225
|
export { JSONAPICache };
|