@ember-data/model 4.8.0-alpha.4 → 4.8.0-beta.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,45 +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, recordIdentifierFor } from '@ember-data/store/-private';
8
+ import { IDENTIFIER_ARRAY_TAG, MUTATE, RecordArray, recordIdentifierFor, SOURCE } from '@ember-data/store/-private';
13
9
  import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class';
14
- import type { NonSingletonRecordDataManager } from '@ember-data/store/-private/managers/record-data-manager';
10
+ import { IdentifierArrayCreateOptions } from '@ember-data/store/-private/record-arrays/identifier-array';
15
11
  import type { CreateRecordProperties } from '@ember-data/store/-private/store-service';
16
- import type { DSModelSchema } from '@ember-data/types/q/ds-model';
17
- import type { CollectionResourceRelationship, Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api';
12
+ import type { Links, PaginationLinks } from '@ember-data/types/q/ember-data-json-api';
18
13
  import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
19
14
  import type { RecordData } from '@ember-data/types/q/record-data';
20
15
  import type { RecordInstance } from '@ember-data/types/q/record-instance';
21
16
  import type { FindOptions } from '@ember-data/types/q/store';
22
17
  import type { Dict } from '@ember-data/types/q/utils';
23
18
 
24
- import diffArray from './diff-array';
25
19
  import { LegacySupport } from './legacy-relationships-support';
26
20
 
27
- interface MutableArrayWithObject<T, M = T> extends EmberObject, MutableArray<M> {}
28
- const MutableArrayWithObject = EmberObject.extend(MutableArray) as unknown as new <
29
- T,
30
- M = T
31
- >() => MutableArrayWithObject<T, M>;
32
-
33
21
  export interface ManyArrayCreateArgs {
22
+ identifiers: StableRecordIdentifier[];
23
+ type: string;
34
24
  store: Store;
35
- type: ShimModelClass;
25
+ allowMutation: boolean;
26
+ manager: LegacySupport;
27
+
36
28
  identifier: StableRecordIdentifier;
37
29
  recordData: RecordData;
30
+ meta: Dict<unknown> | null;
31
+ links: Links | PaginationLinks | null;
38
32
  key: string;
39
33
  isPolymorphic: boolean;
40
34
  isAsync: boolean;
41
35
  _inverseIsAsync: boolean;
42
- legacySupport: LegacySupport;
43
36
  isLoaded: boolean;
44
37
  }
45
38
  /**
@@ -84,44 +77,26 @@ export interface ManyArrayCreateArgs {
84
77
 
85
78
  @class ManyArray
86
79
  @public
87
- @extends Ember.EmberObject
88
- @uses Ember.MutableArray
89
80
  */
90
- export default class ManyArray extends MutableArrayWithObject<StableRecordIdentifier, RecordInstance> {
81
+ export default class RelatedCollection extends RecordArray {
91
82
  declare isAsync: boolean;
92
- declare isLoaded: boolean;
93
- declare isPolymorphic: boolean;
94
- declare _isDirty: boolean;
95
- declare _isUpdating: boolean;
96
- declare _hasNotified: boolean;
97
- declare __hasArrayObservers: boolean;
98
- declare hasArrayObservers: boolean; // override the base declaration
99
- declare _length: number;
100
- declare _meta: Dict<unknown> | null;
101
- declare _links: Links | PaginationLinks | null;
102
- declare currentState: StableRecordIdentifier[];
103
- declare identifier: StableRecordIdentifier;
104
- declare recordData: RecordData;
105
- declare legacySupport: LegacySupport;
106
- declare store: Store;
107
- declare key: string;
108
- declare type: DSModelSchema;
109
-
110
- init() {
111
- super.init();
112
-
113
- /**
83
+ /**
114
84
  The loading state of this array
115
85
 
116
86
  @property {Boolean} isLoaded
117
87
  @public
118
88
  */
119
- this.isLoaded = this.isLoaded || false;
120
- this.isAsync = this.isAsync || false;
121
89
 
122
- this._length = 0;
90
+ declare isLoaded: boolean;
91
+ /**
92
+ `true` if the relationship is polymorphic, `false` otherwise.
123
93
 
124
- /**
94
+ @property {Boolean} isPolymorphic
95
+ @private
96
+ */
97
+ declare isPolymorphic: boolean;
98
+ declare _inverseIsAsync: boolean;
99
+ /**
125
100
  Metadata associated with the request for async hasMany relationships.
126
101
 
127
102
  Example
@@ -158,206 +133,146 @@ export default class ManyArray extends MutableArrayWithObject<StableRecordIdenti
158
133
  @property {Object | null} meta
159
134
  @public
160
135
  */
161
- this._meta = this._meta || null;
162
-
163
- /**
136
+ declare meta: Dict<unknown> | null;
137
+ /**
164
138
  * Retrieve the links for this relationship
165
139
  *
166
140
  @property {Object | null} links
167
141
  @public
168
142
  */
169
- this._links = this._links || null;
170
-
171
- /**
172
- `true` if the relationship is polymorphic, `false` otherwise.
173
-
174
- @property {Boolean} isPolymorphic
175
- @private
176
- */
177
- this.isPolymorphic = this.isPolymorphic || false;
178
-
179
- /**
180
- The relationship which manages this array.
181
-
182
- @property {ManyRelationship} relationship
183
- @private
184
- */
185
- this.currentState = [];
186
- this._isUpdating = false;
187
- this._isDirty = false;
188
- /*
189
- * Unfortunately ArrayProxy adds it's observers lazily,
190
- * so in a first-render situation we may sometimes notify
191
- * prior to the ArrayProxy having installed it's observers
192
- * (which occurs during _revalidate()).
193
- *
194
- * This leads to the flush occuring on access, the flush takes
195
- * the hasObservers codepath which in code out of our control
196
- * notifies again leading to a glimmer rendering invalidation error.
197
- *
198
- * We use this flag to detect the case where we notified without
199
- * array observers but observers were installed prior to flush.
200
- *
201
- * We do not need to fire array observers at all in this case
202
- * since it will be the first-access for those observers.
203
- */
204
- this._hasNotified = false;
205
- // make sure we initialize to the correct state
206
- // since the user has already accessed
207
- this.retrieveLatest();
208
- }
209
-
210
- // TODO refactor away _hasArrayObservers for tests
211
- get _hasArrayObservers() {
212
- // cast necessary because hasArrayObservers is typed as a ComputedProperty<boolean> vs a boolean;
213
- return this.hasArrayObservers || this.__hasArrayObservers;
214
- }
215
-
216
- notify() {
217
- this._isDirty = true;
218
- if (this._hasArrayObservers && !this._hasNotified) {
219
- this.retrieveLatest();
220
- } else {
221
- this._hasNotified = true;
222
- this.notifyPropertyChange('[]');
223
- this.notifyPropertyChange('firstObject');
224
- this.notifyPropertyChange('lastObject');
225
- }
226
- }
227
-
228
- get length() {
229
- if (this._isDirty) {
230
- this.retrieveLatest();
231
- }
232
- // By using `get()`, the tracking system knows to pay attention to changes that occur.
233
- get(this, '[]');
234
-
235
- return this._length;
236
- }
237
-
238
- set length(value) {
239
- this._length = value;
240
- }
241
-
242
- get links() {
243
- get(this, '[]');
244
- if (this._isDirty) {
245
- this.retrieveLatest();
246
- }
247
- return this._links;
248
- }
249
- set links(v) {
250
- this._links = v;
251
- }
252
-
253
- get meta() {
254
- get(this, '[]');
255
- if (this._isDirty) {
256
- this.retrieveLatest();
257
- }
258
- return this._meta;
259
- }
260
- set meta(v) {
261
- this._meta = v;
262
- }
263
-
264
- objectAt(index: number): RecordInstance | undefined {
265
- if (this._isDirty) {
266
- this.retrieveLatest();
267
- }
268
- let identifier = this.currentState[index];
269
- if (identifier === undefined) {
270
- return;
271
- }
272
-
273
- 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;
274
159
  }
275
160
 
276
- replace(idx: number, amt: number, objects?: RecordInstance[]) {
277
- assert(`Cannot push mutations to the cache while updating the relationship from cache`, !this._isUpdating);
278
- assert(
279
- 'The third argument to replace needs to be an array.',
280
- !objects || Array.isArray(objects) || EmberArray.detect(objects)
281
- );
282
- const { store, identifier } = this;
283
- store._backburner.join(() => {
284
- let identifiers: StableRecordIdentifier[];
285
- if (amt > 0) {
286
- identifiers = this.currentState.slice(idx, idx + amt);
287
- this.recordData.removeFromHasMany(identifier, this.key, identifiers);
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;
288
171
  }
289
- if (objects && objects.length > 0) {
290
- this.recordData.addToHasMany(
291
- identifier,
292
- this.key,
293
- objects.map((obj: RecordInstance) => recordIdentifierFor(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
- const identifier = this.identifier;
309
-
310
- let jsonApi = (this.recordData as NonSingletonRecordDataManager).getRelationship(
311
- identifier,
312
- this.key,
313
- true
314
- ) as CollectionResourceRelationship;
315
- const cache = this.store._instanceCache;
316
- const idCache = this.store.identifierCache;
317
-
318
- let identifiers: StableRecordIdentifier[] = [];
319
- if (jsonApi.data) {
320
- for (let i = 0; i < jsonApi.data.length; i++) {
321
- const identifier = idCache.getOrCreateRecordIdentifier(jsonApi.data[i]);
322
-
323
- if (cache.recordIsLoaded(identifier, true)) {
324
- identifiers.push(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
+ });
325
263
  }
326
- }
327
- }
328
-
329
- if (jsonApi.meta) {
330
- this._meta = jsonApi.meta;
331
- }
332
-
333
- if (jsonApi.links) {
334
- this._links = jsonApi.links;
335
- }
336
264
 
337
- if (this._hasArrayObservers && !this._hasNotified) {
338
- // diff to find changes
339
- let diff = diffArray(this.currentState, identifiers);
340
- // it's null if no change found
341
- if (diff.firstChangeIndex !== null) {
342
- // we found a change
343
- this.arrayContentWillChange(diff.firstChangeIndex, diff.removedCount, diff.addedCount);
344
- this._length = identifiers.length;
345
- this.currentState = identifiers;
346
- this.arrayContentDidChange(diff.firstChangeIndex, diff.removedCount, diff.addedCount);
265
+ break;
347
266
  }
348
- } else {
349
- this._hasNotified = false;
350
- this._length = identifiers.length;
351
- this.currentState = identifiers;
267
+ default:
268
+ assert(`unable to convert ${prop} into a transaction that updates the cache state for this record array`);
352
269
  }
353
-
354
- this._isUpdating = false;
355
270
  }
356
271
 
357
- destroy() {
358
- this._length = 0;
359
- this.currentState = [];
360
- return super.destroy();
272
+ notify() {
273
+ const tag = this[IDENTIFIER_ARRAY_TAG];
274
+ tag.ref = null;
275
+ tag.shouldReset = true;
361
276
  }
362
277
 
363
278
  /**
@@ -384,7 +299,7 @@ export default class ManyArray extends MutableArrayWithObject<StableRecordIdenti
384
299
  */
385
300
  reload(options?: FindOptions) {
386
301
  // TODO this is odd, we don't ask the store for anything else like this?
387
- return this.legacySupport.reloadHasMany(this.key, options);
302
+ return this._manager.reloadHasMany(this.key, options);
388
303
  }
389
304
 
390
305
  /**
@@ -405,18 +320,6 @@ export default class ManyArray extends MutableArrayWithObject<StableRecordIdenti
405
320
  @public
406
321
  @return {PromiseArray} promise
407
322
  */
408
- save() {
409
- let manyArray = this;
410
- let promiseLabel = 'DS: ManyArray#save ' + this.type.modelName;
411
- let promise = all(this.invoke('save'), promiseLabel).then(
412
- () => manyArray,
413
- null,
414
- 'DS: ManyArray#save return ManyArray'
415
- );
416
-
417
- // TODO deprecate returning a promiseArray here
418
- return PromiseArray.create({ promise });
419
- }
420
323
 
421
324
  /**
422
325
  Create a child record within the owner
@@ -427,11 +330,70 @@ export default class ManyArray extends MutableArrayWithObject<StableRecordIdenti
427
330
  @return {Model} record
428
331
  */
429
332
  createRecord(hash: CreateRecordProperties): RecordInstance {
430
- const { store, type } = this;
333
+ const { store } = this;
431
334
 
432
- const record = store.createRecord(type.modelName, hash);
433
- this.pushObject(record);
335
+ const record = store.createRecord(this.modelName, hash);
336
+ this.push(record);
434
337
 
435
338
  return record;
436
339
  }
437
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
+ }