@ember-data/store 4.4.0 → 4.5.0-alpha.2

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.
Files changed (33) hide show
  1. package/addon/-private/system/core-store.ts +136 -147
  2. package/addon/-private/system/ds-model-store.ts +1 -10
  3. package/addon/-private/system/fetch-manager.ts +48 -21
  4. package/addon/-private/system/model/internal-model.ts +263 -192
  5. package/addon/-private/system/model/states.js +5 -41
  6. package/addon/-private/system/{promise-proxies.js → promise-proxies.ts} +31 -21
  7. package/addon/-private/system/promise-proxy-base.js +7 -0
  8. package/addon/-private/system/{record-array-manager.js → record-array-manager.ts} +87 -60
  9. package/addon/-private/system/record-arrays/adapter-populated-record-array.ts +129 -0
  10. package/addon/-private/system/record-arrays/{record-array.js → record-array.ts} +96 -75
  11. package/addon/-private/system/record-data-for.ts +2 -0
  12. package/addon/-private/system/references/belongs-to.ts +3 -2
  13. package/addon/-private/system/references/has-many.ts +4 -2
  14. package/addon/-private/system/schema-definition-service.ts +2 -2
  15. package/addon/-private/system/snapshot-record-array.ts +12 -11
  16. package/addon/-private/system/snapshot.ts +24 -7
  17. package/addon/-private/system/store/common.js +24 -1
  18. package/addon/-private/system/store/finders.js +53 -5
  19. package/addon/-private/system/store/internal-model-factory.ts +8 -7
  20. package/addon/-private/system/store/record-data-store-wrapper.ts +7 -2
  21. package/addon/-private/system/store/serializer-response.ts +85 -0
  22. package/addon/-private/ts-interfaces/ds-model.ts +15 -7
  23. package/addon/-private/ts-interfaces/ember-data-json-api.ts +3 -0
  24. package/addon/-private/ts-interfaces/minimum-adapter-interface.ts +19 -20
  25. package/addon/-private/ts-interfaces/minimum-serializer-interface.ts +27 -6
  26. package/addon/-private/ts-interfaces/record-data.ts +4 -1
  27. package/addon/-private/ts-interfaces/record-instance.ts +3 -1
  28. package/addon/-private/ts-interfaces/store.ts +1 -0
  29. package/addon/-private/utils/promise-record.ts +3 -3
  30. package/index.js +3 -0
  31. package/package.json +7 -6
  32. package/addon/-private/system/record-arrays/adapter-populated-record-array.js +0 -95
  33. package/addon/-private/system/store/serializer-response.js +0 -71
@@ -5,8 +5,19 @@ import { get } from '@ember/object';
5
5
  import { _backburner as emberBackburner, cancel, run } from '@ember/runloop';
6
6
  import { DEBUG } from '@glimmer/env';
7
7
 
8
- import RSVP, { Promise } from 'rsvp';
8
+ import { importSync } from '@embroider/macros';
9
+ import RSVP, { resolve } from 'rsvp';
9
10
 
11
+ import type { ManyArray } from '@ember-data/model/-private';
12
+ import RecordState from '@ember-data/model/-private/record-state';
13
+ import type { ManyArrayCreateArgs } from '@ember-data/model/-private/system/many-array';
14
+ import type {
15
+ BelongsToProxyCreateArgs,
16
+ BelongsToProxyMeta,
17
+ } from '@ember-data/model/-private/system/promise-belongs-to';
18
+ import type PromiseBelongsTo from '@ember-data/model/-private/system/promise-belongs-to';
19
+ import type { HasManyProxyCreateArgs } from '@ember-data/model/-private/system/promise-many-array';
20
+ import type PromiseManyArray from '@ember-data/model/-private/system/promise-many-array';
10
21
  import { HAS_MODEL_PACKAGE, HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra';
11
22
  import type {
12
23
  BelongsToRelationship,
@@ -14,16 +25,21 @@ import type {
14
25
  RecordData as DefaultRecordData,
15
26
  } from '@ember-data/record-data/-private';
16
27
  import type { UpgradedMeta } from '@ember-data/record-data/-private/graph/-edge-definition';
28
+ import type {
29
+ DefaultSingleResourceRelationship,
30
+ RelationshipRecordData,
31
+ } from '@ember-data/record-data/-private/ts-interfaces/relationship-record-data';
17
32
 
18
- import { DSModel } from '../../ts-interfaces/ds-model';
33
+ import type { DSModel } from '../../ts-interfaces/ds-model';
19
34
  import type { StableRecordIdentifier } from '../../ts-interfaces/identifier';
20
- import type { RecordData } from '../../ts-interfaces/record-data';
35
+ import type { ChangedAttributesHash, RecordData } from '../../ts-interfaces/record-data';
21
36
  import type { JsonApiResource, JsonApiValidationError } from '../../ts-interfaces/record-data-json-api';
37
+ import type { RelationshipSchema } from '../../ts-interfaces/record-data-schemas';
22
38
  import type { RecordInstance } from '../../ts-interfaces/record-instance';
23
39
  import type { FindOptions } from '../../ts-interfaces/store';
24
- import type { ConfidentDict } from '../../ts-interfaces/utils';
40
+ import type { Dict } from '../../ts-interfaces/utils';
25
41
  import type CoreStore from '../core-store';
26
- import type Store from '../ds-model-store';
42
+ import type { CreateRecordProperties } from '../core-store';
27
43
  import { errorsHashToArray } from '../errors-utils';
28
44
  import recordDataFor from '../record-data-for';
29
45
  import { BelongsToReference, HasManyReference, RecordReference } from '../references';
@@ -31,10 +47,11 @@ import Snapshot from '../snapshot';
31
47
  import { internalModelFactoryFor } from '../store/internal-model-factory';
32
48
  import RootState from './states';
33
49
 
34
- // move to TS hacks module that we can delete when this is no longer a necessary recast
35
- type ManyArray = InstanceType<typeof import('@ember-data/model/-private').ManyArray>;
36
- type PromiseBelongsTo = InstanceType<typeof import('@ember-data/model/-private').PromiseBelongsTo>;
37
- type PromiseManyArray = InstanceType<typeof import('@ember-data/model/-private').PromiseManyArray>;
50
+ type PrivateModelModule = {
51
+ ManyArray: { create(args: ManyArrayCreateArgs): ManyArray };
52
+ PromiseBelongsTo: { create(args: BelongsToProxyCreateArgs): PromiseBelongsTo };
53
+ PromiseManyArray: new (...args: unknown[]) => PromiseManyArray;
54
+ };
38
55
 
39
56
  /**
40
57
  @module @ember-data/store
@@ -42,18 +59,22 @@ type PromiseManyArray = InstanceType<typeof import('@ember-data/model/-private')
42
59
 
43
60
  const { hasOwnProperty } = Object.prototype;
44
61
 
45
- let ManyArray: ManyArray;
46
- let PromiseBelongsTo: PromiseBelongsTo;
47
- let _PromiseManyArray: any; // TODO find a way to get the klass type here
62
+ let _ManyArray: PrivateModelModule['ManyArray'];
63
+ let _PromiseBelongsTo: PrivateModelModule['PromiseBelongsTo'];
64
+ let _PromiseManyArray: PrivateModelModule['PromiseManyArray'];
48
65
 
49
66
  let _found = false;
50
67
  let _getModelPackage: () => boolean;
51
68
  if (HAS_MODEL_PACKAGE) {
52
69
  _getModelPackage = function () {
53
70
  if (!_found) {
54
- let modelPackage = require('@ember-data/model/-private');
55
- ({ ManyArray, PromiseBelongsTo, PromiseManyArray: _PromiseManyArray } = modelPackage);
56
- if (ManyArray && PromiseBelongsTo && _PromiseManyArray) {
71
+ let modelPackage = importSync('@ember-data/model/-private') as PrivateModelModule;
72
+ ({
73
+ ManyArray: _ManyArray,
74
+ PromiseBelongsTo: _PromiseBelongsTo,
75
+ PromiseManyArray: _PromiseManyArray,
76
+ } = modelPackage);
77
+ if (_ManyArray && _PromiseBelongsTo && _PromiseManyArray) {
57
78
  _found = true;
58
79
  }
59
80
  }
@@ -61,13 +82,6 @@ if (HAS_MODEL_PACKAGE) {
61
82
  };
62
83
  }
63
84
 
64
- interface BelongsToMetaWrapper {
65
- key: string;
66
- store: CoreStore;
67
- originatingInternalModel: InternalModel;
68
- modelName: string;
69
- }
70
-
71
85
  /*
72
86
  The TransitionChainMap caches the `state.enters`, `state.setups`, and final state reached
73
87
  when transitioning from one state to another, so that future transitions can replay the
@@ -78,18 +92,32 @@ interface BelongsToMetaWrapper {
78
92
  and setups. It may also be faster to do a two level cache (from: { to }) instead of caching based
79
93
  on a key that adds the two together.
80
94
  */
95
+ // TODO before deleting the state machine we should
96
+ // ensure all things in this map were properly accounted for.
97
+ // in the RecordState class.
81
98
  const TransitionChainMap = Object.create(null);
82
99
 
83
100
  const _extractPivotNameCache = Object.create(null);
84
101
  const _splitOnDotCache = Object.create(null);
85
102
 
86
- function splitOnDot(name) {
103
+ function splitOnDot(name: string): string[] {
87
104
  return _splitOnDotCache[name] || (_splitOnDotCache[name] = name.split('.'));
88
105
  }
89
106
 
90
- function extractPivotName(name) {
107
+ function extractPivotName(name: string): string {
91
108
  return _extractPivotNameCache[name] || (_extractPivotNameCache[name] = splitOnDot(name)[0]);
92
109
  }
110
+
111
+ function isDSModel(record: RecordInstance | null): record is DSModel {
112
+ return (
113
+ HAS_MODEL_PACKAGE &&
114
+ !!record &&
115
+ 'constructor' in record &&
116
+ 'isModel' in record.constructor &&
117
+ record.constructor.isModel === true
118
+ );
119
+ }
120
+
93
121
  export default class InternalModel {
94
122
  declare _id: string | null;
95
123
  declare modelName: string;
@@ -106,25 +134,28 @@ export default class InternalModel {
106
134
 
107
135
  // Not typed yet
108
136
  declare _promiseProxy: any;
109
- declare _record: any;
137
+ declare _record: RecordInstance | null;
110
138
  declare _scheduledDestroy: any;
111
139
  declare _modelClass: any;
112
- declare _deferredTriggers: any;
113
140
  declare __recordArrays: any;
114
141
  declare references: any;
115
142
  declare _recordReference: RecordReference;
116
- declare _manyArrayCache: ConfidentDict<ManyArray>;
143
+ declare _manyArrayCache: Dict<ManyArray>;
117
144
 
118
- declare _relationshipPromisesCache: ConfidentDict<RSVP.Promise<any>>;
119
- declare _relationshipProxyCache: ConfidentDict<PromiseManyArray | PromiseBelongsTo>;
145
+ declare _relationshipPromisesCache: Dict<Promise<ManyArray | RecordInstance>>;
146
+ declare _relationshipProxyCache: Dict<PromiseManyArray | PromiseBelongsTo>;
120
147
  declare error: any;
121
- declare currentState: any;
148
+ declare currentState: RecordState;
122
149
  declare _previousState: any;
150
+ declare store: CoreStore;
151
+ declare identifier: StableRecordIdentifier;
123
152
 
124
- constructor(public store: CoreStore | Store, public identifier: StableRecordIdentifier) {
153
+ constructor(store: CoreStore, identifier: StableRecordIdentifier) {
125
154
  if (HAS_MODEL_PACKAGE) {
126
155
  _getModelPackage();
127
156
  }
157
+ this.store = store;
158
+ this.identifier = identifier;
128
159
  this._id = identifier.id;
129
160
  this._isUpdatingId = false;
130
161
  this.modelName = identifier.type;
@@ -163,7 +194,6 @@ export default class InternalModel {
163
194
  this._relationshipPromisesCache = Object.create(null);
164
195
  this._relationshipProxyCache = Object.create(null);
165
196
  this.references = Object.create(null);
166
- this._deferredTriggers = [];
167
197
  this.currentState = RootState.empty;
168
198
  }
169
199
 
@@ -256,15 +286,27 @@ export default class InternalModel {
256
286
  }
257
287
  }
258
288
 
259
- getRecord(properties?): Object {
260
- if (!this._record && !this._isDematerializing) {
289
+ getRecord(properties?: CreateRecordProperties): RecordInstance {
290
+ let record = this._record;
291
+
292
+ if (this._isDematerializing) {
293
+ // TODO we should assert here instead of this return.
294
+ return null as unknown as RecordInstance;
295
+ }
296
+
297
+ if (!record) {
261
298
  let { store } = this;
262
299
 
263
- this._record = store._instantiateRecord(this, this.modelName, this._recordData, this.identifier, properties);
264
- this._triggerDeferredTriggers();
300
+ record = this._record = store._instantiateRecord(
301
+ this,
302
+ this.modelName,
303
+ this._recordData,
304
+ this.identifier,
305
+ properties
306
+ );
265
307
  }
266
308
 
267
- return this._record;
309
+ return record;
268
310
  }
269
311
 
270
312
  dematerializeRecord() {
@@ -288,9 +330,11 @@ export default class InternalModel {
288
330
  });
289
331
 
290
332
  if (this._record) {
291
- Object.keys(this._relationshipProxyCache).forEach((key) => {
292
- if (this._relationshipProxyCache[key].destroy) {
293
- this._relationshipProxyCache[key].destroy();
333
+ let keys = Object.keys(this._relationshipProxyCache);
334
+ keys.forEach((key) => {
335
+ let proxy = this._relationshipProxyCache[key]!;
336
+ if (proxy.destroy) {
337
+ proxy.destroy();
294
338
  }
295
339
  delete this._relationshipProxyCache[key];
296
340
  });
@@ -315,7 +359,6 @@ export default class InternalModel {
315
359
  // destroyRecord follows up deleteRecord with save(). This prevents an unecessary save for a new record
316
360
  this._deletedRecordWasNew = true;
317
361
  this.send('deleteRecord');
318
- this._triggerDeferredTriggers();
319
362
  this.unloadRecord();
320
363
  } else {
321
364
  this.send('deleteRecord');
@@ -324,9 +367,9 @@ export default class InternalModel {
324
367
  });
325
368
  }
326
369
 
327
- save(options): Promise<void> {
370
+ save(options: FindOptions = {}): Promise<void> {
328
371
  if (this._deletedRecordWasNew) {
329
- return Promise.resolve();
372
+ return resolve();
330
373
  }
331
374
  let promiseLabel = 'DS: Model#save ' + this;
332
375
  let resolver = RSVP.defer<void>(promiseLabel);
@@ -335,22 +378,8 @@ export default class InternalModel {
335
378
  return this.store.scheduleSave(this, resolver, options) as Promise<void>;
336
379
  }
337
380
 
338
- reload(options) {
339
- if (!options) {
340
- options = {};
341
- }
342
- let internalModel = this;
343
-
344
- return internalModel.store._reloadRecord(internalModel, options).then(
345
- function () {
346
- //TODO NOW seems like we shouldn't need to do this
347
- return internalModel;
348
- },
349
- function (error) {
350
- throw error;
351
- },
352
- 'DS: Model#reload complete, update flags'
353
- );
381
+ reload(options: Dict<unknown> = {}): Promise<InternalModel> {
382
+ return this.store._reloadRecord(this, options);
354
383
  }
355
384
 
356
385
  /*
@@ -425,26 +454,32 @@ export default class InternalModel {
425
454
  }
426
455
  }
427
456
 
428
- _findBelongsTo(key, resource, relationshipMeta, options) {
457
+ _findBelongsTo(
458
+ key: string,
459
+ resource: DefaultSingleResourceRelationship,
460
+ relationshipMeta: RelationshipSchema,
461
+ options?: Dict<unknown>
462
+ ): Promise<RecordInstance | null> {
429
463
  // TODO @runspired follow up if parent isNew then we should not be attempting load here
464
+ // TODO @runspired follow up on whether this should be in the relationship requests cache
430
465
  return this.store._findBelongsToByJsonApiResource(resource, this, relationshipMeta, options).then(
431
- (internalModel) => handleCompletedRelationshipRequest(this, key, resource._relationship, internalModel, null),
466
+ (internalModel) => handleCompletedRelationshipRequest(this, key, resource._relationship, internalModel),
432
467
  (e) => handleCompletedRelationshipRequest(this, key, resource._relationship, null, e)
433
468
  );
434
469
  }
435
470
 
436
- getBelongsTo(key, options) {
471
+ getBelongsTo(key: string, options?: Dict<unknown>): PromiseBelongsTo | RecordInstance | null {
437
472
  let resource = (this._recordData as DefaultRecordData).getBelongsTo(key);
438
473
  let identifier =
439
474
  resource && resource.data ? this.store.identifierCache.getOrCreateRecordIdentifier(resource.data) : null;
440
475
  let relationshipMeta = this.store._relationshipMetaFor(this.modelName, null, key);
441
- if (!relationshipMeta) return;
476
+ assert(`Attempted to access a belongsTo relationship but no definition exists for it`, relationshipMeta);
442
477
 
443
478
  let store = this.store;
444
479
  let parentInternalModel = this;
445
480
  let async = relationshipMeta.options.async;
446
481
  let isAsync = typeof async === 'undefined' ? true : async;
447
- let _belongsToState: BelongsToMetaWrapper = {
482
+ let _belongsToState: BelongsToProxyMeta = {
448
483
  key,
449
484
  store,
450
485
  originatingInternalModel: this,
@@ -455,7 +490,7 @@ export default class InternalModel {
455
490
  let internalModel = identifier !== null ? store._internalModelForResource(identifier) : null;
456
491
 
457
492
  if (resource._relationship.state.hasFailedLoadAttempt) {
458
- return this._relationshipProxyCache[key];
493
+ return this._relationshipProxyCache[key] as PromiseBelongsTo;
459
494
  }
460
495
 
461
496
  let promise = this._findBelongsTo(key, resource, relationshipMeta, options);
@@ -479,49 +514,49 @@ export default class InternalModel {
479
514
  "' with id " +
480
515
  parentInternalModel.id +
481
516
  ' 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 })`)',
482
- toReturn === null || !(toReturn as DSModel).isEmpty
517
+ toReturn === null || !internalModel.currentState.isEmpty
483
518
  );
484
519
  return toReturn;
485
520
  }
486
521
  }
487
522
  }
488
523
 
489
- getManyArray(key: string, definition?: UpgradedMeta) {
490
- if (HAS_RECORD_DATA_PACKAGE) {
491
- let manyArray = this._manyArrayCache[key];
492
- if (!definition) {
493
- const graphFor = require('@ember-data/record-data/-private').graphFor;
494
- definition = graphFor(this.store).get(this.identifier, key).definition as UpgradedMeta;
495
- }
496
-
497
- if (!manyArray) {
498
- manyArray = ManyArray.create({
499
- store: this.store,
500
- type: this.store.modelFor(definition.type),
501
- recordData: this._recordData,
502
- key,
503
- isPolymorphic: definition.isPolymorphic,
504
- isAsync: definition.isAsync,
505
- _inverseIsAsync: definition.inverseIsAsync,
506
- internalModel: this,
507
- isLoaded: !definition.isAsync,
508
- });
509
- this._manyArrayCache[key] = manyArray;
510
- }
511
-
512
- return manyArray;
524
+ getManyArray(key: string, definition?: UpgradedMeta): ManyArray {
525
+ assert('hasMany only works with the @ember-data/record-data package', HAS_RECORD_DATA_PACKAGE);
526
+ let manyArray: ManyArray | undefined = this._manyArrayCache[key];
527
+ if (!definition) {
528
+ const graphFor = (
529
+ importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
530
+ ).graphFor;
531
+ definition = graphFor(this.store).get(this.identifier, key).definition as UpgradedMeta;
532
+ }
533
+
534
+ if (!manyArray) {
535
+ manyArray = _ManyArray.create({
536
+ store: this.store,
537
+ type: this.store.modelFor(definition.type),
538
+ recordData: this._recordData as RelationshipRecordData,
539
+ key,
540
+ isPolymorphic: definition.isPolymorphic,
541
+ isAsync: definition.isAsync,
542
+ _inverseIsAsync: definition.inverseIsAsync,
543
+ internalModel: this,
544
+ isLoaded: !definition.isAsync,
545
+ });
546
+ this._manyArrayCache[key] = manyArray;
513
547
  }
514
- assert(`hasMany only works with the @ember-data/record-data package`, HAS_RECORD_DATA_PACKAGE);
548
+
549
+ return manyArray;
515
550
  }
516
551
 
517
552
  fetchAsyncHasMany(
518
553
  key: string,
519
- relationship: ManyRelationship | BelongsToRelationship,
520
- manyArray,
521
- options
522
- ): RSVP.Promise<unknown> {
554
+ relationship: ManyRelationship,
555
+ manyArray: ManyArray,
556
+ options?: Dict<unknown>
557
+ ): Promise<ManyArray> {
523
558
  if (HAS_RECORD_DATA_PACKAGE) {
524
- let loadingPromise = this._relationshipPromisesCache[key];
559
+ let loadingPromise = this._relationshipPromisesCache[key] as Promise<ManyArray> | undefined;
525
560
  if (loadingPromise) {
526
561
  return loadingPromise;
527
562
  }
@@ -529,25 +564,27 @@ export default class InternalModel {
529
564
  const jsonApi = this._recordData.getHasMany(key);
530
565
 
531
566
  loadingPromise = this.store._findHasManyByJsonApiResource(jsonApi, this, relationship, options).then(
532
- () => handleCompletedRelationshipRequest(this, key, relationship, manyArray, null),
567
+ () => handleCompletedRelationshipRequest(this, key, relationship, manyArray),
533
568
  (e) => handleCompletedRelationshipRequest(this, key, relationship, manyArray, e)
534
569
  );
535
570
  this._relationshipPromisesCache[key] = loadingPromise;
536
571
  return loadingPromise;
537
572
  }
538
- assert(`hasMany only works with the @ember-data/record-data package`);
573
+ assert('hasMany only works with the @ember-data/record-data package');
539
574
  }
540
575
 
541
- getHasMany(key: string, options?) {
576
+ getHasMany(key: string, options?): PromiseManyArray | ManyArray {
542
577
  if (HAS_RECORD_DATA_PACKAGE) {
543
- const graphFor = require('@ember-data/record-data/-private').graphFor;
544
- const relationship = graphFor(this.store).get(this.identifier, key);
578
+ const graphFor = (
579
+ importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
580
+ ).graphFor;
581
+ const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship;
545
582
  const { definition, state } = relationship;
546
583
  let manyArray = this.getManyArray(key, definition);
547
584
 
548
585
  if (definition.isAsync) {
549
586
  if (state.hasFailedLoadAttempt) {
550
- return this._relationshipProxyCache[key];
587
+ return this._relationshipProxyCache[key] as PromiseManyArray;
551
588
  }
552
589
 
553
590
  let promise = this.fetchAsyncHasMany(key, relationship, manyArray, options);
@@ -565,47 +602,55 @@ export default class InternalModel {
565
602
  assert(`hasMany only works with the @ember-data/record-data package`);
566
603
  }
567
604
 
605
+ _updatePromiseProxyFor(kind: 'hasMany', key: string, args: HasManyProxyCreateArgs): PromiseManyArray;
606
+ _updatePromiseProxyFor(kind: 'belongsTo', key: string, args: BelongsToProxyCreateArgs): PromiseBelongsTo;
607
+ _updatePromiseProxyFor(
608
+ kind: 'belongsTo',
609
+ key: string,
610
+ args: { promise: Promise<RecordInstance | null> }
611
+ ): PromiseBelongsTo;
568
612
  _updatePromiseProxyFor(
569
613
  kind: 'hasMany' | 'belongsTo',
570
614
  key: string,
571
- args: {
572
- promise: RSVP.Promise<any>;
573
- content?: RecordInstance | ManyArray | null;
574
- _belongsToState?: BelongsToMetaWrapper;
575
- }
576
- ) {
615
+ args: BelongsToProxyCreateArgs | HasManyProxyCreateArgs | { promise: Promise<RecordInstance | null> }
616
+ ): PromiseBelongsTo | PromiseManyArray {
577
617
  let promiseProxy = this._relationshipProxyCache[key];
578
618
  if (kind === 'hasMany') {
619
+ const { promise, content } = args as HasManyProxyCreateArgs;
579
620
  if (promiseProxy) {
580
- promiseProxy._update(args.promise, args.content);
621
+ assert(`Expected a PromiseManyArray`, '_update' in promiseProxy);
622
+ promiseProxy._update(promise, content);
581
623
  } else {
582
- promiseProxy = this._relationshipProxyCache[key] = new _PromiseManyArray(args.promise, args.content);
624
+ promiseProxy = this._relationshipProxyCache[key] = new _PromiseManyArray(promise, content);
583
625
  }
584
626
  return promiseProxy;
585
627
  }
586
628
  if (promiseProxy) {
587
- if (args.content !== undefined) {
588
- // this usage of `any` can be removed when `@types/ember_object` proxy allows `null` for content
589
- promiseProxy.set('content', args.content as any);
629
+ const { promise, content } = args as BelongsToProxyCreateArgs;
630
+ assert(`Expected a PromiseBelongsTo`, '_belongsToState' in promiseProxy);
631
+
632
+ if (content !== undefined) {
633
+ promiseProxy.set('content', content);
590
634
  }
591
- promiseProxy.set('promise', args.promise);
635
+ promiseProxy.set('promise', promise);
592
636
  } else {
593
- const klass = PromiseBelongsTo;
594
637
  // this usage of `any` can be removed when `@types/ember_object` proxy allows `null` for content
595
- this._relationshipProxyCache[key] = klass.create(args as any);
638
+ this._relationshipProxyCache[key] = promiseProxy = _PromiseBelongsTo.create(args as any);
596
639
  }
597
640
 
598
- return this._relationshipProxyCache[key];
641
+ return promiseProxy;
599
642
  }
600
643
 
601
- reloadHasMany(key, options) {
644
+ reloadHasMany(key: string, options) {
602
645
  if (HAS_RECORD_DATA_PACKAGE) {
603
646
  let loadingPromise = this._relationshipPromisesCache[key];
604
647
  if (loadingPromise) {
605
648
  return loadingPromise;
606
649
  }
607
- const graphFor = require('@ember-data/record-data/-private').graphFor;
608
- const relationship = graphFor(this.store).get(this.identifier, key);
650
+ const graphFor = (
651
+ importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
652
+ ).graphFor;
653
+ const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship;
609
654
  const { definition, state } = relationship;
610
655
 
611
656
  state.hasFailedLoadAttempt = false;
@@ -622,8 +667,8 @@ export default class InternalModel {
622
667
  assert(`hasMany only works with the @ember-data/record-data package`);
623
668
  }
624
669
 
625
- reloadBelongsTo(key, options) {
626
- let loadingPromise = this._relationshipPromisesCache[key];
670
+ reloadBelongsTo(key: string, options?: Dict<unknown>): Promise<RecordInstance | null> {
671
+ let loadingPromise = this._relationshipPromisesCache[key] as Promise<RecordInstance | null> | undefined;
627
672
  if (loadingPromise) {
628
673
  return loadingPromise;
629
674
  }
@@ -635,6 +680,7 @@ export default class InternalModel {
635
680
  resource._relationship.state.shouldForceReload = true;
636
681
  }
637
682
  let relationshipMeta = this.store._relationshipMetaFor(this.modelName, null, key);
683
+ assert(`Attempted to reload a belongsTo relationship but no definition exists for it`, relationshipMeta);
638
684
  let promise = this._findBelongsTo(key, resource, relationshipMeta, options);
639
685
  if (this._relationshipProxyCache[key]) {
640
686
  return this._updatePromiseProxyFor('belongsTo', key, { promise });
@@ -653,7 +699,7 @@ export default class InternalModel {
653
699
  destroy() {
654
700
  assert(
655
701
  'Cannot destroy an internalModel while its record is materialized',
656
- !this._record || this._record.get('isDestroyed') || this._record.get('isDestroying')
702
+ !this._record || this._record.isDestroyed || this._record.isDestroying
657
703
  );
658
704
  this.isDestroying = true;
659
705
  if (this._recordReference) {
@@ -662,13 +708,13 @@ export default class InternalModel {
662
708
  this._recordReference = null;
663
709
  let cache = this._manyArrayCache;
664
710
  Object.keys(cache).forEach((key) => {
665
- cache[key].destroy();
711
+ cache[key]!.destroy();
666
712
  delete cache[key];
667
713
  });
668
714
  if (this.references) {
669
715
  cache = this.references;
670
716
  Object.keys(cache).forEach((key) => {
671
- cache[key].destroy();
717
+ cache[key]!.destroy();
672
718
  delete cache[key];
673
719
  });
674
720
  }
@@ -678,24 +724,35 @@ export default class InternalModel {
678
724
  }
679
725
 
680
726
  setupData(data) {
681
- let changedKeys = this._recordData.pushData(data, this.hasRecord);
682
- if (this.hasRecord) {
683
- // TODO @runspired should this be going through the notification manager?
684
- this._record._notifyProperties(changedKeys);
727
+ const hasRecord = this.hasRecord;
728
+ if (hasRecord) {
729
+ let changedKeys = this._recordData.pushData(data, true);
730
+ this.notifyAttributes(changedKeys);
731
+ } else {
732
+ this._recordData.pushData(data);
685
733
  }
686
734
  this.send('pushedData');
687
735
  }
688
736
 
689
- setDirtyHasMany(key, records) {
737
+ notifyAttributes(keys: string[]): void {
738
+ let manager = this.store._notificationManager;
739
+ let { identifier } = this;
740
+
741
+ for (let i = 0; i < keys.length; i++) {
742
+ manager.notify(identifier, 'attributes', keys[i]);
743
+ }
744
+ }
745
+
746
+ setDirtyHasMany(key: string, records) {
690
747
  assertRecordsPassedToHasMany(records);
691
748
  return this._recordData.setDirtyHasMany(key, extractRecordDatasFromRecords(records));
692
749
  }
693
750
 
694
- setDirtyBelongsTo(key, value) {
751
+ setDirtyBelongsTo(key: string, value) {
695
752
  return this._recordData.setDirtyBelongsTo(key, extractRecordDataFromRecord(value));
696
753
  }
697
754
 
698
- setDirtyAttribute(key, value) {
755
+ setDirtyAttribute<T>(key: string, value: T): T {
699
756
  if (this.isDeleted()) {
700
757
  if (DEBUG) {
701
758
  throw new EmberError(`Attempted to set '${key}' to '${value}' on the deleted record ${this}`);
@@ -717,11 +774,11 @@ export default class InternalModel {
717
774
  return value;
718
775
  }
719
776
 
720
- get isDestroyed() {
777
+ get isDestroyed(): boolean {
721
778
  return this._isDestroyed;
722
779
  }
723
780
 
724
- get hasRecord() {
781
+ get hasRecord(): boolean {
725
782
  return !!this._record;
726
783
  }
727
784
 
@@ -729,7 +786,7 @@ export default class InternalModel {
729
786
  return new Snapshot(options, this.identifier, this.store);
730
787
  }
731
788
 
732
- hasChangedAttributes() {
789
+ hasChangedAttributes(): boolean {
733
790
  if (!this.__recordData) {
734
791
  // no need to calculate changed attributes when calling `findRecord`
735
792
  return false;
@@ -737,7 +794,7 @@ export default class InternalModel {
737
794
  return this._recordData.hasChangedAttributes();
738
795
  }
739
796
 
740
- changedAttributes() {
797
+ changedAttributes(): ChangedAttributesHash {
741
798
  if (!this.__recordData) {
742
799
  // no need to calculate changed attributes when calling `findRecord`
743
800
  return {};
@@ -745,16 +802,16 @@ export default class InternalModel {
745
802
  return this._recordData.changedAttributes();
746
803
  }
747
804
 
748
- adapterWillCommit() {
805
+ adapterWillCommit(): void {
749
806
  this._recordData.willCommit();
750
807
  this.send('willCommit');
751
808
  }
752
809
 
753
- adapterDidDirty() {
810
+ adapterDidDirty(): void {
754
811
  this.send('becomeDirty');
755
812
  }
756
813
 
757
- send(name, context?) {
814
+ send(name: string, context?) {
758
815
  let currentState = this.currentState;
759
816
 
760
817
  if (!currentState[name]) {
@@ -785,7 +842,7 @@ export default class InternalModel {
785
842
  }
786
843
  }
787
844
 
788
- notifyPropertyChange(key) {
845
+ notifyPropertyChange(key: string) {
789
846
  if (this.hasRecord) {
790
847
  // TODO this should likely *mostly* be the `attributes` bucket
791
848
  // but it seems for local mutations we rely on computed updating
@@ -795,7 +852,7 @@ export default class InternalModel {
795
852
  }
796
853
  }
797
854
 
798
- notifyStateChange(key?) {
855
+ notifyStateChange(key?: string) {
799
856
  if (this.hasRecord) {
800
857
  this.store._notificationManager.notify(this.identifier, 'state');
801
858
  }
@@ -811,24 +868,24 @@ export default class InternalModel {
811
868
  rollbackAttributes() {
812
869
  this.store._backburner.join(() => {
813
870
  let dirtyKeys = this._recordData.rollbackAttributes();
814
- if (get(this, 'isError')) {
871
+ if (this.isError) {
815
872
  this.didCleanError();
816
873
  }
817
874
 
818
875
  this.send('rolledBack');
819
876
 
820
- if (this._record && dirtyKeys && dirtyKeys.length > 0) {
821
- this._record._notifyProperties(dirtyKeys);
877
+ if (this.hasRecord && dirtyKeys && dirtyKeys.length > 0) {
878
+ this.notifyAttributes(dirtyKeys);
822
879
  }
823
880
  });
824
881
  }
825
882
 
826
- transitionTo(name) {
883
+ transitionTo(name: string) {
827
884
  // POSSIBLE TODO: Remove this code and replace with
828
885
  // always having direct reference to state objects
829
886
 
830
887
  let pivotName = extractPivotName(name);
831
- let state = this.currentState;
888
+ let state: any = this.currentState;
832
889
  let transitionMapId = `${state.stateName}->${name}`;
833
890
 
834
891
  do {
@@ -873,13 +930,12 @@ export default class InternalModel {
873
930
  }
874
931
 
875
932
  this.currentState = state;
876
- if (this.hasRecord && typeof this._record.notifyPropertyChange === 'function') {
877
- // TODO refactor Model to have all flags pull from the notification manager
878
- // and for currentState.stateName to be constructed from flag state.
879
- // Probably just port this work from ember-m3
880
- // After that we can eliminate this.
933
+
934
+ // isDSModel is the guard we want, but may be too restrictive if
935
+ // ember-m3 / ember-data-model-fragments were relying on this still.
936
+ if (this.hasRecord && isDSModel(this._record)) {
937
+ // TODO eliminate this.
881
938
  this.notifyStateChange('currentState');
882
- // this._record.notifyPropertyChange('currentState');
883
939
  }
884
940
 
885
941
  for (i = 0, l = setups.length; i < l; i++) {
@@ -887,7 +943,7 @@ export default class InternalModel {
887
943
  }
888
944
  }
889
945
 
890
- _unhandledEvent(state, name, context) {
946
+ _unhandledEvent(state, name: string, context) {
891
947
  let errorMessage = 'Attempted to handle event `' + name + '` ';
892
948
  errorMessage += 'on ' + String(this) + ' while in state ';
893
949
  errorMessage += state.stateName + '. ';
@@ -899,35 +955,6 @@ export default class InternalModel {
899
955
  throw new EmberError(errorMessage);
900
956
  }
901
957
 
902
- triggerLater(...args) {
903
- if (this._deferredTriggers.push(args) !== 1) {
904
- return;
905
- }
906
-
907
- this.store._updateInternalModel(this);
908
- }
909
-
910
- _triggerDeferredTriggers() {
911
- //TODO: Before 1.0 we want to remove all the events that happen on the pre materialized record,
912
- //but for now, we queue up all the events triggered before the record was materialized, and flush
913
- //them once we have the record
914
- if (!this.hasRecord) {
915
- return;
916
- }
917
- let triggers = this._deferredTriggers;
918
- let record = this._record;
919
- let trigger = record.trigger;
920
- // TODO Igor make nicer check
921
- if (trigger && typeof trigger === 'function') {
922
- for (let i = 0, l = triggers.length; i < l; i++) {
923
- let eventName = triggers[i];
924
- trigger.apply(record, eventName);
925
- }
926
- }
927
-
928
- triggers.length = 0;
929
- }
930
-
931
958
  removeFromInverseRelationships() {
932
959
  if (this.__recordData) {
933
960
  this.store._backburner.join(() => {
@@ -1057,26 +1084,34 @@ export default class InternalModel {
1057
1084
  this.store._notificationManager.notify(this.identifier, 'attributes');
1058
1085
  }
1059
1086
 
1060
- hasErrors() {
1087
+ hasErrors(): boolean {
1088
+ // TODO add assertion forcing consuming RecordData's to implement getErrors
1061
1089
  if (this._recordData.getErrors) {
1062
1090
  return this._recordData.getErrors(this.identifier).length > 0;
1063
1091
  } else {
1064
- let errors = (this.getRecord() as DSModel).errors;
1092
+ // we can't have errors if we never tried loading
1093
+ if (!this._record) {
1094
+ return false;
1095
+ }
1096
+ let errors = (this._record as DSModel).errors;
1065
1097
  return errors.length > 0;
1066
1098
  }
1067
1099
  }
1068
1100
 
1069
1101
  // FOR USE DURING COMMIT PROCESS
1070
- adapterDidInvalidate(parsedErrors, error) {
1102
+ adapterDidInvalidate(parsedErrors, error?) {
1071
1103
  // TODO @runspired this should be handled by RecordState
1072
1104
  // and errors should be dirtied but lazily fetch if at
1073
1105
  // all possible. We should only notify errors here.
1074
1106
  let attribute;
1075
1107
  if (error && parsedErrors) {
1108
+ // TODO add assertion forcing consuming RecordData's to implement getErrors
1076
1109
  if (!this._recordData.getErrors) {
1110
+ let record = this.getRecord() as DSModel;
1111
+ let errors = record.errors;
1077
1112
  for (attribute in parsedErrors) {
1078
1113
  if (hasOwnProperty.call(parsedErrors, attribute)) {
1079
- (this.getRecord() as DSModel).errors._add(attribute, parsedErrors[attribute]);
1114
+ errors._add(attribute, parsedErrors[attribute]);
1080
1115
  }
1081
1116
  }
1082
1117
  }
@@ -1117,7 +1152,9 @@ export default class InternalModel {
1117
1152
  // because of the intimate API access involved. This is something we will need to redesign.
1118
1153
  assert(`snapshot.belongsTo only supported for @ember-data/record-data`);
1119
1154
  }
1120
- const graphFor = require('@ember-data/record-data/-private').graphFor;
1155
+ const graphFor = (
1156
+ importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
1157
+ ).graphFor;
1121
1158
  const relationship = graphFor(this.store._storeWrapper).get(this.identifier, name);
1122
1159
 
1123
1160
  if (DEBUG && kind) {
@@ -1145,7 +1182,39 @@ export default class InternalModel {
1145
1182
  }
1146
1183
  }
1147
1184
 
1148
- function handleCompletedRelationshipRequest(internalModel, key, relationship, value, error) {
1185
+ function handleCompletedRelationshipRequest(
1186
+ internalModel: InternalModel,
1187
+ key: string,
1188
+ relationship: BelongsToRelationship,
1189
+ value: InternalModel | null
1190
+ ): RecordInstance | null;
1191
+ function handleCompletedRelationshipRequest(
1192
+ internalModel: InternalModel,
1193
+ key: string,
1194
+ relationship: ManyRelationship,
1195
+ value: ManyArray
1196
+ ): ManyArray;
1197
+ function handleCompletedRelationshipRequest(
1198
+ internalModel: InternalModel,
1199
+ key: string,
1200
+ relationship: BelongsToRelationship,
1201
+ value: null,
1202
+ error: Error
1203
+ ): never;
1204
+ function handleCompletedRelationshipRequest(
1205
+ internalModel: InternalModel,
1206
+ key: string,
1207
+ relationship: ManyRelationship,
1208
+ value: ManyArray,
1209
+ error: Error
1210
+ ): never;
1211
+ function handleCompletedRelationshipRequest(
1212
+ internalModel: InternalModel,
1213
+ key: string,
1214
+ relationship: BelongsToRelationship | ManyRelationship,
1215
+ value: ManyArray | InternalModel | null,
1216
+ error?: Error
1217
+ ): ManyArray | RecordInstance | null {
1149
1218
  delete internalModel._relationshipPromisesCache[key];
1150
1219
  relationship.state.shouldForceReload = false;
1151
1220
  const isHasMany = relationship.definition.kind === 'hasMany';
@@ -1153,7 +1222,7 @@ function handleCompletedRelationshipRequest(internalModel, key, relationship, va
1153
1222
  if (isHasMany) {
1154
1223
  // we don't notify the record property here to avoid refetch
1155
1224
  // only the many array
1156
- value.notify();
1225
+ (value as ManyArray).notify();
1157
1226
  }
1158
1227
 
1159
1228
  if (error) {
@@ -1168,7 +1237,9 @@ function handleCompletedRelationshipRequest(internalModel, key, relationship, va
1168
1237
  // has never been accessed
1169
1238
  if (proxy && !isHasMany) {
1170
1239
  if (proxy.content && proxy.content.isDestroying) {
1171
- proxy.set('content', null);
1240
+ // TODO @types/ember__object incorrectly disallows `null`, we should either
1241
+ // override or fix upstream
1242
+ (proxy as PromiseBelongsTo).set('content', null as unknown as undefined);
1172
1243
  }
1173
1244
  }
1174
1245
 
@@ -1176,14 +1247,14 @@ function handleCompletedRelationshipRequest(internalModel, key, relationship, va
1176
1247
  }
1177
1248
 
1178
1249
  if (isHasMany) {
1179
- value.set('isLoaded', true);
1250
+ (value as ManyArray).set('isLoaded', true);
1180
1251
  }
1181
1252
 
1182
1253
  relationship.state.hasFailedLoadAttempt = false;
1183
1254
  // only set to not stale if no error is thrown
1184
1255
  relationship.state.isStale = false;
1185
1256
 
1186
- return value;
1257
+ return isHasMany || !value ? (value as ManyArray | null) : (value as InternalModel).getRecord();
1187
1258
  }
1188
1259
 
1189
1260
  export function assertRecordsPassedToHasMany(records) {