@ember-data/store 4.12.6 → 4.12.8

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/addon/-private.js CHANGED
@@ -1 +1 @@
1
- export { f as AdapterPopulatedRecordArray, C as CacheHandler, j as IDENTIFIER_ARRAY_TAG, I as IdentifierArray, M as MUTATE, I as RecordArray, R as RecordArrayManager, h as SOURCE, S as Store, _ as _clearCaches, e as coerceId, k as fastPush, i as isStableIdentifier, n as normalizeModelName, g as notifyArray, p as peekCache, r as recordIdentifierFor, l as removeRecordDataFor, c as setIdentifierForgetMethod, a as setIdentifierGenerationMethod, d as setIdentifierResetMethod, b as setIdentifierUpdateMethod, s as storeFor } from "./index-ecc2c2e5";
1
+ export { h as AdapterPopulatedRecordArray, C as CacheHandler, l as IDENTIFIER_ARRAY_TAG, I as IdentifierArray, M as MUTATE, I as RecordArray, R as RecordArrayManager, k as SOURCE, S as Store, _ as _clearCaches, f as coerceId, e as constructResource, g as ensureStringId, m as fastPush, i as isStableIdentifier, n as normalizeModelName, j as notifyArray, p as peekCache, r as recordIdentifierFor, o as removeRecordDataFor, c as setIdentifierForgetMethod, a as setIdentifierGenerationMethod, d as setIdentifierResetMethod, b as setIdentifierUpdateMethod, s as storeFor } from "./index-cc461d33";
@@ -164,6 +164,23 @@ async function _request2(link, options = {}) {
164
164
  }));
165
165
  return response.content;
166
166
  }
167
+
168
+ /**
169
+ * A service which an application may provide to the store via
170
+ * the store's `lifetimes` property to configure the behavior
171
+ * of the CacheHandler.
172
+ *
173
+ * The default behavior for request lifetimes is to never expire
174
+ * unless manually refreshed via `cacheOptions.reload` or `cacheOptions.backgroundReload`.
175
+ *
176
+ * Implementing this service allows you to programatically define
177
+ * when a request should be considered expired.
178
+ *
179
+ * @class <Interface> LifetimesService
180
+ * @public
181
+ */
182
+
183
+ const MUTATION_OPS = new Set(['createRecord', 'updateRecord', 'deleteRecord']);
167
184
  function isErrorDocument(document) {
168
185
  return 'errors' in document;
169
186
  }
@@ -171,6 +188,10 @@ function maybeUpdateUiObjects(store, request, options, document, isFromCache) {
171
188
  const {
172
189
  identifier
173
190
  } = options;
191
+ if (!document) {
192
+ assert(`The CacheHandler expected response content but none was found`, !options.shouldHydrate);
193
+ return document;
194
+ }
174
195
  if (isErrorDocument(document)) {
175
196
  if (!identifier && !options.shouldHydrate) {
176
197
  return document;
@@ -262,25 +283,54 @@ function calcShouldFetch(store, request, hasCachedValue, identifier) {
262
283
  const {
263
284
  cacheOptions
264
285
  } = request;
265
- return cacheOptions?.reload || !hasCachedValue || (store.lifetimes && identifier ? store.lifetimes.isHardExpired(identifier) : false);
286
+ return request.op && MUTATION_OPS.has(request.op) || cacheOptions?.reload || !hasCachedValue || (store.lifetimes && identifier ? store.lifetimes.isHardExpired(identifier, store) : false);
266
287
  }
267
288
  function calcShouldBackgroundFetch(store, request, willFetch, identifier) {
268
289
  const {
269
290
  cacheOptions
270
291
  } = request;
271
- return !willFetch && (cacheOptions?.backgroundReload || (store.lifetimes && identifier ? store.lifetimes.isSoftExpired(identifier) : false));
292
+ return !willFetch && (cacheOptions?.backgroundReload || (store.lifetimes && identifier ? store.lifetimes.isSoftExpired(identifier, store) : false));
293
+ }
294
+ function isMutation(request) {
295
+ return Boolean(request.op && MUTATION_OPS.has(request.op));
272
296
  }
273
297
  function fetchContentAndHydrate(next, context, identifier, shouldFetch, shouldBackgroundFetch) {
274
298
  const {
275
299
  store
276
300
  } = context.request;
277
- const shouldHydrate = context.request[Symbol.for('ember-data:enable-hydration')] || false;
278
- return next(context.request).then(document => {
301
+ const shouldHydrate = context.request[EnableHydration] || false;
302
+ let isMut = false;
303
+ if (isMutation(context.request)) {
304
+ isMut = true;
305
+ // TODO should we handle multiple records in request.records by iteratively calling willCommit for each
306
+ const record = context.request.data?.record || context.request.records?.[0];
307
+ assert(`Expected to receive a list of records included in the ${context.request.op} request`, record || !shouldHydrate);
308
+ if (record) {
309
+ store.cache.willCommit(record, context);
310
+ }
311
+ }
312
+ if (store.lifetimes?.willRequest) {
313
+ store.lifetimes.willRequest(context.request, identifier, store);
314
+ }
315
+ const promise = next(context.request).then(document => {
279
316
  store.requestManager._pending.delete(context.id);
280
317
  store._enableAsyncFlush = true;
281
318
  let response;
282
319
  store._join(() => {
283
- response = store.cache.put(document);
320
+ if (isMutation(context.request)) {
321
+ const record = context.request.data?.record || context.request.records?.[0];
322
+ if (record) {
323
+ response = store.cache.didCommit(record, document);
324
+
325
+ // a mutation combined with a 204 has no cache impact when no known records were involved
326
+ // a createRecord with a 201 with an empty response and no known records should similarly
327
+ // have no cache impact
328
+ } else if (isCacheAffecting(document)) {
329
+ response = store.cache.put(document);
330
+ }
331
+ } else {
332
+ response = store.cache.put(document);
333
+ }
284
334
  response = maybeUpdateUiObjects(store, context.request, {
285
335
  shouldHydrate,
286
336
  shouldFetch,
@@ -289,6 +339,9 @@ function fetchContentAndHydrate(next, context, identifier, shouldFetch, shouldBa
289
339
  }, response, false);
290
340
  });
291
341
  store._enableAsyncFlush = null;
342
+ if (store.lifetimes?.didRequest) {
343
+ store.lifetimes.didRequest(context.request, document.response, identifier, store);
344
+ }
292
345
  if (shouldFetch) {
293
346
  return response;
294
347
  } else if (shouldBackgroundFetch) {
@@ -303,15 +356,28 @@ function fetchContentAndHydrate(next, context, identifier, shouldFetch, shouldBa
303
356
  store._enableAsyncFlush = true;
304
357
  let response;
305
358
  store._join(() => {
306
- response = store.cache.put(error);
307
- response = maybeUpdateUiObjects(store, context.request, {
308
- shouldHydrate,
309
- shouldFetch,
310
- shouldBackgroundFetch,
311
- identifier
312
- }, response, false);
359
+ if (isMutation(context.request)) {
360
+ // TODO similar to didCommit we should spec this to be similar to cache.put for handling full response
361
+ // currently we let the response remain undefiend.
362
+ const errors = error && error.content && typeof error.content === 'object' && 'errors' in error.content && Array.isArray(error.content.errors) ? error.content.errors : undefined;
363
+ const record = context.request.data?.record || context.request.records?.[0];
364
+ store.cache.commitWasRejected(record, errors);
365
+ // re-throw the original error to preserve `errors` property.
366
+ throw error;
367
+ } else {
368
+ response = store.cache.put(error);
369
+ response = maybeUpdateUiObjects(store, context.request, {
370
+ shouldHydrate,
371
+ shouldFetch,
372
+ shouldBackgroundFetch,
373
+ identifier
374
+ }, response, false);
375
+ }
313
376
  });
314
377
  store._enableAsyncFlush = null;
378
+ if (identifier && store.lifetimes?.didRequest) {
379
+ store.lifetimes.didRequest(context.request, error.response, identifier, store);
380
+ }
315
381
  if (!shouldBackgroundFetch) {
316
382
  const newError = cloneError(error);
317
383
  newError.content = response;
@@ -320,15 +386,82 @@ function fetchContentAndHydrate(next, context, identifier, shouldFetch, shouldBa
320
386
  store.notifications._flush();
321
387
  }
322
388
  });
389
+ if (!isMut) {
390
+ return promise;
391
+ }
392
+ assert(`Expected a mutation`, isMutation(context.request));
393
+
394
+ // for mutations we need to enqueue the promise with the requestStateService
395
+ // TODO should we enque a request per record in records?
396
+ const record = context.request.data?.record || context.request.records?.[0];
397
+ return store._requestCache._enqueue(promise, {
398
+ data: [{
399
+ op: 'saveRecord',
400
+ recordIdentifier: record,
401
+ options: undefined
402
+ }]
403
+ });
404
+ }
405
+ function isAggregateError(error) {
406
+ return error instanceof AggregateError || error.name === 'AggregateError' && Array.isArray(error.errors);
323
407
  }
408
+ // TODO @runspired, consider if we should deep freeze errors (potentially only in debug) vs cloning them
324
409
  function cloneError(error) {
325
- const cloned = new Error(error.message);
410
+ const isAggregate = isAggregateError(error);
411
+ const cloned = isAggregate ?
412
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
413
+ new AggregateError(structuredClone(error.errors), error.message) : new Error(error.message);
326
414
  cloned.stack = error.stack;
327
415
  cloned.error = error.error;
416
+
417
+ // copy over enumerable properties
418
+ Object.assign(cloned, error);
328
419
  return cloned;
329
420
  }
330
421
  const SkipCache = Symbol.for('ember-data:skip-cache');
331
422
  const EnableHydration = Symbol.for('ember-data:enable-hydration');
423
+
424
+ /**
425
+ * A CacheHandler that adds support for using an EmberData Cache with a RequestManager.
426
+ *
427
+ * This handler will only run when a request has supplied a `store` instance. Requests
428
+ * issued by the store via `store.request()` will automatically have the `store` instance
429
+ * attached to the request.
430
+ *
431
+ * ```ts
432
+ * requestManager.request({
433
+ * store: store,
434
+ * url: '/api/posts',
435
+ * method: 'GET'
436
+ * });
437
+ * ```
438
+ *
439
+ * When this handler elects to handle a request, it will return the raw `StructuredDocument`
440
+ * unless the request has `[EnableHydration]` set to `true`. In this case, the handler will
441
+ * return a `Document` instance that will automatically update the UI when the cache is updated
442
+ * in the future and will hydrate any identifiers in the StructuredDocument into Record instances.
443
+ *
444
+ * When issuing a request via the store, [EnableHydration] is automatically set to `true`. This
445
+ * means that if desired you can issue requests that utilize the cache without needing to also
446
+ * utilize Record instances if desired.
447
+ *
448
+ * Said differently, you could elect to issue all requests via a RequestManager, without ever using
449
+ * the store directly, by setting [EnableHydration] to `true` and providing a store instance. Not
450
+ * necessarily the most useful thing, but the decoupled nature of the RequestManager and incremental-feature
451
+ * approach of EmberData allows for this flexibility.
452
+ *
453
+ * ```ts
454
+ * import { EnableHydration } from '@warp-drive/core-types/request';
455
+ *
456
+ * requestManager.request({
457
+ * store: store,
458
+ * url: '/api/posts',
459
+ * method: 'GET',
460
+ * [EnableHydration]: true
461
+ * });
462
+ *
463
+ * @typedoc
464
+ */
332
465
  const CacheHandler = {
333
466
  request(context, next) {
334
467
  // if we have no cache or no cache-key skip cache handling
@@ -348,7 +481,7 @@ const CacheHandler = {
348
481
 
349
482
  // if we have not skipped cache, determine if we should update behind the scenes
350
483
  if (calcShouldBackgroundFetch(store, context.request, false, identifier)) {
351
- let promise = fetchContentAndHydrate(next, context, identifier, false, true);
484
+ const promise = fetchContentAndHydrate(next, context, identifier, false, true);
352
485
  store.requestManager._pending.set(context.id, promise);
353
486
  }
354
487
  const shouldHydrate = context.request[EnableHydration] || false;
@@ -361,10 +494,11 @@ const CacheHandler = {
361
494
  newError.content = content;
362
495
  throw newError;
363
496
  }
364
- return Promise.resolve(shouldHydrate ? maybeUpdateUiObjects(store, context.request, {
497
+ const result = shouldHydrate ? maybeUpdateUiObjects(store, context.request, {
365
498
  shouldHydrate,
366
499
  identifier
367
- }, peeked.content, true) : peeked.content);
500
+ }, peeked.content, true) : peeked.content;
501
+ return result;
368
502
  }
369
503
  };
370
504
  function copyDocumentProperties(target, source) {
@@ -378,6 +512,19 @@ function copyDocumentProperties(target, source) {
378
512
  target.errors = source.errors;
379
513
  }
380
514
  }
515
+ function isCacheAffecting(document) {
516
+ if (!isMutation(document.request)) {
517
+ return true;
518
+ }
519
+ // a mutation combined with a 204 has no cache impact when no known records were involved
520
+ // a createRecord with a 201 with an empty response and no known records should similarly
521
+ // have no cache impact
522
+
523
+ if (document.request.op === 'createRecord' && document.response?.status === 201) {
524
+ return document.content ? Object.keys(document.content).length > 0 : false;
525
+ }
526
+ return document.response?.status !== 204;
527
+ }
381
528
 
382
529
  /*
383
530
  * Returns the Cache instance associated with a given
@@ -7132,11 +7279,6 @@ class Store extends EmberObject {
7132
7279
  [SkipCache]: true
7133
7280
  }
7134
7281
  };
7135
-
7136
- // we lie here on the type because legacy doesn't have enough context
7137
- cache.willCommit(identifier, {
7138
- request
7139
- });
7140
7282
  return this.request(request).then(document => document.content);
7141
7283
  }
7142
7284
 
@@ -7493,4 +7635,4 @@ function normalizeModelName(modelName) {
7493
7635
  }
7494
7636
  assert(`normalizeModelName support has been removed`);
7495
7637
  }
7496
- export { CacheHandler as C, IdentifierArray as I, MUTATE as M, RecordArrayManager as R, Store as S, _clearCaches as _, setIdentifierGenerationMethod as a, setIdentifierUpdateMethod as b, setIdentifierForgetMethod as c, setIdentifierResetMethod as d, coerceId as e, Collection as f, notifyArray as g, SOURCE as h, isStableIdentifier as i, IDENTIFIER_ARRAY_TAG as j, fastPush as k, removeRecordDataFor as l, normalizeModelName as n, peekCache as p, recordIdentifierFor as r, storeFor as s };
7638
+ export { CacheHandler as C, IdentifierArray as I, MUTATE as M, RecordArrayManager as R, Store as S, _clearCaches as _, setIdentifierGenerationMethod as a, setIdentifierUpdateMethod as b, setIdentifierForgetMethod as c, setIdentifierResetMethod as d, constructResource as e, coerceId as f, ensureStringId as g, Collection as h, isStableIdentifier as i, notifyArray as j, SOURCE as k, IDENTIFIER_ARRAY_TAG as l, fastPush as m, normalizeModelName as n, removeRecordDataFor as o, peekCache as p, recordIdentifierFor as r, storeFor as s };