@ember-data/model 4.7.0-beta.1 → 4.7.0

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.
@@ -1,43 +1,38 @@
1
1
  /**
2
2
  @module @ember-data/store
3
3
  */
4
- import EmberArray from '@ember/array';
5
- import MutableArray from '@ember/array/mutable';
6
- import { assert } from '@ember/debug';
7
- import EmberObject, { get } from '@ember/object';
8
-
9
- import { all } from 'rsvp';
4
+ import { assert, deprecate } from '@ember/debug';
10
5
 
6
+ import { DEPRECATE_PROMISE_PROXIES } from '@ember-data/private-build-infra/deprecations';
11
7
  import type Store from '@ember-data/store';
12
- import { PromiseArray, recordDataFor } from '@ember-data/store/-private';
13
- import type { CreateRecordProperties } from '@ember-data/store/-private/core-store';
14
- import type ShimModelClass from '@ember-data/store/-private/model/shim-model-class';
15
- import type { DSModelSchema } from '@ember-data/types/q/ds-model';
8
+ import { IDENTIFIER_ARRAY_TAG, MUTATE, RecordArray, recordIdentifierFor, SOURCE } from '@ember-data/store/-private';
9
+ import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class';
10
+ import { IdentifierArrayCreateOptions } from '@ember-data/store/-private/record-arrays/identifier-array';
11
+ import type { CreateRecordProperties } from '@ember-data/store/-private/store-service';
16
12
  import type { Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api';
17
13
  import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
14
+ import type { RecordData } from '@ember-data/types/q/record-data';
18
15
  import type { RecordInstance } from '@ember-data/types/q/record-instance';
19
- import type { RelationshipRecordData } from '@ember-data/types/q/relationship-record-data';
20
16
  import type { FindOptions } from '@ember-data/types/q/store';
21
17
  import type { Dict } from '@ember-data/types/q/utils';
22
18
 
23
- import diffArray from './diff-array';
24
19
  import { LegacySupport } from './legacy-relationships-support';
25
20
 
26
- interface MutableArrayWithObject<T, M = T> extends EmberObject, MutableArray<M> {}
27
- const MutableArrayWithObject = EmberObject.extend(MutableArray) as unknown as new <
28
- T,
29
- M = T
30
- >() => MutableArrayWithObject<T, M>;
31
-
32
21
  export interface ManyArrayCreateArgs {
22
+ identifiers: StableRecordIdentifier[];
23
+ type: string;
33
24
  store: Store;
34
- type: ShimModelClass;
35
- recordData: RelationshipRecordData;
25
+ allowMutation: boolean;
26
+ manager: LegacySupport;
27
+
28
+ identifier: StableRecordIdentifier;
29
+ recordData: RecordData;
30
+ meta: Dict<unknown> | null;
31
+ links: Links | PaginationLinks | null;
36
32
  key: string;
37
33
  isPolymorphic: boolean;
38
34
  isAsync: boolean;
39
35
  _inverseIsAsync: boolean;
40
- legacySupport: LegacySupport;
41
36
  isLoaded: boolean;
42
37
  }
43
38
  /**
@@ -82,43 +77,26 @@ export interface ManyArrayCreateArgs {
82
77
 
83
78
  @class ManyArray
84
79
  @public
85
- @extends Ember.EmberObject
86
- @uses Ember.MutableArray
87
80
  */
88
- export default class ManyArray extends MutableArrayWithObject<StableRecordIdentifier, RecordInstance> {
81
+ export default class RelatedCollection extends RecordArray {
89
82
  declare isAsync: boolean;
90
- declare isLoaded: boolean;
91
- declare isPolymorphic: boolean;
92
- declare _isDirty: boolean;
93
- declare _isUpdating: boolean;
94
- declare _hasNotified: boolean;
95
- declare __hasArrayObservers: boolean;
96
- declare hasArrayObservers: boolean; // override the base declaration
97
- declare _length: number;
98
- declare _meta: Dict<unknown> | null;
99
- declare _links: Links | PaginationLinks | null;
100
- declare currentState: StableRecordIdentifier[];
101
- declare recordData: RelationshipRecordData;
102
- declare legacySupport: LegacySupport;
103
- declare store: Store;
104
- declare key: string;
105
- declare type: DSModelSchema;
106
-
107
- init() {
108
- super.init();
109
-
110
- /**
83
+ /**
111
84
  The loading state of this array
112
85
 
113
86
  @property {Boolean} isLoaded
114
87
  @public
115
88
  */
116
- this.isLoaded = this.isLoaded || false;
117
- this.isAsync = this.isAsync || false;
118
89
 
119
- this._length = 0;
90
+ declare isLoaded: boolean;
91
+ /**
92
+ `true` if the relationship is polymorphic, `false` otherwise.
120
93
 
121
- /**
94
+ @property {Boolean} isPolymorphic
95
+ @private
96
+ */
97
+ declare isPolymorphic: boolean;
98
+ declare _inverseIsAsync: boolean;
99
+ /**
122
100
  Metadata associated with the request for async hasMany relationships.
123
101
 
124
102
  Example
@@ -155,197 +133,146 @@ export default class ManyArray extends MutableArrayWithObject<StableRecordIdenti
155
133
  @property {Object | null} meta
156
134
  @public
157
135
  */
158
- this._meta = this._meta || null;
159
-
160
- /**
136
+ declare meta: Dict<unknown> | null;
137
+ /**
161
138
  * Retrieve the links for this relationship
162
139
  *
163
140
  @property {Object | null} links
164
141
  @public
165
142
  */
166
- this._links = this._links || null;
167
-
168
- /**
169
- `true` if the relationship is polymorphic, `false` otherwise.
170
-
171
- @property {Boolean} isPolymorphic
172
- @private
173
- */
174
- this.isPolymorphic = this.isPolymorphic || false;
175
-
176
- /**
177
- The relationship which manages this array.
178
-
179
- @property {ManyRelationship} relationship
180
- @private
181
- */
182
- this.currentState = [];
183
- this._isUpdating = false;
184
- this._isDirty = false;
185
- /*
186
- * Unfortunately ArrayProxy adds it's observers lazily,
187
- * so in a first-render situation we may sometimes notify
188
- * prior to the ArrayProxy having installed it's observers
189
- * (which occurs during _revalidate()).
190
- *
191
- * This leads to the flush occuring on access, the flush takes
192
- * the hasObservers codepath which in code out of our control
193
- * notifies again leading to a glimmer rendering invalidation error.
194
- *
195
- * We use this flag to detect the case where we notified without
196
- * array observers but observers were installed prior to flush.
197
- *
198
- * We do not need to fire array observers at all in this case
199
- * since it will be the first-access for those observers.
200
- */
201
- this._hasNotified = false;
202
- // make sure we initialize to the correct state
203
- // since the user has already accessed
204
- this.retrieveLatest();
205
- }
206
-
207
- // TODO refactor away _hasArrayObservers for tests
208
- get _hasArrayObservers() {
209
- // cast necessary because hasArrayObservers is typed as a ComputedProperty<boolean> vs a boolean;
210
- return this.hasArrayObservers || this.__hasArrayObservers;
211
- }
212
-
213
- notify() {
214
- this._isDirty = true;
215
- if (this._hasArrayObservers && !this._hasNotified) {
216
- this.retrieveLatest();
217
- } else {
218
- this._hasNotified = true;
219
- this.notifyPropertyChange('[]');
220
- this.notifyPropertyChange('firstObject');
221
- this.notifyPropertyChange('lastObject');
222
- }
223
- }
224
-
225
- get length() {
226
- if (this._isDirty) {
227
- this.retrieveLatest();
228
- }
229
- // By using `get()`, the tracking system knows to pay attention to changes that occur.
230
- get(this, '[]');
231
-
232
- return this._length;
233
- }
234
-
235
- set length(value) {
236
- this._length = value;
237
- }
238
-
239
- get links() {
240
- get(this, '[]');
241
- if (this._isDirty) {
242
- this.retrieveLatest();
243
- }
244
- return this._links;
245
- }
246
- set links(v) {
247
- this._links = v;
248
- }
249
-
250
- get meta() {
251
- get(this, '[]');
252
- if (this._isDirty) {
253
- this.retrieveLatest();
254
- }
255
- return this._meta;
256
- }
257
- set meta(v) {
258
- this._meta = v;
259
- }
260
-
261
- objectAt(index: number): RecordInstance | undefined {
262
- if (this._isDirty) {
263
- this.retrieveLatest();
264
- }
265
- let identifier = this.currentState[index];
266
- if (identifier === undefined) {
267
- return;
268
- }
269
-
270
- return this.store._instanceCache.getRecord(identifier);
143
+ declare links: Links | PaginationLinks | null;
144
+ declare identifier: StableRecordIdentifier;
145
+ declare recordData: RecordData;
146
+ // @ts-expect-error
147
+ declare _manager: LegacySupport;
148
+ declare store: Store;
149
+ declare key: string;
150
+ declare type: ShimModelClass;
151
+
152
+ constructor(options: ManyArrayCreateArgs) {
153
+ super(options as unknown as IdentifierArrayCreateOptions);
154
+ this.isLoaded = options.isLoaded || false;
155
+ this.isAsync = options.isAsync || false;
156
+ this.isPolymorphic = options.isPolymorphic || false;
157
+ this.identifier = options.identifier;
158
+ this.key = options.key;
271
159
  }
272
160
 
273
- replace(idx: number, amt: number, objects?: RecordInstance[]) {
274
- assert(`Cannot push mutations to the cache while updating the relationship from cache`, !this._isUpdating);
275
- const { store } = this;
276
- store._backburner.join(() => {
277
- let identifiers: StableRecordIdentifier[];
278
- if (amt > 0) {
279
- identifiers = this.currentState.slice(idx, idx + amt);
280
- this.recordData.removeFromHasMany(
281
- this.key,
282
- // TODO RecordData V2: recordData should take identifiers not RecordDatas
283
- identifiers.map((identifier) => store._instanceCache.getRecordData(identifier))
284
- );
161
+ [MUTATE](prop: string, args: unknown[], result?: unknown) {
162
+ switch (prop) {
163
+ case 'length 0': {
164
+ this._manager.updateCache({
165
+ op: 'replaceRelatedRecords',
166
+ record: this.identifier,
167
+ field: this.key,
168
+ value: [],
169
+ });
170
+ break;
285
171
  }
286
- if (objects) {
287
- assert(
288
- 'The third argument to replace needs to be an array.',
289
- Array.isArray(objects) || EmberArray.detect(objects)
290
- );
291
- this.recordData.addToHasMany(
292
- this.key,
293
- objects.map((obj: RecordInstance) => recordDataFor(obj)),
294
- idx
295
- );
172
+ case 'replace cell': {
173
+ const [index, prior, value] = args as [number, StableRecordIdentifier, StableRecordIdentifier];
174
+ this._manager.updateCache({
175
+ op: 'replaceRelatedRecord',
176
+ record: this.identifier,
177
+ field: this.key,
178
+ value,
179
+ prior,
180
+ index,
181
+ });
182
+ break;
296
183
  }
297
- this.notify();
298
- });
299
- }
300
-
301
- retrieveLatest() {
302
- // It’s possible the parent side of the relationship may have been destroyed by this point
303
- if (this.isDestroyed || this.isDestroying || this._isUpdating) {
304
- return;
305
- }
306
- this._isDirty = false;
307
- this._isUpdating = true;
308
- let jsonApi = this.recordData.getHasMany(this.key);
309
-
310
- let identifiers: StableRecordIdentifier[] = [];
311
- if (jsonApi.data) {
312
- for (let i = 0; i < jsonApi.data.length; i++) {
313
- // TODO figure out where this state comes from
314
- let im = this.store._instanceCache._internalModelForResource(jsonApi.data[i]);
315
- let shouldRemove = im._isDematerializing || im.isEmpty || !im.isLoaded;
316
-
317
- if (!shouldRemove) {
318
- identifiers.push(im.identifier);
184
+ case 'push':
185
+ this._manager.updateCache({
186
+ op: 'addToRelatedRecords',
187
+ record: this.identifier,
188
+ field: this.key,
189
+ value: extractIdentifiersFromRecords(args as RecordInstance[]),
190
+ });
191
+ break;
192
+ case 'pop':
193
+ if (result) {
194
+ this._manager.updateCache({
195
+ op: 'removeFromRelatedRecords',
196
+ record: this.identifier,
197
+ field: this.key,
198
+ value: recordIdentifierFor(result as RecordInstance),
199
+ });
200
+ }
201
+ break;
202
+
203
+ case 'unshift':
204
+ this._manager.updateCache({
205
+ op: 'addToRelatedRecords',
206
+ record: this.identifier,
207
+ field: this.key,
208
+ value: extractIdentifiersFromRecords(args as RecordInstance[]),
209
+ index: 0,
210
+ });
211
+ break;
212
+
213
+ case 'shift':
214
+ if (result) {
215
+ this._manager.updateCache({
216
+ op: 'removeFromRelatedRecords',
217
+ record: this.identifier,
218
+ field: this.key,
219
+ value: recordIdentifierFor(result as RecordInstance),
220
+ index: 0,
221
+ });
222
+ }
223
+ break;
224
+
225
+ case 'sort':
226
+ this._manager.updateCache({
227
+ op: 'sortRelatedRecords',
228
+ record: this.identifier,
229
+ field: this.key,
230
+ value: (result as RecordInstance[]).map(recordIdentifierFor),
231
+ });
232
+ break;
233
+
234
+ case 'splice': {
235
+ const [start, removeCount, ...adds] = args as [number, number, RecordInstance];
236
+ // detect a full replace
237
+ if (removeCount > 0 && adds.length === this[SOURCE].length) {
238
+ this._manager.updateCache({
239
+ op: 'replaceRelatedRecords',
240
+ record: this.identifier,
241
+ field: this.key,
242
+ value: extractIdentifiersFromRecords(adds),
243
+ });
244
+ return;
245
+ }
246
+ if (removeCount > 0) {
247
+ this._manager.updateCache({
248
+ op: 'removeFromRelatedRecords',
249
+ record: this.identifier,
250
+ field: this.key,
251
+ value: (result as RecordInstance[]).map(recordIdentifierFor),
252
+ index: start,
253
+ });
254
+ }
255
+ if (adds?.length) {
256
+ this._manager.updateCache({
257
+ op: 'addToRelatedRecords',
258
+ record: this.identifier,
259
+ field: this.key,
260
+ value: extractIdentifiersFromRecords(adds),
261
+ index: start,
262
+ });
319
263
  }
320
- }
321
- }
322
-
323
- if (jsonApi.meta) {
324
- this._meta = jsonApi.meta;
325
- }
326
-
327
- if (jsonApi.links) {
328
- this._links = jsonApi.links;
329
- }
330
264
 
331
- if (this._hasArrayObservers && !this._hasNotified) {
332
- // diff to find changes
333
- let diff = diffArray(this.currentState, identifiers);
334
- // it's null if no change found
335
- if (diff.firstChangeIndex !== null) {
336
- // we found a change
337
- this.arrayContentWillChange(diff.firstChangeIndex, diff.removedCount, diff.addedCount);
338
- this._length = identifiers.length;
339
- this.currentState = identifiers;
340
- this.arrayContentDidChange(diff.firstChangeIndex, diff.removedCount, diff.addedCount);
265
+ break;
341
266
  }
342
- } else {
343
- this._hasNotified = false;
344
- this._length = identifiers.length;
345
- this.currentState = identifiers;
267
+ default:
268
+ assert(`unable to convert ${prop} into a transaction that updates the cache state for this record array`);
346
269
  }
270
+ }
347
271
 
348
- this._isUpdating = false;
272
+ notify() {
273
+ const tag = this[IDENTIFIER_ARRAY_TAG];
274
+ tag.ref = null;
275
+ tag.shouldReset = true;
349
276
  }
350
277
 
351
278
  /**
@@ -372,7 +299,7 @@ export default class ManyArray extends MutableArrayWithObject<StableRecordIdenti
372
299
  */
373
300
  reload(options?: FindOptions) {
374
301
  // TODO this is odd, we don't ask the store for anything else like this?
375
- return this.legacySupport.reloadHasMany(this.key, options);
302
+ return this._manager.reloadHasMany(this.key, options);
376
303
  }
377
304
 
378
305
  /**
@@ -393,18 +320,6 @@ export default class ManyArray extends MutableArrayWithObject<StableRecordIdenti
393
320
  @public
394
321
  @return {PromiseArray} promise
395
322
  */
396
- save() {
397
- let manyArray = this;
398
- let promiseLabel = 'DS: ManyArray#save ' + this.type.modelName;
399
- let promise = all(this.invoke('save'), promiseLabel).then(
400
- () => manyArray,
401
- null,
402
- 'DS: ManyArray#save return ManyArray'
403
- );
404
-
405
- // TODO deprecate returning a promiseArray here
406
- return PromiseArray.create({ promise });
407
- }
408
323
 
409
324
  /**
410
325
  Create a child record within the owner
@@ -415,11 +330,70 @@ export default class ManyArray extends MutableArrayWithObject<StableRecordIdenti
415
330
  @return {Model} record
416
331
  */
417
332
  createRecord(hash: CreateRecordProperties): RecordInstance {
418
- const { store, type } = this;
333
+ const { store } = this;
419
334
 
420
- const record = store.createRecord(type.modelName, hash);
421
- this.pushObject(record);
335
+ const record = store.createRecord(this.modelName, hash);
336
+ this.push(record);
422
337
 
423
338
  return record;
424
339
  }
425
340
  }
341
+ RelatedCollection.prototype.isAsync = false;
342
+ RelatedCollection.prototype.isPolymorphic = false;
343
+ RelatedCollection.prototype.identifier = null as unknown as StableRecordIdentifier;
344
+ RelatedCollection.prototype.recordData = null as unknown as RecordData;
345
+ RelatedCollection.prototype._inverseIsAsync = false;
346
+ RelatedCollection.prototype.key = '';
347
+ RelatedCollection.prototype.DEPRECATED_CLASS_NAME = 'ManyArray';
348
+
349
+ type PromiseProxyRecord = { then(): void; content: RecordInstance | null | undefined };
350
+
351
+ function assertRecordPassedToHasMany(record: RecordInstance | PromiseProxyRecord) {
352
+ assert(
353
+ `All elements of a hasMany relationship must be instances of Model, you passed $${typeof record}`,
354
+ (function () {
355
+ try {
356
+ recordIdentifierFor(record);
357
+ return true;
358
+ } catch {
359
+ return false;
360
+ }
361
+ })()
362
+ );
363
+ }
364
+
365
+ function extractIdentifiersFromRecords(records: RecordInstance[]): StableRecordIdentifier[] {
366
+ return records.map(extractIdentifierFromRecord);
367
+ }
368
+
369
+ function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | RecordInstance) {
370
+ if (DEPRECATE_PROMISE_PROXIES && isPromiseRecord(recordOrPromiseRecord)) {
371
+ let content = recordOrPromiseRecord.content;
372
+ assert(
373
+ 'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo relationship.',
374
+ content !== undefined && content !== null
375
+ );
376
+ deprecate(
377
+ `You passed in a PromiseProxy to a Relationship API that now expects a resolved value. await the value before setting it.`,
378
+ false,
379
+ {
380
+ id: 'ember-data:deprecate-promise-proxies',
381
+ until: '5.0',
382
+ since: {
383
+ enabled: '4.8',
384
+ available: '4.8',
385
+ },
386
+ for: 'ember-data',
387
+ }
388
+ );
389
+ assertRecordPassedToHasMany(content);
390
+ return recordIdentifierFor(content);
391
+ }
392
+
393
+ assertRecordPassedToHasMany(recordOrPromiseRecord);
394
+ return recordIdentifierFor(recordOrPromiseRecord);
395
+ }
396
+
397
+ function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is PromiseProxyRecord {
398
+ return !!record.then;
399
+ }
@@ -29,10 +29,8 @@ export default function modelForMixin(store: Store, normalizedModelName: string)
29
29
  let mixin = MaybeMixin && MaybeMixin.class;
30
30
  if (mixin) {
31
31
  let ModelForMixin = Model.extend(mixin);
32
- ModelForMixin.reopenClass({
33
- __isMixin: true,
34
- __mixin: mixin,
35
- });
32
+ ModelForMixin.__isMixin = true;
33
+ ModelForMixin.__mixin = mixin;
36
34
  //Cache the class as a model
37
35
  owner.register('model:' + normalizedModelName, ModelForMixin);
38
36
  }