@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/dist/index.js CHANGED
@@ -196,7 +196,10 @@ class Reporter {
196
196
  const allFields = this.schema.fields({
197
197
  type
198
198
  });
199
- const attrs = Array.from(allFields.values()).filter(isRemoteField).map(v => v.name);
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 {JSON:API} document returned by ${this.contextDocument.request?.method} ${this.contextDocument.request?.url}`;
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 {JSON:API} member`);
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 {JSON:API} member`);
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 {JSON:API} Document must contain one-of `data` `errors` or `meta`');
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 {JSON:API} Document MUST NOT contain both `data` and `errors` members');
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 {JSON:API} Document MUST NOT contain both `included` and `errors` members');
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 {JSON:API} Document MUST NOT contain `included` if `data` is not present');
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 {JSON:API} Document MUST NOT contain `included` if `data` is null');
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 {JSON:API} link for a ${type}`);
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 {JSON:API} ResourceObject member`);
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 {JSON:API} ResourceObject member.${didYouMean}`);
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 schema = reporter.schema.fields({
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(schema, key);
794
- const actualField = schema.get(key);
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 {JSON:API} ResourceObject member`);
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 {JSON:API} ResourceObject member`);
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
- A {@link Cache} implementation tuned for [{json:api}](https://jsonapi.org/)
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
- @public
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 = Object.assign({}, peeked.remoteAttrs);
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
- const cached = this.__peek(identifier, false);
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
- cached.inflightAttrs = cached.localAttrs;
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 ? result.request.op : null;
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
- if (!data) {
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(`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.`);
1676
+ throw new Error(`Expected the array of primary data to match the array of committed identifiers`);
1766
1677
  }
1767
- })(committedIdentifier.id) : {};
1768
- }
1769
- const {
1770
- cacheKeyManager
1771
- } = this._capabilities;
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
- })(identifier.id === cached.id) : {};
1810
- if (data.relationships) {
1811
- if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
1812
- if (macroCondition(!getGlobalConfig().WarpDriveMirror.deprecations.DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE)) {
1813
- // assert against bad API behavior where a belongsTo relationship
1814
- // is saved but the return payload indicates a different final state.
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
- newCanonicalAttributes = data.attributes;
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: identifier
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
- const cached = this.__peek(identifier, false);
1869
- if (cached.inflightAttrs) {
1870
- const keys = Object.keys(cached.inflightAttrs);
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
- cached.inflightAttrs = null;
1880
- }
1881
- if (errors) {
1882
- cached.errors = errors;
1723
+ return;
1883
1724
  }
1884
- this._capabilities.notifyChange(identifier, 'errors', null);
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.peekRecordIdentifier(resource);
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
- return cache._capabilities.schema.cacheFields(identifier);
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 };