@ember-data/store 4.2.0-alpha.0 → 4.2.0-alpha.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.
@@ -30,7 +30,6 @@ import {
30
30
  } from '@ember-data/private-build-infra';
31
31
  import {
32
32
  DEPRECATE_DEFAULT_ADAPTER,
33
- DEPRECATE_DEFAULT_SERIALIZER,
34
33
  DEPRECATE_LEGACY_TEST_REGISTRATIONS,
35
34
  } from '@ember-data/private-build-infra/deprecations';
36
35
  import type {
@@ -1248,7 +1247,7 @@ abstract class CoreStore extends Service {
1248
1247
  return Promise.resolve(internalModel);
1249
1248
  }
1250
1249
 
1251
- _findByInternalModel(internalModel, options: { preload?: any } = {}) {
1250
+ _findByInternalModel(internalModel: InternalModel, options: FindOptions = {}) {
1252
1251
  if (options.preload) {
1253
1252
  this._backburner.join(() => {
1254
1253
  internalModel.preloadData(options.preload);
@@ -1263,7 +1262,7 @@ abstract class CoreStore extends Service {
1263
1262
  );
1264
1263
  }
1265
1264
 
1266
- _findEmptyInternalModel(internalModel, options) {
1265
+ _findEmptyInternalModel(internalModel: InternalModel, options: FindOptions) {
1267
1266
  if (internalModel.currentState.isEmpty) {
1268
1267
  return this._scheduleFetch(internalModel, options);
1269
1268
  }
@@ -1275,9 +1274,9 @@ abstract class CoreStore extends Service {
1275
1274
  }
1276
1275
  } else {
1277
1276
  if (internalModel.currentState.isLoading) {
1278
- let pending = this._fetchManager.getPendingFetch(internalModel.identifier);
1279
- if (pending) {
1280
- return pending.then(() => Promise.resolve(internalModel));
1277
+ let pendingRequest = this._fetchManager.getPendingFetch(internalModel.identifier, options);
1278
+ if (pendingRequest) {
1279
+ return pendingRequest.then(() => Promise.resolve(internalModel));
1281
1280
  }
1282
1281
  return this._scheduleFetch(internalModel, options);
1283
1282
  }
@@ -1729,7 +1728,7 @@ abstract class CoreStore extends Service {
1729
1728
  if (arguments.length === 1 && isMaybeIdentifier(identifier)) {
1730
1729
  let stableIdentifier = identifierCacheFor(this).peekRecordIdentifier(identifier);
1731
1730
  if (stableIdentifier) {
1732
- return internalModelFactoryFor(this).peek(stableIdentifier)?.getRecord();
1731
+ return internalModelFactoryFor(this).peek(stableIdentifier)?.getRecord() || null;
1733
1732
  }
1734
1733
  return null;
1735
1734
  }
@@ -2019,7 +2018,7 @@ abstract class CoreStore extends Service {
2019
2018
  if (internalModel) {
2020
2019
  // short circuit if we are already loading
2021
2020
  if (REQUEST_SERVICE) {
2022
- let pendingRequest = this._fetchManager.getPendingFetch(internalModel.identifier);
2021
+ let pendingRequest = this._fetchManager.getPendingFetch(internalModel.identifier, options);
2023
2022
  if (pendingRequest) {
2024
2023
  return pendingRequest.then(() => internalModel.getRecord());
2025
2024
  }
@@ -2051,6 +2050,10 @@ abstract class CoreStore extends Service {
2051
2050
  return resolve(null);
2052
2051
  }
2053
2052
 
2053
+ if (!internalModel) {
2054
+ assert(`No InternalModel found for ${resource.lid}`, internalModel);
2055
+ }
2056
+
2054
2057
  return this._findByInternalModel(internalModel, options);
2055
2058
  }
2056
2059
 
@@ -3601,10 +3604,6 @@ abstract class CoreStore extends Service {
3601
3604
  for an `App.ApplicationSerializer` (the default serializer for
3602
3605
  your entire application).
3603
3606
 
3604
- if no `App.ApplicationSerializer` is found, it will attempt
3605
- to get the `defaultSerializer` from the `PersonAdapter`
3606
- (`adapterFor('person')`).
3607
-
3608
3607
  If a serializer cannot be found on the adapter, it will fall back
3609
3608
  to an instance of `JSONSerializer`.
3610
3609
 
@@ -3672,31 +3671,6 @@ abstract class CoreStore extends Service {
3672
3671
  }
3673
3672
 
3674
3673
  let serializerName;
3675
- if (DEPRECATE_DEFAULT_SERIALIZER) {
3676
- // no model specific serializer or application serializer, check for the `defaultSerializer`
3677
- // property defined on the adapter
3678
- let adapter = this.adapterFor(modelName);
3679
- serializerName = get(adapter, 'defaultSerializer');
3680
-
3681
- deprecate(
3682
- `store.serializerFor("${modelName}") resolved the "${serializerName}" serializer via the deprecated \`adapter.defaultSerializer\` property.\n\n\tPreviously, if no application or type-specific serializer was specified, the store would attempt to lookup a serializer via the \`defaultSerializer\` property on the type's adapter. This behavior is deprecated in favor of explicitly defining a type-specific serializer or application serializer`,
3683
- !serializerName,
3684
- {
3685
- id: 'ember-data:default-serializer',
3686
- until: '4.0',
3687
- url: 'https://deprecations.emberjs.com/ember-data/v3.x/#toc_ember-data-default-serializers',
3688
- for: '@ember-data/store',
3689
- since: {
3690
- available: '3.15',
3691
- enabled: '3.15',
3692
- },
3693
- }
3694
- );
3695
-
3696
- serializer = serializerName
3697
- ? _serializerCache[serializerName] || owner.lookup(`serializer:${serializerName}`)
3698
- : undefined;
3699
- }
3700
3674
 
3701
3675
  if (DEPRECATE_LEGACY_TEST_REGISTRATIONS) {
3702
3676
  // in production this is handled by the re-export
@@ -3727,49 +3701,10 @@ abstract class CoreStore extends Service {
3727
3701
  }
3728
3702
  }
3729
3703
 
3730
- if (DEPRECATE_DEFAULT_SERIALIZER) {
3731
- // final fallback, no model specific serializer, no application serializer, no
3732
- // `serializer` property on store: use the convenience JSONSerializer
3733
- serializer = _serializerCache['-default'] || owner.lookup('serializer:-default');
3734
- if (DEBUG && HAS_EMBER_DATA_PACKAGE && HAS_SERIALIZER_PACKAGE && serializer === undefined) {
3735
- const JSONSerializer = require('@ember-data/serializer/json').default;
3736
- owner.register('serializer:-default', JSONSerializer);
3737
- serializer = owner.lookup('serializer:-default');
3738
-
3739
- serializer && deprecateTestRegistration('serializer', '-default');
3740
- }
3741
-
3742
- deprecate(
3743
- `store.serializerFor("${modelName}") resolved the "-default" serializer via the deprecated "-default" lookup fallback.\n\n\tPreviously, when no type-specific serializer, application serializer, or adapter.defaultSerializer had been defined by the app, the "-default" serializer would be used which defaulted to the \`JSONSerializer\`. This behavior is deprecated in favor of explicitly defining an application or type-specific serializer`,
3744
- !serializer,
3745
- {
3746
- id: 'ember-data:default-serializer',
3747
- until: '4.0',
3748
- url: 'https://deprecations.emberjs.com/ember-data/v3.x/#toc_ember-data-default-serializers',
3749
- for: '@ember-data/store',
3750
- since: {
3751
- available: '3.15',
3752
- enabled: '3.15',
3753
- },
3754
- }
3755
- );
3756
-
3757
- assert(
3758
- `No serializer was found for '${modelName}' and no 'application' serializer was found as a fallback`,
3759
- serializer !== undefined
3760
- );
3761
-
3762
- set(serializer, 'store', this);
3763
- _serializerCache[normalizedModelName] = serializer;
3764
- _serializerCache['-default'] = serializer;
3765
-
3766
- return serializer;
3767
- } else {
3768
- assert(
3769
- `No serializer was found for '${modelName}' and no 'application' serializer was found as a fallback`,
3770
- serializer !== undefined
3771
- );
3772
- }
3704
+ assert(
3705
+ `No serializer was found for '${modelName}' and no 'application' serializer was found as a fallback`,
3706
+ serializer !== undefined
3707
+ );
3773
3708
  }
3774
3709
 
3775
3710
  destroy() {
@@ -37,9 +37,10 @@ class Store extends CoreStore {
37
37
  let createOptions: any = {
38
38
  store: this,
39
39
  _internalModel: internalModel,
40
+ // TODO deprecate allowing unknown args setting
41
+ _createProps: createRecordArgs,
40
42
  container: null,
41
43
  };
42
- Object.assign(createOptions, createRecordArgs);
43
44
 
44
45
  // ensure that `getOwner(this)` works inside a model instance
45
46
  setOwner(createOptions, getOwner(this));
@@ -501,10 +501,10 @@ export default class FetchManager {
501
501
  }
502
502
  }
503
503
 
504
- getPendingFetch(identifier: StableRecordIdentifier) {
505
- let pendingRequests = this.requestCache
506
- .getPendingRequestsForRecord(identifier)
507
- .filter((req) => req.type === 'query');
504
+ getPendingFetch(identifier: StableRecordIdentifier, options) {
505
+ let pendingRequests = this.requestCache.getPendingRequestsForRecord(identifier).filter((req) => {
506
+ return req.type === 'query' && isSameRequest(options, req.request.data[0].options);
507
+ });
508
508
 
509
509
  if (pendingRequests.length > 0) {
510
510
  return pendingRequests[0][RequestPromise];
@@ -532,3 +532,8 @@ function assertIsString(id: string | null): asserts id is string {
532
532
  }
533
533
  }
534
534
  }
535
+
536
+ // this function helps resolve whether we have a pending request that we should use instead
537
+ function isSameRequest(options: Dict<unknown> = {}, reqOptions: Dict<unknown> = {}) {
538
+ return options.include === reqOptions.include;
539
+ }
@@ -24,6 +24,7 @@ import type {
24
24
  import type { UpgradedMeta } from '@ember-data/record-data/-private/graph/-edge-definition';
25
25
 
26
26
  import { identifierCacheFor } from '../../identifiers/cache';
27
+ import { DSModel } from '../../ts-interfaces/ds-model';
27
28
  import type { StableRecordIdentifier } from '../../ts-interfaces/identifier';
28
29
  import type { RecordData } from '../../ts-interfaces/record-data';
29
30
  import type { JsonApiResource, JsonApiValidationError } from '../../ts-interfaces/record-data-json-api';
@@ -127,7 +128,7 @@ export default class InternalModel {
127
128
  declare _deferredTriggers: any;
128
129
  declare __recordArrays: any;
129
130
  declare references: any;
130
- declare _recordReference: any;
131
+ declare _recordReference: RecordReference;
131
132
  declare _manyArrayCache: ConfidentDict<ManyArray>;
132
133
 
133
134
  declare _relationshipPromisesCache: ConfidentDict<RSVP.Promise<any>>;
@@ -199,7 +200,7 @@ export default class InternalModel {
199
200
  }
200
201
  }
201
202
 
202
- get recordReference() {
203
+ get recordReference(): RecordReference {
203
204
  if (this._recordReference === null) {
204
205
  this._recordReference = new RecordReference(this.store, this.identifier);
205
206
  }
@@ -291,7 +292,7 @@ export default class InternalModel {
291
292
  }
292
293
  }
293
294
 
294
- getRecord(properties?) {
295
+ getRecord(properties?): Object {
295
296
  if (!this._record && !this._isDematerializing) {
296
297
  let { store } = this;
297
298
 
@@ -613,7 +614,7 @@ export default class InternalModel {
613
614
  "' with id " +
614
615
  parentInternalModel.id +
615
616
  ' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (`belongsTo({ async: true })`)',
616
- toReturn === null || !toReturn.get('isEmpty')
617
+ toReturn === null || !(toReturn as DSModel).isEmpty
617
618
  );
618
619
  return toReturn;
619
620
  }
@@ -672,7 +673,7 @@ export default class InternalModel {
672
673
  assert(`hasMany only works with the @ember-data/record-data package`);
673
674
  }
674
675
 
675
- getHasMany(key: string, options) {
676
+ getHasMany(key: string, options?) {
676
677
  if (HAS_RECORD_DATA_PACKAGE) {
677
678
  const graphFor = require('@ember-data/record-data/-private').graphFor;
678
679
  const relationship = graphFor(this.store).get(this.identifier, key);
@@ -790,11 +791,22 @@ export default class InternalModel {
790
791
  !this._record || this._record.get('isDestroyed') || this._record.get('isDestroying')
791
792
  );
792
793
  this.isDestroying = true;
794
+ if (this._recordReference) {
795
+ this._recordReference.destroy();
796
+ }
797
+ this._recordReference = null;
793
798
  let cache = this._manyArrayCache;
794
799
  Object.keys(cache).forEach((key) => {
795
800
  cache[key].destroy();
796
801
  delete cache[key];
797
802
  });
803
+ if (this.references) {
804
+ cache = this.references;
805
+ Object.keys(cache).forEach((key) => {
806
+ cache[key].destroy();
807
+ delete cache[key];
808
+ });
809
+ }
798
810
 
799
811
  internalModelFactoryFor(this.store).remove(this);
800
812
  this._isDestroyed = true;
@@ -803,6 +815,7 @@ export default class InternalModel {
803
815
  setupData(data) {
804
816
  let changedKeys = this._recordData.pushData(data, this.hasRecord);
805
817
  if (this.hasRecord) {
818
+ // TODO @runspired should this be going through the notification manager?
806
819
  this._record._notifyProperties(changedKeys);
807
820
  }
808
821
  this.send('pushedData');
@@ -902,11 +915,18 @@ export default class InternalModel {
902
915
 
903
916
  notifyHasManyChange(key: string) {
904
917
  if (this.hasRecord) {
918
+ let manyArray = this._manyArrayCache[key];
919
+ let hasPromise = !!this._relationshipPromisesCache[key];
920
+
921
+ if (manyArray && hasPromise) {
922
+ // do nothing, we will notify the ManyArray directly
923
+ // once the fetch has completed.
924
+ return;
925
+ }
926
+
905
927
  if (CUSTOM_MODEL_CLASS) {
906
928
  this.store._notificationManager.notify(this.identifier, 'relationships', key);
907
929
  } else {
908
- let manyArray = this._manyArrayCache[key];
909
-
910
930
  if (manyArray) {
911
931
  manyArray.notify();
912
932
 
@@ -956,10 +976,10 @@ export default class InternalModel {
956
976
  this.store._notificationManager.notify(this.identifier, 'state');
957
977
  } else {
958
978
  if (!key || key === 'isNew') {
959
- this.getRecord().notifyPropertyChange('isNew');
979
+ (this.getRecord() as DSModel).notifyPropertyChange('isNew');
960
980
  }
961
981
  if (!key || key === 'isDeleted') {
962
- this.getRecord().notifyPropertyChange('isDeleted');
982
+ (this.getRecord() as DSModel).notifyPropertyChange('isDeleted');
963
983
  }
964
984
  }
965
985
  }
@@ -1263,12 +1283,12 @@ export default class InternalModel {
1263
1283
  if (this._recordData.getErrors) {
1264
1284
  return this._recordData.getErrors(this.identifier).length > 0;
1265
1285
  } else {
1266
- let errors = get(this.getRecord(), 'errors');
1267
- return errors.get('length') > 0;
1286
+ let errors = (this.getRecord() as DSModel).errors;
1287
+ return errors.length > 0;
1268
1288
  }
1269
1289
  } else {
1270
- let errors = get(this.getRecord(), 'errors');
1271
- return errors.get('length') > 0;
1290
+ let errors = (this.getRecord() as DSModel).errors;
1291
+ return errors.length > 0;
1272
1292
  }
1273
1293
  }
1274
1294
 
@@ -1283,7 +1303,7 @@ export default class InternalModel {
1283
1303
  if (!this._recordData.getErrors) {
1284
1304
  for (attribute in parsedErrors) {
1285
1305
  if (hasOwnProperty.call(parsedErrors, attribute)) {
1286
- this.getRecord().errors._add(attribute, parsedErrors[attribute]);
1306
+ (this.getRecord() as DSModel).errors._add(attribute, parsedErrors[attribute]);
1287
1307
  }
1288
1308
  }
1289
1309
  }
@@ -1303,7 +1323,7 @@ export default class InternalModel {
1303
1323
 
1304
1324
  for (attribute in parsedErrors) {
1305
1325
  if (hasOwnProperty.call(parsedErrors, attribute)) {
1306
- this.getRecord().errors._add(attribute, parsedErrors[attribute]);
1326
+ (this.getRecord() as DSModel).errors._add(attribute, parsedErrors[attribute]);
1307
1327
  }
1308
1328
  }
1309
1329
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { assert } from '@ember/debug';
5
5
 
6
- import { REQUEST_SERVICE } from '@ember-data/canary-features';
6
+ import { CUSTOM_MODEL_CLASS, REQUEST_SERVICE } from '@ember-data/canary-features';
7
7
  /*
8
8
  This file encapsulates the various states that a record can transition
9
9
  through during its lifecycle.
@@ -431,6 +431,12 @@ createdState.uncommitted.rollback = function (internalModel) {
431
431
  };
432
432
 
433
433
  createdState.uncommitted.pushedData = function (internalModel) {
434
+ // TODO @runspired consider where to do this once we kill off state machine
435
+ if (CUSTOM_MODEL_CLASS) {
436
+ internalModel.store._notificationManager.notify(internalModel.identifier, 'identity');
437
+ } else {
438
+ internalModel.notifyPropertyChange('id');
439
+ }
434
440
  internalModel.transitionTo('loaded.updated.uncommitted');
435
441
  internalModel.triggerLater('didLoad');
436
442
  };
@@ -4,7 +4,7 @@ import type CoreStore from './core-store';
4
4
 
5
5
  type UnsubscribeToken = Object;
6
6
 
7
- const Cache = new WeakMap<StableRecordIdentifier, NotificationCallback>();
7
+ const Cache = new WeakMap<StableRecordIdentifier, Map<UnsubscribeToken, NotificationCallback>>();
8
8
  const Tokens = new WeakMap<UnsubscribeToken, StableRecordIdentifier>();
9
9
 
10
10
  export type NotificationType =
@@ -29,7 +29,8 @@ export function unsubscribe(token: UnsubscribeToken) {
29
29
  throw new Error('Passed unknown unsubscribe token to unsubscribe');
30
30
  }
31
31
  Tokens.delete(token);
32
- Cache.delete(identifier);
32
+ const map = Cache.get(identifier);
33
+ map?.delete(token);
33
34
  }
34
35
  /*
35
36
  Currently only support a single callback per identifier
@@ -39,8 +40,13 @@ export default class NotificationManager {
39
40
 
40
41
  subscribe(identifier: RecordIdentifier, callback: NotificationCallback): UnsubscribeToken {
41
42
  let stableIdentifier = identifierCacheFor(this.store).getOrCreateRecordIdentifier(identifier);
42
- Cache.set(stableIdentifier, callback);
43
+ let map = Cache.get(stableIdentifier);
44
+ if (map === undefined) {
45
+ map = new Map();
46
+ Cache.set(stableIdentifier, map);
47
+ }
43
48
  let unsubToken = {};
49
+ map.set(unsubToken, callback);
44
50
  Tokens.set(unsubToken, stableIdentifier);
45
51
  return unsubToken;
46
52
  }
@@ -49,11 +55,13 @@ export default class NotificationManager {
49
55
  notify(identifier: RecordIdentifier, value: 'errors' | 'meta' | 'identity' | 'unload' | 'state'): boolean;
50
56
  notify(identifier: RecordIdentifier, value: NotificationType, key?: string): boolean {
51
57
  let stableIdentifier = identifierCacheFor(this.store).getOrCreateRecordIdentifier(identifier);
52
- let callback = Cache.get(stableIdentifier);
53
- if (!callback) {
58
+ let callbackMap = Cache.get(stableIdentifier);
59
+ if (!callbackMap || !callbackMap.size) {
54
60
  return false;
55
61
  }
56
- callback(stableIdentifier, value, key);
62
+ callbackMap.forEach((cb) => {
63
+ cb(stableIdentifier, value, key);
64
+ });
57
65
  return true;
58
66
  }
59
67
  }
@@ -1,11 +1,20 @@
1
1
  import { deprecate } from '@ember/debug';
2
+ import { dependentKeyCompat } from '@ember/object/compat';
3
+ import { cached, tracked } from '@glimmer/tracking';
2
4
 
3
5
  import { resolve } from 'rsvp';
4
6
 
7
+ import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features';
5
8
  import { DEPRECATE_BELONGS_TO_REFERENCE_PUSH } from '@ember-data/private-build-infra/deprecations';
9
+ import type { BelongsToRelationship } from '@ember-data/record-data/-private';
6
10
  import { assertPolymorphicType } from '@ember-data/store/-debug';
7
11
 
12
+ import { SingleResourceDocument } from '../../ts-interfaces/ember-data-json-api';
13
+ import { StableRecordIdentifier } from '../../ts-interfaces/identifier';
14
+ import CoreStore from '../core-store';
15
+ import { NotificationType, unsubscribe } from '../record-notification-manager';
8
16
  import { internalModelFactoryFor, peekRecordIdentifier, recordIdentifierFor } from '../store/internal-model-factory';
17
+ import RecordReference from './record';
9
18
  import Reference from './reference';
10
19
 
11
20
  /**
@@ -22,17 +31,81 @@ import Reference from './reference';
22
31
  @extends Reference
23
32
  */
24
33
  export default class BelongsToReference extends Reference {
25
- constructor(store, parentIMOrIdentifier, belongsToRelationship, key) {
26
- super(store, parentIMOrIdentifier);
34
+ declare key: string;
35
+ declare belongsToRelationship: BelongsToRelationship;
36
+ declare type: string;
37
+ declare parent: RecordReference;
38
+ declare parentIdentifier: StableRecordIdentifier;
39
+
40
+ // unsubscribe tokens given to us by the notification manager
41
+ #token!: Object;
42
+ #relatedToken: Object | null = null;
43
+
44
+ @tracked _ref = 0;
45
+
46
+ constructor(
47
+ store: CoreStore,
48
+ parentIdentifier: StableRecordIdentifier,
49
+ belongsToRelationship: BelongsToRelationship,
50
+ key: string
51
+ ) {
52
+ super(store, parentIdentifier);
27
53
  this.key = key;
28
54
  this.belongsToRelationship = belongsToRelationship;
29
55
  this.type = belongsToRelationship.definition.type;
30
- this.parent = internalModelFactoryFor(store).peek(parentIMOrIdentifier).recordReference;
31
- this.parentIdentifier = parentIMOrIdentifier;
56
+ const parent = internalModelFactoryFor(store).peek(parentIdentifier);
57
+ this.parent = parent!.recordReference;
58
+ this.parentIdentifier = parentIdentifier;
59
+
60
+ if (CUSTOM_MODEL_CLASS) {
61
+ this.#token = store._notificationManager.subscribe(
62
+ parentIdentifier,
63
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
64
+ if ((bucket === 'relationships' || bucket === 'property') && notifiedKey === key) {
65
+ this._ref++;
66
+ }
67
+ }
68
+ );
69
+ }
32
70
 
33
71
  // TODO inverse
34
72
  }
35
73
 
74
+ destroy() {
75
+ if (CUSTOM_MODEL_CLASS) {
76
+ unsubscribe(this.#token);
77
+ if (this.#relatedToken) {
78
+ unsubscribe(this.#relatedToken);
79
+ }
80
+ }
81
+ }
82
+
83
+ @cached
84
+ @dependentKeyCompat
85
+ get _relatedIdentifier(): StableRecordIdentifier | null {
86
+ this._ref; // consume the tracked prop
87
+ if (this.#relatedToken) {
88
+ unsubscribe(this.#relatedToken);
89
+ }
90
+
91
+ let resource = this._resource();
92
+ if (resource && resource.data) {
93
+ const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource.data);
94
+ this.#relatedToken = this.store._notificationManager.subscribe(
95
+ identifier,
96
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
97
+ if (bucket === 'identity' || ((bucket === 'attributes' || bucket === 'property') && notifiedKey === 'id')) {
98
+ this._ref++;
99
+ }
100
+ }
101
+ );
102
+
103
+ return identifier;
104
+ }
105
+
106
+ return null;
107
+ }
108
+
36
109
  /**
37
110
  The `id` of the record that this reference refers to. Together, the
38
111
  `type()` and `id()` methods form a composite key for the identity
@@ -73,13 +146,18 @@ export default class BelongsToReference extends Reference {
73
146
  @public
74
147
  @return {String} The id of the record in this belongsTo relationship.
75
148
  */
76
- id() {
77
- let id = null;
149
+ id(): string | null {
150
+ if (CUSTOM_MODEL_CLASS) {
151
+ return this._relatedIdentifier?.id || null;
152
+ }
78
153
  let resource = this._resource();
79
154
  if (resource && resource.data) {
80
- id = resource.data.id;
155
+ const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource.data);
156
+
157
+ return identifier.id;
81
158
  }
82
- return id;
159
+
160
+ return null;
83
161
  }
84
162
 
85
163
  _resource() {
@@ -132,10 +210,10 @@ export default class BelongsToReference extends Reference {
132
210
  @param {Object|Promise} objectOrPromise a promise that resolves to a JSONAPI document object describing the new value of this relationship.
133
211
  @return {Promise<record>} A promise that resolves with the new value in this belongs-to relationship.
134
212
  */
135
- push(objectOrPromise) {
213
+ async push(objectOrPromise: Object | SingleResourceDocument): Promise<Object> {
136
214
  // TODO deprecate thenable support
137
215
  return resolve(objectOrPromise).then((data) => {
138
- let record;
216
+ let record: Object;
139
217
 
140
218
  if (DEPRECATE_BELONGS_TO_REFERENCE_PUSH && peekRecordIdentifier(data)) {
141
219
  deprecate('Pushing a record into a BelongsToReference is deprecated', false, {
@@ -147,15 +225,16 @@ export default class BelongsToReference extends Reference {
147
225
  enabled: '3.16',
148
226
  },
149
227
  });
150
- record = data;
228
+ record = data as Object;
151
229
  } else {
152
- record = this.store.push(data);
230
+ record = this.store.push(data as SingleResourceDocument);
153
231
  }
154
232
 
233
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
155
234
  assertPolymorphicType(
156
235
  this.belongsToRelationship.identifier,
157
236
  this.belongsToRelationship.definition,
158
- record._internalModel.identifier,
237
+ recordIdentifierFor(record),
159
238
  this.store
160
239
  );
161
240
 
@@ -223,7 +302,7 @@ export default class BelongsToReference extends Reference {
223
302
  @public
224
303
  @return {Model} the record in this relationship
225
304
  */
226
- value() {
305
+ value(): Object | null {
227
306
  let resource = this._resource();
228
307
  if (resource && resource.data) {
229
308
  let inverseInternalModel = this.store._internalModelForResource(resource.data);
@@ -299,7 +378,7 @@ export default class BelongsToReference extends Reference {
299
378
  */
300
379
  load(options) {
301
380
  let parentInternalModel = internalModelFactoryFor(this.store).peek(this.parentIdentifier);
302
- return parentInternalModel.getBelongsTo(this.key, options);
381
+ return parentInternalModel!.getBelongsTo(this.key, options);
303
382
  }
304
383
 
305
384
  /**
@@ -354,7 +433,7 @@ export default class BelongsToReference extends Reference {
354
433
  */
355
434
  reload(options) {
356
435
  let parentInternalModel = internalModelFactoryFor(this.store).peek(this.parentIdentifier);
357
- return parentInternalModel.reloadBelongsTo(this.key, options).then((internalModel) => {
436
+ return parentInternalModel!.reloadBelongsTo(this.key, options).then((internalModel) => {
358
437
  return this.value();
359
438
  });
360
439
  }
@@ -1,10 +1,23 @@
1
+ import { dependentKeyCompat } from '@ember/object/compat';
1
2
  import { DEBUG } from '@glimmer/env';
3
+ import { cached, tracked } from '@glimmer/tracking';
2
4
 
3
5
  import { resolve } from 'rsvp';
4
6
 
7
+ import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features';
8
+ import type { ManyRelationship } from '@ember-data/record-data/-private';
5
9
  import { assertPolymorphicType } from '@ember-data/store/-debug';
6
10
 
11
+ import {
12
+ CollectionResourceDocument,
13
+ ExistingResourceObject,
14
+ SingleResourceDocument,
15
+ } from '../../ts-interfaces/ember-data-json-api';
16
+ import { StableRecordIdentifier } from '../../ts-interfaces/identifier';
17
+ import CoreStore from '../core-store';
18
+ import { NotificationType, unsubscribe } from '../record-notification-manager';
7
19
  import { internalModelFactoryFor, recordIdentifierFor } from '../store/internal-model-factory';
20
+ import RecordReference from './record';
8
21
  import Reference, { internalModelForReference } from './reference';
9
22
 
10
23
  /**
@@ -20,17 +33,88 @@ import Reference, { internalModelForReference } from './reference';
20
33
  @extends Reference
21
34
  */
22
35
  export default class HasManyReference extends Reference {
23
- constructor(store, parentIMOrIdentifier, hasManyRelationship, key) {
24
- super(store, parentIMOrIdentifier);
36
+ declare key: string;
37
+ declare hasManyRelationship: ManyRelationship;
38
+ declare type: string;
39
+ declare parent: RecordReference;
40
+ declare parentIdentifier: StableRecordIdentifier;
41
+
42
+ // unsubscribe tokens given to us by the notification manager
43
+ #token!: Object;
44
+ #relatedTokenMap!: Map<StableRecordIdentifier, Object>;
45
+
46
+ @tracked _ref = 0;
47
+
48
+ constructor(
49
+ store: CoreStore,
50
+ parentIdentifier: StableRecordIdentifier,
51
+ hasManyRelationship: ManyRelationship,
52
+ key: string
53
+ ) {
54
+ super(store, parentIdentifier);
25
55
  this.key = key;
26
56
  this.hasManyRelationship = hasManyRelationship;
27
57
  this.type = hasManyRelationship.definition.type;
28
58
 
29
- this.parent = internalModelFactoryFor(store).peek(parentIMOrIdentifier).recordReference;
59
+ this.parent = internalModelFactoryFor(store).peek(parentIdentifier)!.recordReference;
30
60
 
61
+ if (CUSTOM_MODEL_CLASS) {
62
+ this.#token = store._notificationManager.subscribe(
63
+ parentIdentifier,
64
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
65
+ if ((bucket === 'relationships' || bucket === 'property') && notifiedKey === key) {
66
+ this._ref++;
67
+ }
68
+ }
69
+ );
70
+ this.#relatedTokenMap = new Map();
71
+ }
31
72
  // TODO inverse
32
73
  }
33
74
 
75
+ destroy() {
76
+ if (CUSTOM_MODEL_CLASS) {
77
+ unsubscribe(this.#token);
78
+ this.#relatedTokenMap.forEach((token) => {
79
+ unsubscribe(token);
80
+ });
81
+ this.#relatedTokenMap.clear();
82
+ }
83
+ }
84
+
85
+ @cached
86
+ @dependentKeyCompat
87
+ get _relatedIdentifiers(): StableRecordIdentifier[] {
88
+ this._ref; // consume the tracked prop
89
+
90
+ let resource = this._resource();
91
+
92
+ this.#relatedTokenMap.forEach((token) => {
93
+ unsubscribe(token);
94
+ });
95
+ this.#relatedTokenMap.clear();
96
+
97
+ if (resource && resource.data) {
98
+ return resource.data.map((resourceIdentifier) => {
99
+ const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resourceIdentifier);
100
+ const token = this.store._notificationManager.subscribe(
101
+ identifier,
102
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
103
+ if (bucket === 'identity' || ((bucket === 'attributes' || bucket === 'property') && notifiedKey === 'id')) {
104
+ this._ref++;
105
+ }
106
+ }
107
+ );
108
+
109
+ this.#relatedTokenMap.set(identifier, token);
110
+
111
+ return identifier;
112
+ });
113
+ }
114
+
115
+ return [];
116
+ }
117
+
34
118
  _resource() {
35
119
  return this.recordData.getHasMany(this.key);
36
120
  }
@@ -77,7 +161,7 @@ export default class HasManyReference extends Reference {
77
161
  @public
78
162
  @return {String} The name of the remote type. This should either be `link` or `ids`
79
163
  */
80
- remoteType() {
164
+ remoteType(): 'link' | 'ids' {
81
165
  let value = this._resource();
82
166
  if (value && value.links && value.links.related) {
83
167
  return 'link';
@@ -121,15 +205,22 @@ export default class HasManyReference extends Reference {
121
205
  @public
122
206
  @return {Array} The ids in this has-many relationship
123
207
  */
124
- ids() {
208
+ ids(): Array<string | null> {
209
+ if (CUSTOM_MODEL_CLASS) {
210
+ return this._relatedIdentifiers.map((identifier) => identifier.id);
211
+ }
212
+
125
213
  let resource = this._resource();
126
214
 
127
- let ids = [];
128
- if (resource.data) {
129
- ids = resource.data.map((data) => data.id);
215
+ if (resource && resource.data) {
216
+ return resource.data.map((resourceIdentifier) => {
217
+ const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resourceIdentifier);
218
+
219
+ return identifier.id;
220
+ });
130
221
  }
131
222
 
132
- return ids;
223
+ return [];
133
224
  }
134
225
 
135
226
  /**
@@ -177,45 +268,50 @@ export default class HasManyReference extends Reference {
177
268
  @param {Array|Promise} objectOrPromise a promise that resolves to a JSONAPI document object describing the new value of this relationship.
178
269
  @return {ManyArray}
179
270
  */
180
- push(objectOrPromise) {
181
- return resolve(objectOrPromise).then((payload) => {
182
- let array = payload;
183
-
184
- if (typeof payload === 'object' && payload.data) {
185
- array = payload.data;
186
- }
271
+ async push(
272
+ objectOrPromise: ExistingResourceObject[] | CollectionResourceDocument | { data: SingleResourceDocument[] }
273
+ ): Promise<any> {
274
+ const payload = await resolve(objectOrPromise);
275
+ let array: Array<ExistingResourceObject | SingleResourceDocument>;
276
+
277
+ if (!Array.isArray(payload) && typeof payload === 'object' && Array.isArray(payload.data)) {
278
+ array = payload.data;
279
+ } else {
280
+ array = payload as ExistingResourceObject[];
281
+ }
187
282
 
188
- let internalModel = internalModelForReference(this);
283
+ const internalModel = internalModelForReference(this)!;
284
+ const { store } = this;
189
285
 
190
- let identifiers = array.map((obj) => {
191
- let record = this.store.push(obj);
286
+ let identifiers = array.map((obj) => {
287
+ let record;
288
+ if ('data' in obj) {
289
+ // TODO deprecate pushing non-valid JSON:API here
290
+ record = store.push(obj);
291
+ } else {
292
+ record = store.push({ data: obj });
293
+ }
192
294
 
193
- if (DEBUG) {
194
- let relationshipMeta = this.hasManyRelationship.definition;
195
- assertPolymorphicType(
196
- internalModel.identifier,
197
- relationshipMeta,
198
- record._internalModel.identifier,
199
- this.store
200
- );
201
- }
202
- return recordIdentifierFor(record);
203
- });
295
+ if (DEBUG) {
296
+ let relationshipMeta = this.hasManyRelationship.definition;
297
+ let identifier = this.hasManyRelationship.identifier;
298
+ assertPolymorphicType(identifier, relationshipMeta, recordIdentifierFor(record), store);
299
+ }
300
+ return recordIdentifierFor(record);
301
+ });
204
302
 
205
- const { graph, identifier } = this.hasManyRelationship;
206
- this.store._backburner.join(() => {
207
- graph.push({
208
- op: 'replaceRelatedRecords',
209
- record: identifier,
210
- field: this.key,
211
- value: identifiers,
212
- });
303
+ const { graph, identifier } = this.hasManyRelationship;
304
+ store._backburner.join(() => {
305
+ graph.push({
306
+ op: 'replaceRelatedRecords',
307
+ record: identifier,
308
+ field: this.key,
309
+ value: identifiers,
213
310
  });
214
-
215
- return internalModel.getHasMany(this.key);
216
- // TODO IGOR it seems wrong that we were returning the many array here
217
- //return this.hasManyRelationship.manyArray;
218
311
  });
312
+
313
+ // TODO IGOR it seems wrong that we were returning the many array here
314
+ return internalModel.getHasMany(this.key);
219
315
  }
220
316
 
221
317
  _isLoaded() {
@@ -275,7 +371,7 @@ export default class HasManyReference extends Reference {
275
371
  @return {ManyArray}
276
372
  */
277
373
  value() {
278
- let internalModel = internalModelForReference(this);
374
+ let internalModel = internalModelForReference(this)!;
279
375
  if (this._isLoaded()) {
280
376
  return internalModel.getManyArray(this.key);
281
377
  }
@@ -348,7 +444,7 @@ export default class HasManyReference extends Reference {
348
444
  this has-many relationship.
349
445
  */
350
446
  load(options) {
351
- let internalModel = internalModelForReference(this);
447
+ let internalModel = internalModelForReference(this)!;
352
448
  return internalModel.getHasMany(this.key, options);
353
449
  }
354
450
 
@@ -403,7 +499,7 @@ export default class HasManyReference extends Reference {
403
499
  @return {Promise} a promise that resolves with the ManyArray in this has-many relationship.
404
500
  */
405
501
  reload(options) {
406
- let internalModel = internalModelForReference(this);
502
+ let internalModel = internalModelForReference(this)!;
407
503
  return internalModel.reloadHasMany(this.key, options);
408
504
  }
409
505
  }
@@ -1,8 +1,15 @@
1
+ import { dependentKeyCompat } from '@ember/object/compat';
2
+ import { cached, tracked } from '@glimmer/tracking';
3
+
1
4
  import RSVP, { resolve } from 'rsvp';
2
5
 
6
+ import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features';
7
+
3
8
  import type { SingleResourceDocument } from '../../ts-interfaces/ember-data-json-api';
4
9
  import type { StableRecordIdentifier } from '../../ts-interfaces/identifier';
5
10
  import type { RecordInstance } from '../../ts-interfaces/record-instance';
11
+ import type CoreStore from '../core-store';
12
+ import { NotificationType, unsubscribe } from '../record-notification-manager';
6
13
  import Reference, { internalModelForReference, REFERENCE_CACHE } from './reference';
7
14
 
8
15
  /**
@@ -18,11 +25,39 @@ import Reference, { internalModelForReference, REFERENCE_CACHE } from './referen
18
25
  @extends Reference
19
26
  */
20
27
  export default class RecordReference extends Reference {
28
+ // unsubscribe token given to us by the notification manager
29
+ #token!: Object;
30
+
31
+ @tracked _ref = 0;
32
+
33
+ constructor(public store: CoreStore, identifier: StableRecordIdentifier) {
34
+ super(store, identifier);
35
+ if (CUSTOM_MODEL_CLASS) {
36
+ this.#token = store._notificationManager.subscribe(
37
+ identifier,
38
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
39
+ if (bucket === 'identity' || ((bucket === 'attributes' || bucket === 'property') && notifiedKey === 'id')) {
40
+ this._ref++;
41
+ }
42
+ }
43
+ );
44
+ }
45
+ }
46
+
47
+ destroy() {
48
+ if (CUSTOM_MODEL_CLASS) {
49
+ unsubscribe(this.#token);
50
+ }
51
+ }
52
+
21
53
  public get type(): string {
22
54
  return this.identifier().type;
23
55
  }
24
56
 
57
+ @cached
58
+ @dependentKeyCompat
25
59
  private get _id(): string | null {
60
+ this._ref; // consume the tracked prop
26
61
  let identifier = this.identifier();
27
62
  if (identifier) {
28
63
  return identifier.id;
@@ -50,7 +85,15 @@ export default class RecordReference extends Reference {
50
85
  @return {String} The id of the record.
51
86
  */
52
87
  id() {
53
- return this._id;
88
+ if (CUSTOM_MODEL_CLASS) {
89
+ return this._id;
90
+ }
91
+ let identifier = this.identifier();
92
+ if (identifier) {
93
+ return identifier.id;
94
+ }
95
+
96
+ return null;
54
97
  }
55
98
 
56
99
  /**
@@ -92,7 +135,7 @@ export default class RecordReference extends Reference {
92
135
  @public
93
136
  @return {String} 'identity'
94
137
  */
95
- remoteType(): 'link' | 'id' | 'identity' {
138
+ remoteType(): 'identity' {
96
139
  return 'identity';
97
140
  }
98
141
 
@@ -159,7 +202,7 @@ export default class RecordReference extends Reference {
159
202
  @return {Model} the record for this RecordReference
160
203
  */
161
204
  value(): RecordInstance | null {
162
- if (this._id !== null) {
205
+ if (this.id() !== null) {
163
206
  let internalModel = internalModelForReference(this);
164
207
  if (internalModel && internalModel.currentState.isLoaded) {
165
208
  return internalModel.getRecord();
@@ -186,8 +229,9 @@ export default class RecordReference extends Reference {
186
229
  @return {Promise<record>} the record for this RecordReference
187
230
  */
188
231
  load() {
189
- if (this._id !== null) {
190
- return this.store.findRecord(this.type, this._id);
232
+ const id = this.id();
233
+ if (id !== null) {
234
+ return this.store.findRecord(this.type, id);
191
235
  }
192
236
  throw new Error(`Unable to fetch record of type ${this.type} without an id`);
193
237
  }
@@ -210,8 +254,9 @@ export default class RecordReference extends Reference {
210
254
  @return {Promise<record>} the record for this RecordReference
211
255
  */
212
256
  reload() {
213
- if (this._id !== null) {
214
- return this.store.findRecord(this.type, this._id, { reload: true });
257
+ const id = this.id();
258
+ if (id !== null) {
259
+ return this.store.findRecord(this.type, id, { reload: true });
215
260
  }
216
261
  throw new Error(`Unable to fetch record of type ${this.type} without an id`);
217
262
  }
@@ -94,7 +94,7 @@ abstract class Reference {
94
94
  @public
95
95
  @return {String} The name of the remote type. This should either be "link" or "ids"
96
96
  */
97
- remoteType(): 'link' | 'id' | 'identity' {
97
+ remoteType(): 'link' | 'id' | 'ids' | 'identity' {
98
98
  let value = this._resource();
99
99
  if (isResourceIdentiferWithRelatedLinks(value)) {
100
100
  return 'link';
@@ -17,6 +17,7 @@ export interface DSModel extends RecordInstance, EmberObject {
17
17
  isDeleted: boolean;
18
18
  deleteRecord(): void;
19
19
  unloadRecord(): void;
20
+ errors: any;
20
21
  }
21
22
 
22
23
  // Implemented by both ShimModelClass and DSModel
@@ -1,7 +1,11 @@
1
+ import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils';
2
+
1
3
  import type { RecordIdentifier } from './identifier';
2
4
 
3
5
  export interface Operation {
4
6
  op: string;
7
+ options: Dict<unknown> | undefined;
8
+ recordIdentifier: RecordIdentifier;
5
9
  }
6
10
 
7
11
  export interface FindRecordQuery extends Operation {
package/index.js CHANGED
@@ -10,6 +10,8 @@ module.exports = Object.assign({}, addonBaseConfig, {
10
10
  shouldRollupPrivate: true,
11
11
  externalDependenciesForPrivateModule() {
12
12
  return [
13
+ 'ember-cached-decorator-polyfill',
14
+
13
15
  '@ember-data/canary-features',
14
16
  '@ember-data/store/-debug',
15
17
 
@@ -23,6 +25,7 @@ module.exports = Object.assign({}, addonBaseConfig, {
23
25
  '@ember/object/evented',
24
26
  '@ember/object/internals',
25
27
  '@ember/object/mixin',
28
+ '@ember/object/compat',
26
29
  '@ember/object/promise-proxy-mixin',
27
30
  '@ember/object/proxy',
28
31
  '@ember/polyfills',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ember-data/store",
3
- "version": "4.2.0-alpha.0",
3
+ "version": "4.2.0-alpha.1",
4
4
  "description": "The default blueprint for ember-cli addons.",
5
5
  "keywords": [
6
6
  "ember-addon"
@@ -17,17 +17,18 @@
17
17
  "start": "ember serve"
18
18
  },
19
19
  "dependencies": {
20
- "@ember-data/canary-features": "4.2.0-alpha.0",
21
- "@ember-data/private-build-infra": "4.2.0-alpha.0",
20
+ "@ember-data/canary-features": "4.2.0-alpha.1",
21
+ "@ember-data/private-build-infra": "4.2.0-alpha.1",
22
22
  "@ember/string": "^3.0.0",
23
23
  "@glimmer/tracking": "^1.0.4",
24
24
  "ember-auto-import": "^2.2.4",
25
+ "ember-cached-decorator-polyfill": "^0.1.4",
25
26
  "ember-cli-babel": "^7.26.6",
26
27
  "ember-cli-path-utils": "^1.0.0",
27
28
  "ember-cli-typescript": "^4.1.0"
28
29
  },
29
30
  "devDependencies": {
30
- "@ember-data/unpublished-test-infra": "4.2.0-alpha.0",
31
+ "@ember-data/unpublished-test-infra": "4.2.0-alpha.1",
31
32
  "@ember/optional-features": "^2.0.0",
32
33
  "@ember/test-helpers": "^2.6.0",
33
34
  "@types/ember": "^3.16.5",
@@ -44,8 +45,8 @@
44
45
  "ember-load-initializers": "^2.1.1",
45
46
  "ember-maybe-import-regenerator": "^0.1.6",
46
47
  "ember-qunit": "^5.1.5",
47
- "ember-resolver": "^8.0.0",
48
- "ember-source": "~3.28.6",
48
+ "ember-resolver": "^8.0.3",
49
+ "ember-source": "~4.0.0",
49
50
  "ember-source-channel-url": "^3.0.0",
50
51
  "ember-try": "^1.4.0",
51
52
  "loader.js": "^4.7.0",