@warp-drive-mirror/json-api 5.8.0-beta.0 → 5.8.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -28
- package/declarations/-private/cache.d.ts +18 -77
- package/declarations/index.d.ts +4 -0
- package/dist/index.js +237 -237
- package/dist/unpkg/dev/index.js +3350 -0
- package/dist/unpkg/dev-deprecated/index.js +3311 -0
- package/dist/unpkg/prod/index.js +1884 -0
- package/dist/unpkg/prod-deprecated/index.js +1884 -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 +14 -6
- 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.
|
|
@@ -1350,6 +1285,15 @@ class JSONAPICache {
|
|
|
1350
1285
|
* of the Graph handling necessary entanglements and
|
|
1351
1286
|
* notifications for relational data.
|
|
1352
1287
|
*
|
|
1288
|
+
* :::warning
|
|
1289
|
+
* It is not recommended to use the return value as
|
|
1290
|
+
* a serialized representation of the resource both
|
|
1291
|
+
* due to it containing local mutations and because
|
|
1292
|
+
* it may contain additional fields not recognized
|
|
1293
|
+
* by the {json:api} API implementation such as `lid` and
|
|
1294
|
+
* the various internal WarpDrive bookkeeping fields.
|
|
1295
|
+
* :::
|
|
1296
|
+
*
|
|
1353
1297
|
* @category Cache Management
|
|
1354
1298
|
* @public
|
|
1355
1299
|
*/
|
|
@@ -1365,7 +1309,7 @@ class JSONAPICache {
|
|
|
1365
1309
|
id,
|
|
1366
1310
|
lid
|
|
1367
1311
|
} = identifier;
|
|
1368
|
-
const attributes = Object.assign({}, peeked.remoteAttrs, peeked.inflightAttrs, peeked.localAttrs);
|
|
1312
|
+
const attributes = structuredClone(Object.assign({}, peeked.remoteAttrs, peeked.inflightAttrs, peeked.localAttrs));
|
|
1369
1313
|
const relationships = {};
|
|
1370
1314
|
const rels = this.__graph.identifiers.get(identifier);
|
|
1371
1315
|
if (rels) {
|
|
@@ -1423,7 +1367,7 @@ class JSONAPICache {
|
|
|
1423
1367
|
id,
|
|
1424
1368
|
lid
|
|
1425
1369
|
} = identifier;
|
|
1426
|
-
const attributes =
|
|
1370
|
+
const attributes = structuredClone(peeked.remoteAttrs);
|
|
1427
1371
|
const relationships = {};
|
|
1428
1372
|
const rels = this.__graph.identifiers.get(identifier);
|
|
1429
1373
|
if (rels) {
|
|
@@ -1432,7 +1376,7 @@ class JSONAPICache {
|
|
|
1432
1376
|
if (rel.definition.isImplicit) {
|
|
1433
1377
|
return;
|
|
1434
1378
|
} else {
|
|
1435
|
-
relationships[key] = this.__graph.getData(identifier, key);
|
|
1379
|
+
relationships[key] = structuredClone(this.__graph.getData(identifier, key));
|
|
1436
1380
|
}
|
|
1437
1381
|
});
|
|
1438
1382
|
}
|
|
@@ -1689,50 +1633,12 @@ class JSONAPICache {
|
|
|
1689
1633
|
* @public
|
|
1690
1634
|
*/
|
|
1691
1635
|
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);
|
|
1636
|
+
if (Array.isArray(identifier)) {
|
|
1637
|
+
for (const key of identifier) {
|
|
1638
|
+
willCommit(this, key);
|
|
1717
1639
|
}
|
|
1718
1640
|
} 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
|
-
}
|
|
1641
|
+
willCommit(this, identifier);
|
|
1736
1642
|
}
|
|
1737
1643
|
}
|
|
1738
1644
|
|
|
@@ -1743,9 +1649,10 @@ class JSONAPICache {
|
|
|
1743
1649
|
* @category Resource Lifecycle
|
|
1744
1650
|
* @public
|
|
1745
1651
|
*/
|
|
1652
|
+
|
|
1746
1653
|
didCommit(committedIdentifier, result) {
|
|
1747
1654
|
const payload = result ? result.content : null;
|
|
1748
|
-
const operation = result
|
|
1655
|
+
const operation = result?.request?.op ?? null;
|
|
1749
1656
|
const data = payload && payload.data;
|
|
1750
1657
|
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
1751
1658
|
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
@@ -1759,101 +1666,45 @@ class JSONAPICache {
|
|
|
1759
1666
|
}
|
|
1760
1667
|
}
|
|
1761
1668
|
}
|
|
1762
|
-
|
|
1669
|
+
const responseIsCollection = Array.isArray(data);
|
|
1670
|
+
const hasMultipleIdentifiers = Array.isArray(committedIdentifier) && committedIdentifier.length > 1;
|
|
1671
|
+
if (Array.isArray(committedIdentifier)) {
|
|
1672
|
+
// if we get back an array of primary data, we treat each
|
|
1673
|
+
// entry as a separate commit for each identifier
|
|
1763
1674
|
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1764
1675
|
if (!test) {
|
|
1765
|
-
throw new Error(`
|
|
1676
|
+
throw new Error(`Expected the array of primary data to match the array of committed identifiers`);
|
|
1766
1677
|
}
|
|
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}'.`);
|
|
1678
|
+
})(!hasMultipleIdentifiers || !responseIsCollection || data.length === committedIdentifier.length) : {};
|
|
1679
|
+
if (responseIsCollection) {
|
|
1680
|
+
for (let i = 0; i < committedIdentifier.length; i++) {
|
|
1681
|
+
const identifier = committedIdentifier[i];
|
|
1682
|
+
didCommit(this, identifier, data[i] ?? null, operation);
|
|
1808
1683
|
}
|
|
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
|
-
}
|
|
1684
|
+
// but if we get back no data or a single entry, we apply
|
|
1685
|
+
// the change back to the original identifier
|
|
1686
|
+
} else {
|
|
1687
|
+
for (let i = 0; i < committedIdentifier.length; i++) {
|
|
1688
|
+
const identifier = committedIdentifier[i];
|
|
1689
|
+
didCommit(this, identifier, i === 0 ? data : null, operation);
|
|
1834
1690
|
}
|
|
1835
|
-
setupRelationships(this.__graph, fields, identifier, data);
|
|
1836
1691
|
}
|
|
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);
|
|
1692
|
+
} else {
|
|
1693
|
+
didCommit(this, committedIdentifier, data, operation);
|
|
1846
1694
|
}
|
|
1847
|
-
if (changedKeys?.size) notifyAttributes(this._capabilities, identifier, changedKeys);
|
|
1848
|
-
this._capabilities.notifyChange(identifier, 'state', null);
|
|
1849
1695
|
const included = payload && payload.included;
|
|
1696
|
+
const {
|
|
1697
|
+
cacheKeyManager
|
|
1698
|
+
} = this._capabilities;
|
|
1850
1699
|
if (included) {
|
|
1851
1700
|
for (let i = 0, length = included.length; i < length; i++) {
|
|
1852
1701
|
putOne(this, cacheKeyManager, included[i]);
|
|
1853
1702
|
}
|
|
1854
1703
|
}
|
|
1855
|
-
return {
|
|
1856
|
-
data:
|
|
1704
|
+
return hasMultipleIdentifiers && responseIsCollection ? {
|
|
1705
|
+
data: committedIdentifier
|
|
1706
|
+
} : {
|
|
1707
|
+
data: Array.isArray(committedIdentifier) ? committedIdentifier[0] : committedIdentifier
|
|
1857
1708
|
};
|
|
1858
1709
|
}
|
|
1859
1710
|
|
|
@@ -1865,23 +1716,13 @@ class JSONAPICache {
|
|
|
1865
1716
|
* @public
|
|
1866
1717
|
*/
|
|
1867
1718
|
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
|
-
}
|
|
1719
|
+
if (Array.isArray(identifier)) {
|
|
1720
|
+
for (let i = 0; i < identifier.length; i++) {
|
|
1721
|
+
commitDidError(this, identifier[i], errors && i === 0 ? errors : null);
|
|
1878
1722
|
}
|
|
1879
|
-
|
|
1880
|
-
}
|
|
1881
|
-
if (errors) {
|
|
1882
|
-
cached.errors = errors;
|
|
1723
|
+
return;
|
|
1883
1724
|
}
|
|
1884
|
-
this
|
|
1725
|
+
return commitDidError(this, identifier, errors || null);
|
|
1885
1726
|
}
|
|
1886
1727
|
|
|
1887
1728
|
/**
|
|
@@ -2901,7 +2742,7 @@ function putOne(cache, identifiers, resource) {
|
|
|
2901
2742
|
throw new Error(`Missing Resource Type: received resource data with a type '${resource.type}' but no schema could be found with that name.`);
|
|
2902
2743
|
}
|
|
2903
2744
|
})(cache._capabilities.schema.hasResource(resource)) : {};
|
|
2904
|
-
let identifier = identifiers.
|
|
2745
|
+
let identifier = identifiers.peekResourceKey(resource);
|
|
2905
2746
|
if (identifier) {
|
|
2906
2747
|
identifier = identifiers.updateRecordIdentifier(identifier, resource);
|
|
2907
2748
|
} else {
|
|
@@ -3227,11 +3068,170 @@ function patchCache(Cache, op) {
|
|
|
3227
3068
|
}
|
|
3228
3069
|
function getCacheFields(cache, identifier) {
|
|
3229
3070
|
if (cache._capabilities.schema.cacheFields) {
|
|
3230
|
-
|
|
3071
|
+
const result = cache._capabilities.schema.cacheFields(identifier);
|
|
3072
|
+
if (result) {
|
|
3073
|
+
return result;
|
|
3074
|
+
}
|
|
3231
3075
|
}
|
|
3232
3076
|
|
|
3233
3077
|
// the model schema service cannot process fields that are not cache fields
|
|
3234
3078
|
return cache._capabilities.schema.fields(identifier);
|
|
3235
3079
|
}
|
|
3080
|
+
function commitDidError(cache, identifier, errors) {
|
|
3081
|
+
const cached = cache.__peek(identifier, false);
|
|
3082
|
+
if (cached.inflightAttrs) {
|
|
3083
|
+
const keys = Object.keys(cached.inflightAttrs);
|
|
3084
|
+
if (keys.length > 0) {
|
|
3085
|
+
const attrs = cached.localAttrs = cached.localAttrs || Object.create(null);
|
|
3086
|
+
for (let i = 0; i < keys.length; i++) {
|
|
3087
|
+
if (attrs[keys[i]] === undefined) {
|
|
3088
|
+
attrs[keys[i]] = cached.inflightAttrs[keys[i]];
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
cached.inflightAttrs = null;
|
|
3093
|
+
}
|
|
3094
|
+
if (errors) {
|
|
3095
|
+
cached.errors = errors;
|
|
3096
|
+
}
|
|
3097
|
+
cache._capabilities.notifyChange(identifier, 'errors', null);
|
|
3098
|
+
}
|
|
3099
|
+
function didCommit(cache, committedIdentifier, data, op) {
|
|
3100
|
+
if (!data) {
|
|
3101
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3102
|
+
if (!test) {
|
|
3103
|
+
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.`);
|
|
3104
|
+
}
|
|
3105
|
+
})(committedIdentifier.id) : {};
|
|
3106
|
+
}
|
|
3107
|
+
const {
|
|
3108
|
+
cacheKeyManager
|
|
3109
|
+
} = cache._capabilities;
|
|
3110
|
+
const existingId = committedIdentifier.id;
|
|
3111
|
+
const identifier = op !== 'deleteRecord' && data ? cacheKeyManager.updateRecordIdentifier(committedIdentifier, data) : committedIdentifier;
|
|
3112
|
+
const cached = cache.__peek(identifier, false);
|
|
3113
|
+
if (cached.isDeleted || op === 'deleteRecord') {
|
|
3114
|
+
cache.__graph.push({
|
|
3115
|
+
op: 'deleteRecord',
|
|
3116
|
+
record: identifier,
|
|
3117
|
+
isNew: false
|
|
3118
|
+
});
|
|
3119
|
+
cached.isDeleted = true;
|
|
3120
|
+
cached.isDeletionCommitted = true;
|
|
3121
|
+
cache._capabilities.notifyChange(identifier, 'removed', null);
|
|
3122
|
+
// TODO @runspired should we early exit here?
|
|
3123
|
+
}
|
|
3124
|
+
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3125
|
+
if (cached.isNew && !identifier.id && (typeof data?.id !== 'string' || data.id.length > 0)) {
|
|
3126
|
+
const error = new Error(`Expected an id ${String(identifier)} in response ${JSON.stringify(data)}`);
|
|
3127
|
+
//@ts-expect-error
|
|
3128
|
+
error.isAdapterError = true;
|
|
3129
|
+
//@ts-expect-error
|
|
3130
|
+
error.code = 'InvalidError';
|
|
3131
|
+
throw error;
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
const fields = getCacheFields(cache, identifier);
|
|
3135
|
+
cached.isNew = false;
|
|
3136
|
+
let newCanonicalAttributes;
|
|
3137
|
+
if (data) {
|
|
3138
|
+
if (data.id && !cached.id) {
|
|
3139
|
+
cached.id = data.id;
|
|
3140
|
+
}
|
|
3141
|
+
if (identifier === committedIdentifier && identifier.id !== existingId) {
|
|
3142
|
+
cache._capabilities.notifyChange(identifier, 'identity', null);
|
|
3143
|
+
}
|
|
3144
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3145
|
+
if (!test) {
|
|
3146
|
+
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}'.`);
|
|
3147
|
+
}
|
|
3148
|
+
})(identifier.id === cached.id) : {};
|
|
3149
|
+
if (data.relationships) {
|
|
3150
|
+
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3151
|
+
if (macroCondition(!getGlobalConfig().WarpDriveMirror.deprecations.DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE)) {
|
|
3152
|
+
// assert against bad API behavior where a belongsTo relationship
|
|
3153
|
+
// is saved but the return payload indicates a different final state.
|
|
3154
|
+
fields.forEach((field, name) => {
|
|
3155
|
+
if (field.kind === 'belongsTo') {
|
|
3156
|
+
const relationshipData = data.relationships[name]?.data;
|
|
3157
|
+
if (relationshipData !== undefined) {
|
|
3158
|
+
const inFlightData = cached.inflightRelationships?.[name];
|
|
3159
|
+
if (!inFlightData || !('data' in inFlightData)) {
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
const actualData = relationshipData ? cache._capabilities.cacheKeyManager.getOrCreateRecordIdentifier(relationshipData) : null;
|
|
3163
|
+
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3164
|
+
if (!test) {
|
|
3165
|
+
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>'}`);
|
|
3166
|
+
}
|
|
3167
|
+
})(inFlightData.data === actualData) : {};
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
});
|
|
3171
|
+
cached.inflightRelationships = null;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
setupRelationships(cache.__graph, fields, identifier, data);
|
|
3175
|
+
}
|
|
3176
|
+
newCanonicalAttributes = data.attributes;
|
|
3177
|
+
}
|
|
3178
|
+
const changedKeys = newCanonicalAttributes && calculateChangedKeys(cached, newCanonicalAttributes, fields);
|
|
3179
|
+
cached.remoteAttrs = Object.assign(cached.remoteAttrs || Object.create(null), cached.inflightAttrs, newCanonicalAttributes);
|
|
3180
|
+
cached.inflightAttrs = null;
|
|
3181
|
+
patchLocalAttributes(cached, changedKeys);
|
|
3182
|
+
if (cached.errors) {
|
|
3183
|
+
cached.errors = null;
|
|
3184
|
+
cache._capabilities.notifyChange(identifier, 'errors', null);
|
|
3185
|
+
}
|
|
3186
|
+
if (changedKeys?.size) notifyAttributes(cache._capabilities, identifier, changedKeys);
|
|
3187
|
+
cache._capabilities.notifyChange(identifier, 'state', null);
|
|
3188
|
+
}
|
|
3189
|
+
function willCommit(cache, identifier) {
|
|
3190
|
+
const cached = cache.__peek(identifier, false);
|
|
3191
|
+
|
|
3192
|
+
/*
|
|
3193
|
+
if we have multiple saves in flight at once then
|
|
3194
|
+
we have information loss no matter what. This
|
|
3195
|
+
attempts to lose the least information.
|
|
3196
|
+
If we were to clear inflightAttrs, previous requests
|
|
3197
|
+
would not be able to use it during their didCommit.
|
|
3198
|
+
If we upsert inflightattrs, previous requests incorrectly
|
|
3199
|
+
see more recent inflight changes as part of their own and
|
|
3200
|
+
will incorrectly mark the new state as the correct remote state.
|
|
3201
|
+
We choose this latter behavior to avoid accidentally removing
|
|
3202
|
+
earlier changes.
|
|
3203
|
+
If apps do not want this behavior they can either
|
|
3204
|
+
- chain save requests serially vs allowing concurrent saves
|
|
3205
|
+
- move to using a request handler that caches the inflight state
|
|
3206
|
+
on a per-request basis
|
|
3207
|
+
- change their save requests to only send a "PATCH" instead of a "PUT"
|
|
3208
|
+
so that only latest changes are involved in each request, and then also
|
|
3209
|
+
ensure that the API or their handler reflects only those changes back
|
|
3210
|
+
for upsert into the cache.
|
|
3211
|
+
*/
|
|
3212
|
+
if (cached.inflightAttrs) {
|
|
3213
|
+
if (cached.localAttrs) {
|
|
3214
|
+
Object.assign(cached.inflightAttrs, cached.localAttrs);
|
|
3215
|
+
}
|
|
3216
|
+
} else {
|
|
3217
|
+
cached.inflightAttrs = cached.localAttrs;
|
|
3218
|
+
}
|
|
3219
|
+
cached.localAttrs = null;
|
|
3220
|
+
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3221
|
+
if (macroCondition(!getGlobalConfig().WarpDriveMirror.deprecations.DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE)) {
|
|
3222
|
+
// save off info about saved relationships
|
|
3223
|
+
const fields = getCacheFields(cache, identifier);
|
|
3224
|
+
fields.forEach((schema, name) => {
|
|
3225
|
+
if (schema.kind === 'belongsTo') {
|
|
3226
|
+
if (cache.__graph._isDirty(identifier, name)) {
|
|
3227
|
+
const relationshipData = cache.__graph.getData(identifier, name);
|
|
3228
|
+
const inFlight = cached.inflightRelationships = cached.inflightRelationships || Object.create(null);
|
|
3229
|
+
inFlight[name] = relationshipData;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
3236
|
|
|
3237
3237
|
export { JSONAPICache };
|