@ember-data/model 4.5.0-beta.0 → 4.6.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.
@@ -1,15 +1,15 @@
1
1
  import { assert } from '@ember/debug';
2
2
  import { dependentKeyCompat } from '@ember/object/compat';
3
+ import { DEBUG } from '@glimmer/env';
3
4
  import { cached, tracked } from '@glimmer/tracking';
4
5
 
5
- import type { RecordData } from '@ember-data/record-data/-private';
6
- import { errorsArrayToHash } from '@ember-data/store/-private';
7
- import type CoreStore from '@ember-data/store/-private/system/core-store';
8
- import type { NotificationType } from '@ember-data/store/-private/system/record-notification-manager';
9
- import type RequestCache from '@ember-data/store/-private/system/request-cache';
10
- import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier';
11
-
12
- import notifyChanges from './notify-changes';
6
+ import type Store from '@ember-data/store';
7
+ import { storeFor } from '@ember-data/store';
8
+ import { errorsArrayToHash, recordIdentifierFor } from '@ember-data/store/-private';
9
+ import type { NotificationType } from '@ember-data/store/-private/record-notification-manager';
10
+ import type RequestCache from '@ember-data/store/-private/request-cache';
11
+ import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
12
+ import type { RecordData } from '@ember-data/types/q/record-data';
13
13
 
14
14
  type Model = InstanceType<typeof import('./model')>;
15
15
 
@@ -139,7 +139,7 @@ root
139
139
  @internal
140
140
  */
141
141
  export default class RecordState {
142
- declare store: CoreStore;
142
+ declare store: Store;
143
143
  declare identifier: StableRecordIdentifier;
144
144
  declare record: Model;
145
145
  declare rs: RequestCache;
@@ -152,24 +152,23 @@ export default class RecordState {
152
152
  declare _lastError: any;
153
153
 
154
154
  constructor(record: Model) {
155
- const { store } = record;
156
-
157
- let id = record._internalModel.identifier;
155
+ const store = storeFor(record)!;
156
+ const identity = recordIdentifierFor(record);
158
157
 
158
+ this.identifier = identity;
159
159
  this.record = record;
160
- this.recordData = record._internalModel._recordData;
160
+ this.recordData = store._instanceCache.getRecordData(identity);
161
161
 
162
162
  this.pendingCount = 0;
163
163
  this.fulfilledCount = 0;
164
164
  this.rejectedCount = 0;
165
-
166
165
  this._errorRequests = [];
167
166
  this._lastError = null;
168
167
 
169
168
  let requests = store.getRequestStateService();
170
169
  let notifications = store._notificationManager;
171
170
 
172
- requests.subscribeForRecord(id, (req) => {
171
+ const handleRequest = (req) => {
173
172
  if (req.type === 'mutation') {
174
173
  switch (req.state) {
175
174
  case 'pending':
@@ -218,10 +217,20 @@ export default class RecordState {
218
217
  break;
219
218
  }
220
219
  }
221
- });
220
+ };
221
+
222
+ requests.subscribeForRecord(identity, handleRequest);
223
+
224
+ // we instantiate lazily
225
+ // so we grab anything we don't have yet
226
+ if (!DEBUG) {
227
+ const lastRequest = requests.getLastRequestForRecord(identity);
228
+ if (lastRequest) {
229
+ handleRequest(lastRequest);
230
+ }
231
+ }
222
232
 
223
- notifications.subscribe(id, (identifier: StableRecordIdentifier, type: NotificationType, key?: string) => {
224
- notifyChanges(identifier, type, key, record, store);
233
+ notifications.subscribe(identity, (identifier: StableRecordIdentifier, type: NotificationType, key?: string) => {
225
234
  switch (type) {
226
235
  case 'state':
227
236
  this.notify('isNew');
@@ -253,15 +262,15 @@ export default class RecordState {
253
262
  }
254
263
 
255
264
  updateInvalidErrors() {
256
- let jsonApiErrors = this.recordData.getErrors!();
265
+ let jsonApiErrors = this.recordData.getErrors!(this.identifier);
257
266
 
258
267
  const { errors } = this.record;
259
- errors._clear();
268
+ errors.clear();
260
269
  let newErrors = errorsArrayToHash(jsonApiErrors);
261
270
  let errorKeys = Object.keys(newErrors);
262
271
 
263
272
  for (let i = 0; i < errorKeys.length; i++) {
264
- errors._add(errorKeys[i], newErrors[errorKeys[i]]);
273
+ errors.add(errorKeys[i], newErrors[errorKeys[i]]);
265
274
  }
266
275
  }
267
276
 
@@ -280,6 +289,7 @@ export default class RecordState {
280
289
  return !this.isLoaded && this.pendingCount > 0 && this.fulfilledCount === 0;
281
290
  }
282
291
 
292
+ // TODO @runspired handle "unloadRecord" see note in InternalModel
283
293
  @tagged
284
294
  get isLoaded() {
285
295
  if (this.isNew) {
@@ -0,0 +1,585 @@
1
+ import { dependentKeyCompat } from '@ember/object/compat';
2
+ import { cached, tracked } from '@glimmer/tracking';
3
+
4
+ import type { Object as JSONObject, Value as JSONValue } from 'json-typescript';
5
+ import { resolve } from 'rsvp';
6
+
7
+ import type { BelongsToRelationship } from '@ember-data/record-data/-private';
8
+ import type Store from '@ember-data/store';
9
+ import { assertPolymorphicType } from '@ember-data/store/-debug';
10
+ import { recordIdentifierFor } from '@ember-data/store/-private';
11
+ import type { NotificationType } from '@ember-data/store/-private/record-notification-manager';
12
+ import type { DebugWeakCache } from '@ember-data/store/-private/weak-cache';
13
+ import type {
14
+ LinkObject,
15
+ Links,
16
+ SingleResourceDocument,
17
+ SingleResourceRelationship,
18
+ } from '@ember-data/types/q/ember-data-json-api';
19
+ import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
20
+ import type { RecordInstance } from '@ember-data/types/q/record-instance';
21
+ import type { Dict } from '@ember-data/types/q/utils';
22
+
23
+ import type { LegacySupport } from '../legacy-relationships-support';
24
+ import { LEGACY_SUPPORT } from '../model';
25
+
26
+ /**
27
+ @module @ember-data/model
28
+ */
29
+
30
+ interface ResourceIdentifier {
31
+ links?: {
32
+ related?: string | LinkObject;
33
+ };
34
+ meta?: JSONObject;
35
+ }
36
+
37
+ function isResourceIdentiferWithRelatedLinks(
38
+ value: SingleResourceRelationship | ResourceIdentifier | null
39
+ ): value is ResourceIdentifier & { links: { related: string | LinkObject | null } } {
40
+ return Boolean(value && value.links && value.links.related);
41
+ }
42
+
43
+ /**
44
+ A `BelongsToReference` is a low-level API that allows users and
45
+ addon authors to perform meta-operations on a belongs-to
46
+ relationship.
47
+
48
+ @class BelongsToReference
49
+ @public
50
+ */
51
+ export default class BelongsToReference {
52
+ declare key: string;
53
+ declare belongsToRelationship: BelongsToRelationship;
54
+ declare type: string;
55
+ #identifier: StableRecordIdentifier;
56
+ declare store: Store;
57
+
58
+ // unsubscribe tokens given to us by the notification manager
59
+ #token!: Object;
60
+ #relatedToken: Object | null = null;
61
+
62
+ @tracked _ref = 0;
63
+
64
+ constructor(
65
+ store: Store,
66
+ parentIdentifier: StableRecordIdentifier,
67
+ belongsToRelationship: BelongsToRelationship,
68
+ key: string
69
+ ) {
70
+ this.key = key;
71
+ this.belongsToRelationship = belongsToRelationship;
72
+ this.type = belongsToRelationship.definition.type;
73
+ this.store = store;
74
+ this.#identifier = parentIdentifier;
75
+
76
+ this.#token = store._notificationManager.subscribe(
77
+ parentIdentifier,
78
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
79
+ if ((bucket === 'relationships' || bucket === 'property') && notifiedKey === key) {
80
+ this._ref++;
81
+ }
82
+ }
83
+ );
84
+
85
+ // TODO inverse
86
+ }
87
+
88
+ destroy() {
89
+ // TODO @feature we need the notification manager often enough
90
+ // we should potentially just expose it fully public
91
+ this.store._notificationManager.unsubscribe(this.#token);
92
+ if (this.#relatedToken) {
93
+ this.store._notificationManager.unsubscribe(this.#relatedToken);
94
+ }
95
+ }
96
+
97
+ @cached
98
+ @dependentKeyCompat
99
+ get _relatedIdentifier(): StableRecordIdentifier | null {
100
+ this._ref; // consume the tracked prop
101
+ if (this.#relatedToken) {
102
+ this.store._notificationManager.unsubscribe(this.#relatedToken);
103
+ }
104
+
105
+ let resource = this._resource();
106
+ if (resource && resource.data) {
107
+ const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource.data);
108
+ this.#relatedToken = this.store._notificationManager.subscribe(
109
+ identifier,
110
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
111
+ if (bucket === 'identity' || ((bucket === 'attributes' || bucket === 'property') && notifiedKey === 'id')) {
112
+ this._ref++;
113
+ }
114
+ }
115
+ );
116
+
117
+ return identifier;
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ The `id` of the record that this reference refers to. Together, the
125
+ `type()` and `id()` methods form a composite key for the identity
126
+ map. This can be used to access the id of an async relationship
127
+ without triggering a fetch that would normally happen if you
128
+ attempted to use `record.get('relationship.id')`.
129
+
130
+ Example
131
+
132
+ ```javascript
133
+ // models/blog.js
134
+ import Model, { belongsTo } from '@ember-data/model';
135
+
136
+ export default class BlogModel extends Model {
137
+ @belongsTo({ async: true }) user;
138
+ }
139
+
140
+ let blog = store.push({
141
+ data: {
142
+ type: 'blog',
143
+ id: 1,
144
+ relationships: {
145
+ user: {
146
+ data: { type: 'user', id: 1 }
147
+ }
148
+ }
149
+ }
150
+ });
151
+ let userRef = blog.belongsTo('user');
152
+
153
+ // get the identifier of the reference
154
+ if (userRef.remoteType() === "id") {
155
+ let id = userRef.id();
156
+ }
157
+ ```
158
+
159
+ @method id
160
+ @public
161
+ @return {String} The id of the record in this belongsTo relationship.
162
+ */
163
+ id(): string | null {
164
+ return this._relatedIdentifier?.id || null;
165
+ }
166
+
167
+ /**
168
+ The link Ember Data will use to fetch or reload this belongs-to
169
+ relationship. By default it uses only the "related" resource linkage.
170
+
171
+ Example
172
+
173
+ ```javascript
174
+ // models/blog.js
175
+ import Model, { belongsTo } from '@ember-data/model';
176
+ export default Model.extend({
177
+ user: belongsTo({ async: true })
178
+ });
179
+
180
+ let blog = store.push({
181
+ data: {
182
+ type: 'blog',
183
+ id: 1,
184
+ relationships: {
185
+ user: {
186
+ links: {
187
+ related: '/articles/1/author'
188
+ }
189
+ }
190
+ }
191
+ }
192
+ });
193
+ let userRef = blog.belongsTo('user');
194
+
195
+ // get the identifier of the reference
196
+ if (userRef.remoteType() === "link") {
197
+ let link = userRef.link();
198
+ }
199
+ ```
200
+
201
+ @method link
202
+ @public
203
+ @return {String} The link Ember Data will use to fetch or reload this belongs-to relationship.
204
+ */
205
+ link(): string | null {
206
+ let resource = this._resource();
207
+
208
+ if (isResourceIdentiferWithRelatedLinks(resource)) {
209
+ if (resource.links) {
210
+ let related = resource.links.related;
211
+ return !related || typeof related === 'string' ? related : related.href;
212
+ }
213
+ }
214
+ return null;
215
+ }
216
+
217
+ /**
218
+ * any links that have been received for this relationship
219
+ *
220
+ * @method links
221
+ * @public
222
+ * @returns
223
+ */
224
+ links(): Links | null {
225
+ let resource = this._resource();
226
+
227
+ return resource && resource.links ? resource.links : null;
228
+ }
229
+
230
+ /**
231
+ The meta data for the belongs-to relationship.
232
+
233
+ Example
234
+
235
+ ```javascript
236
+ // models/blog.js
237
+ import Model, { belongsTo } from '@ember-data/model';
238
+ export default Model.extend({
239
+ user: belongsTo({ async: true })
240
+ });
241
+
242
+ let blog = store.push({
243
+ data: {
244
+ type: 'blog',
245
+ id: 1,
246
+ relationships: {
247
+ user: {
248
+ links: {
249
+ related: {
250
+ href: '/articles/1/author'
251
+ },
252
+ },
253
+ meta: {
254
+ lastUpdated: 1458014400000
255
+ }
256
+ }
257
+ }
258
+ }
259
+ });
260
+
261
+ let userRef = blog.belongsTo('user');
262
+
263
+ userRef.meta() // { lastUpdated: 1458014400000 }
264
+ ```
265
+
266
+ @method meta
267
+ @public
268
+ @return {Object} The meta information for the belongs-to relationship.
269
+ */
270
+ meta() {
271
+ let meta: Dict<JSONValue> | null = null;
272
+ let resource = this._resource();
273
+ if (resource && resource.meta && typeof resource.meta === 'object') {
274
+ meta = resource.meta;
275
+ }
276
+ return meta;
277
+ }
278
+
279
+ _resource() {
280
+ return this.store._instanceCache.recordDataFor(this.#identifier, false).getBelongsTo(this.key);
281
+ }
282
+
283
+ /**
284
+ This returns a string that represents how the reference will be
285
+ looked up when it is loaded. If the relationship has a link it will
286
+ use the "link" otherwise it defaults to "id".
287
+
288
+ Example
289
+
290
+ ```app/models/post.js
291
+ import Model, { hasMany } from '@ember-data/model';
292
+
293
+ export default class PostModel extends Model {
294
+ @hasMany({ async: true }) comments;
295
+ }
296
+ ```
297
+
298
+ ```javascript
299
+ let post = store.push({
300
+ data: {
301
+ type: 'post',
302
+ id: 1,
303
+ relationships: {
304
+ comments: {
305
+ data: [{ type: 'comment', id: 1 }]
306
+ }
307
+ }
308
+ }
309
+ });
310
+
311
+ let commentsRef = post.hasMany('comments');
312
+
313
+ // get the identifier of the reference
314
+ if (commentsRef.remoteType() === "ids") {
315
+ let ids = commentsRef.ids();
316
+ } else if (commentsRef.remoteType() === "link") {
317
+ let link = commentsRef.link();
318
+ }
319
+ ```
320
+
321
+ @method remoteType
322
+ @public
323
+ @return {String} The name of the remote type. This should either be `link` or `id`
324
+ */
325
+ remoteType(): 'link' | 'id' {
326
+ let value = this._resource();
327
+ if (isResourceIdentiferWithRelatedLinks(value)) {
328
+ return 'link';
329
+ }
330
+ return 'id';
331
+ }
332
+
333
+ /**
334
+ `push` can be used to update the data in the relationship and Ember
335
+ Data will treat the new data as the canonical value of this
336
+ relationship on the backend.
337
+
338
+ Example
339
+
340
+ ```app/models/blog.js
341
+ import Model, { belongsTo } from '@ember-data/model';
342
+
343
+ export default class BlogModel extends Model {
344
+ @belongsTo({ async: true }) user;
345
+ }
346
+
347
+ let blog = store.push({
348
+ data: {
349
+ type: 'blog',
350
+ id: 1,
351
+ relationships: {
352
+ user: {
353
+ data: { type: 'user', id: 1 }
354
+ }
355
+ }
356
+ }
357
+ });
358
+ let userRef = blog.belongsTo('user');
359
+
360
+ // provide data for reference
361
+ userRef.push({
362
+ data: {
363
+ type: 'user',
364
+ id: 1,
365
+ attributes: {
366
+ username: "@user"
367
+ }
368
+ }
369
+ }).then(function(user) {
370
+ userRef.value() === user;
371
+ });
372
+ ```
373
+
374
+ @method push
375
+ @public
376
+ @param {Object|Promise} objectOrPromise a promise that resolves to a JSONAPI document object describing the new value of this relationship.
377
+ @return {Promise<record>} A promise that resolves with the new value in this belongs-to relationship.
378
+ */
379
+ async push(data: SingleResourceDocument | Promise<SingleResourceDocument>): Promise<RecordInstance> {
380
+ // TODO @deprecate pushing unresolved payloads
381
+ const jsonApiDoc = await resolve(data);
382
+ let record = this.store.push(jsonApiDoc);
383
+
384
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
385
+ assertPolymorphicType(
386
+ this.belongsToRelationship.identifier,
387
+ this.belongsToRelationship.definition,
388
+ recordIdentifierFor(record),
389
+ this.store
390
+ );
391
+
392
+ const { graph, identifier } = this.belongsToRelationship;
393
+ this.store._backburner.join(() => {
394
+ graph.push({
395
+ op: 'replaceRelatedRecord',
396
+ record: identifier,
397
+ field: this.key,
398
+ value: recordIdentifierFor(record),
399
+ });
400
+ });
401
+
402
+ return record;
403
+ }
404
+
405
+ /**
406
+ `value()` synchronously returns the current value of the belongs-to
407
+ relationship. Unlike `record.get('relationshipName')`, calling
408
+ `value()` on a reference does not trigger a fetch if the async
409
+ relationship is not yet loaded. If the relationship is not loaded
410
+ it will always return `null`.
411
+
412
+ Example
413
+
414
+ ```javascript
415
+ // models/blog.js
416
+ import Model, { belongsTo } from '@ember-data/model';
417
+
418
+ export default class BlogModel extends Model {
419
+ @belongsTo({ async: true }) user;
420
+ }
421
+
422
+ let blog = store.push({
423
+ data: {
424
+ type: 'blog',
425
+ id: 1,
426
+ relationships: {
427
+ user: {
428
+ data: { type: 'user', id: 1 }
429
+ }
430
+ }
431
+ }
432
+ });
433
+ let userRef = blog.belongsTo('user');
434
+
435
+ userRef.value(); // null
436
+
437
+ // provide data for reference
438
+ userRef.push({
439
+ data: {
440
+ type: 'user',
441
+ id: 1,
442
+ attributes: {
443
+ username: "@user"
444
+ }
445
+ }
446
+ }).then(function(user) {
447
+ userRef.value(); // user
448
+ });
449
+ ```
450
+
451
+ @method value
452
+ @public
453
+ @return {Model} the record in this relationship
454
+ */
455
+ value(): RecordInstance | null {
456
+ let resource = this._resource();
457
+ return resource && resource.data ? this.store.peekRecord(resource.data) : null;
458
+ }
459
+
460
+ /**
461
+ Loads a record in a belongs-to relationship if it is not already
462
+ loaded. If the relationship is already loaded this method does not
463
+ trigger a new load.
464
+
465
+ Example
466
+
467
+ ```javascript
468
+ // models/blog.js
469
+ import Model, { belongsTo } from '@ember-data/model';
470
+
471
+ export default class BlogModel extends Model {
472
+ @belongsTo({ async: true }) user;
473
+ }
474
+
475
+ let blog = store.push({
476
+ data: {
477
+ type: 'blog',
478
+ id: 1,
479
+ relationships: {
480
+ user: {
481
+ data: { type: 'user', id: 1 }
482
+ }
483
+ }
484
+ }
485
+ });
486
+ let userRef = blog.belongsTo('user');
487
+
488
+ userRef.value(); // null
489
+
490
+ userRef.load().then(function(user) {
491
+ userRef.value() === user
492
+ });
493
+ ```
494
+
495
+ You may also pass in an options object whose properties will be
496
+ fed forward. This enables you to pass `adapterOptions` into the
497
+ request given to the adapter via the reference.
498
+
499
+ Example
500
+
501
+ ```javascript
502
+ userRef.load({ adapterOptions: { isPrivate: true } }).then(function(user) {
503
+ userRef.value() === user;
504
+ });
505
+ ```
506
+ ```app/adapters/user.js
507
+ import Adapter from '@ember-data/adapter';
508
+
509
+ export default class UserAdapter extends Adapter {
510
+ findRecord(store, type, id, snapshot) {
511
+ // In the adapter you will have access to adapterOptions.
512
+ let adapterOptions = snapshot.adapterOptions;
513
+ }
514
+ });
515
+ ```
516
+
517
+ @method load
518
+ @public
519
+ @param {Object} options the options to pass in.
520
+ @return {Promise} a promise that resolves with the record in this belongs-to relationship.
521
+ */
522
+ load(options?: Dict<unknown>) {
523
+ const support: LegacySupport = (
524
+ LEGACY_SUPPORT as DebugWeakCache<StableRecordIdentifier, LegacySupport>
525
+ ).getWithError(this.#identifier);
526
+ return support.getBelongsTo(this.key, options);
527
+ }
528
+
529
+ /**
530
+ Triggers a reload of the value in this relationship. If the
531
+ remoteType is `"link"` Ember Data will use the relationship link to
532
+ reload the relationship. Otherwise it will reload the record by its
533
+ id.
534
+
535
+ Example
536
+
537
+ ```javascript
538
+ // models/blog.js
539
+ import Model, { belongsTo } from '@ember-data/model';
540
+
541
+ export default class BlogModel extends Model {
542
+ @belongsTo({ async: true }) user;
543
+ }
544
+
545
+ let blog = store.push({
546
+ data: {
547
+ type: 'blog',
548
+ id: 1,
549
+ relationships: {
550
+ user: {
551
+ data: { type: 'user', id: 1 }
552
+ }
553
+ }
554
+ }
555
+ });
556
+ let userRef = blog.belongsTo('user');
557
+
558
+ userRef.reload().then(function(user) {
559
+ userRef.value() === user
560
+ });
561
+ ```
562
+
563
+ You may also pass in an options object whose properties will be
564
+ fed forward. This enables you to pass `adapterOptions` into the
565
+ request given to the adapter via the reference. A full example
566
+ can be found in the `load` method.
567
+
568
+ Example
569
+
570
+ ```javascript
571
+ userRef.reload({ adapterOptions: { isPrivate: true } })
572
+ ```
573
+
574
+ @method reload
575
+ @public
576
+ @param {Object} options the options to pass in.
577
+ @return {Promise} a promise that resolves with the record in this belongs-to relationship after the reload has completed.
578
+ */
579
+ reload(options?: Dict<unknown>) {
580
+ const support: LegacySupport = (
581
+ LEGACY_SUPPORT as DebugWeakCache<StableRecordIdentifier, LegacySupport>
582
+ ).getWithError(this.#identifier);
583
+ return support.reloadBelongsTo(this.key, options).then(() => this.value());
584
+ }
585
+ }