velocious 1.0.448 → 1.0.449

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 (53) hide show
  1. package/build/database/record/index.js +13 -8
  2. package/build/environment-handlers/node/cli/commands/generate/base-models.js +1 -16
  3. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +125 -73
  4. package/build/frontend-model-controller.js +9 -0
  5. package/build/frontend-model-resource/base-resource.js +266 -53
  6. package/build/frontend-models/base.js +241 -97
  7. package/build/frontend-models/preloader.js +3 -2
  8. package/build/src/background-jobs/job-record.d.ts +2 -1
  9. package/build/src/background-jobs/job-record.d.ts.map +1 -1
  10. package/build/src/database/query/preloader/belongs-to.d.ts +2 -2
  11. package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
  12. package/build/src/database/query/preloader/has-many.d.ts +1 -1
  13. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  14. package/build/src/database/query/preloader/has-one.d.ts +2 -2
  15. package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
  16. package/build/src/database/query/preloader.d.ts +1 -1
  17. package/build/src/database/query/preloader.d.ts.map +1 -1
  18. package/build/src/database/record/attachments/handle.d.ts +1 -1
  19. package/build/src/database/record/attachments/handle.d.ts.map +1 -1
  20. package/build/src/database/record/index.d.ts +23 -13
  21. package/build/src/database/record/index.d.ts.map +1 -1
  22. package/build/src/database/record/index.js +14 -9
  23. package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
  24. package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +2 -15
  25. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +89 -32
  26. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  27. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +123 -72
  28. package/build/src/frontend-model-controller.d.ts.map +1 -1
  29. package/build/src/frontend-model-controller.js +8 -1
  30. package/build/src/frontend-model-resource/base-resource.d.ts +203 -64
  31. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  32. package/build/src/frontend-model-resource/base-resource.js +237 -54
  33. package/build/src/frontend-models/base.d.ts +173 -110
  34. package/build/src/frontend-models/base.d.ts.map +1 -1
  35. package/build/src/frontend-models/base.js +218 -102
  36. package/build/src/frontend-models/preloader.d.ts.map +1 -1
  37. package/build/src/frontend-models/preloader.js +4 -3
  38. package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  39. package/build/src/testing/expect.d.ts +6 -0
  40. package/build/src/testing/expect.d.ts.map +1 -1
  41. package/build/src/testing/expect.js +9 -1
  42. package/build/testing/browser-frontend-model-event-hook-scenarios.js +1 -1
  43. package/build/testing/expect.js +9 -0
  44. package/package.json +1 -1
  45. package/src/database/record/index.js +13 -8
  46. package/src/environment-handlers/node/cli/commands/generate/base-models.js +1 -16
  47. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +125 -73
  48. package/src/frontend-model-controller.js +9 -0
  49. package/src/frontend-model-resource/base-resource.js +266 -53
  50. package/src/frontend-models/base.js +241 -97
  51. package/src/frontend-models/preloader.js +3 -2
  52. package/src/testing/browser-frontend-model-event-hook-scenarios.js +1 -1
  53. package/src/testing/expect.js +9 -0
@@ -20,6 +20,28 @@ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQuer
20
20
  /**
21
21
  * FrontendModelRequestCommandType type.
22
22
  @typedef {FrontendModelCommandType | string} FrontendModelRequestCommandType */
23
+ /**
24
+ * Model-like instance value supported by frontend-model transport.
25
+ * @typedef {{attributes: () => Record<string, unknown>}} FrontendModelTransportModelValue
26
+ */
27
+ /**
28
+ * Special scalar values restored by frontend-model transport.
29
+ * @typedef {undefined | null | boolean | number | string | bigint | Date | FrontendModelTransportModelValue} FrontendModelTransportScalarValue
30
+ */
31
+ /**
32
+ * Plain object supported by frontend-model transport values.
33
+ * Nested values are intentionally opaque because TypeScript rejects recursive
34
+ * JSDoc typedefs for this transport value contract.
35
+ * @typedef {Record<string, unknown>} FrontendModelTransportObject
36
+ */
37
+ /**
38
+ * Value supported by frontend-model transport serialization and deserialization.
39
+ * @typedef {FrontendModelTransportScalarValue | FrontendModelTransportObject | Array<unknown>} FrontendModelTransportValue
40
+ */
41
+ /**
42
+ * Frontend model attribute value used when generated metadata cannot infer a narrower type.
43
+ * @typedef {FrontendModelTransportValue} FrontendModelAttributeValue
44
+ */
23
45
  /**
24
46
  * Defines this typedef.
25
47
  * @typedef {{type: "hasOne" | "hasMany"}} FrontendModelAttachmentDefinition
@@ -34,11 +56,15 @@ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQuer
34
56
  */
35
57
  /**
36
58
  * Frontend model constructor type.
37
- * @typedef {{new (attributes?: Record<string, ?>): FrontendModelBase}} FrontendModelConstructor
59
+ * @template {FrontendModelBase} [T=FrontendModelBase]
60
+ * @typedef {{new (attributes?: Record<string, FrontendModelAttributeValue>): T}} FrontendModelConstructor
38
61
  */
39
62
  /**
40
- * Frontend model static side without generated per-model create overloads.
41
- * @typedef {FrontendModelConstructor & Omit<typeof FrontendModelBase, "create">} FrontendModelClass
63
+ * Frontend model static side.
64
+ * @template {FrontendModelBase} [T=FrontendModelBase]
65
+ * @template {Record<string, FrontendModelAttributeValue>} [Attributes=Record<string, FrontendModelAttributeValue>]
66
+ * @template {Record<string, FrontendModelAttributeValue>} [CreateAttributes=Record<string, FrontendModelAttributeValue>]
67
+ * @typedef {{new (attributes?: Attributes | CreateAttributes): T, create(attributes?: CreateAttributes): Promise<T>} & Omit<typeof FrontendModelBase, "create">} FrontendModelClass
42
68
  */
43
69
  /**
44
70
  * FrontendModelTransportConfig type.
@@ -238,27 +264,29 @@ export class AttributeNotSelectedError extends Error {
238
264
 
239
265
  /**
240
266
  * Lightweight singular relationship state holder for frontend model instances.
241
- * @template {FrontendModelClass} S
242
- * @template {FrontendModelClass} T
267
+ * @template {FrontendModelBase} S
268
+ * @template {FrontendModelBase} T
269
+ * @template {Record<string, FrontendModelAttributeValue>} [TargetCreateAttributes=Record<string, FrontendModelAttributeValue>]
243
270
  */
244
271
  export class FrontendModelSingularRelationship {
245
272
  /**
246
273
  * Runs constructor.
247
- * @param {InstanceType<S>} model - Parent model.
274
+ * @param {S} model - Parent model.
248
275
  * @param {string} relationshipName - Relationship name.
249
- * @param {T | null} targetModelClass - Target model class.
276
+ * @param {FrontendModelClass<T, Record<string, FrontendModelAttributeValue>, TargetCreateAttributes> | null} targetModelClass - Target model class.
250
277
  */
251
278
  constructor(model, relationshipName, targetModelClass) {
252
279
  this.model = model
253
280
  this.relationshipName = relationshipName
254
281
  this.targetModelClass = targetModelClass
255
282
  this._preloaded = false
283
+ /** @type {T | null} */
256
284
  this._loadedValue = null
257
285
  }
258
286
 
259
287
  /**
260
288
  * Runs set loaded.
261
- * @param {?} loadedValue - Loaded relationship value.
289
+ * @param {T | null | undefined} loadedValue - Loaded relationship value.
262
290
  * @returns {void}
263
291
  */
264
292
  setLoaded(loadedValue) {
@@ -276,7 +304,7 @@ export class FrontendModelSingularRelationship {
276
304
 
277
305
  /**
278
306
  * Runs loaded.
279
- * @returns {?} - Loaded relationship value.
307
+ * @returns {T | null} - Loaded relationship value.
280
308
  */
281
309
  loaded() {
282
310
  if (!this._preloaded) {
@@ -286,42 +314,91 @@ export class FrontendModelSingularRelationship {
286
314
  return this._loadedValue
287
315
  }
288
316
 
317
+ /**
318
+ * Copies loaded value from another singular relationship helper.
319
+ * @param {FrontendModelRelationship} sourceRelationship - Source relationship helper.
320
+ * @returns {void}
321
+ */
322
+ copyLoadedFrom(sourceRelationship) {
323
+ if (sourceRelationship instanceof FrontendModelHasManyRelationship) {
324
+ throw new Error(`Expected ${this.model.constructor.name}#${this.relationshipName} source relationship to be singular`)
325
+ }
326
+
327
+ // Narrows the runtime value to the target relationship's documented model type.
328
+ const loadedValue = /** @type {T | null} */ (sourceRelationship.loaded())
329
+
330
+ this.setLoaded(loadedValue)
331
+ }
332
+
289
333
  /**
290
334
  * Runs build.
291
- * @param {Record<string, ?>} [attributes] - New model attributes.
292
- * @returns {InstanceType<T>} - Built model.
335
+ * @param {TargetCreateAttributes} [attributes] - New model attributes.
336
+ * @returns {T} - Built model.
293
337
  */
294
- build(attributes = {}) {
338
+ build(attributes = /** @type {TargetCreateAttributes} */ ({})) {
295
339
  if (!this.targetModelClass) {
296
340
  throw new Error(`No target model class configured for ${this.model.constructor.name}#${this.relationshipName}`)
297
341
  }
298
342
 
299
- const model = /**
300
- * Narrows the runtime value to the documented type.
301
- @type {InstanceType<T>} */ (new this.targetModelClass(attributes))
343
+ // Narrows the runtime value to the documented relationship model type.
344
+ const model = /** @type {T} */ (new this.targetModelClass(attributes))
302
345
 
303
346
  this.setLoaded(model)
304
347
 
305
348
  return model
306
349
  }
350
+
351
+ /**
352
+ * Force-reload the relationship.
353
+ * @returns {Promise<T | null>} - Loaded relationship model.
354
+ */
355
+ async load() {
356
+ this._preloaded = false
357
+ this._loadedValue = null
358
+
359
+ const batched = await this.model._tryCohortPreload(this.relationshipName)
360
+
361
+ if (batched) return this.loaded()
362
+
363
+ await this.model.loadRelationship(this.relationshipName)
364
+
365
+ return this.loaded()
366
+ }
367
+
368
+ /**
369
+ * Returns the loaded relationship or loads it.
370
+ * @returns {Promise<T | null>} - Loaded relationship model.
371
+ */
372
+ async orLoad() {
373
+ if (this.getPreloaded()) return this.loaded()
374
+
375
+ const batched = await this.model._tryCohortPreload(this.relationshipName)
376
+
377
+ if (batched) return this.loaded()
378
+
379
+ await this.model.loadRelationship(this.relationshipName)
380
+
381
+ return this.loaded()
382
+ }
307
383
  }
308
384
 
309
385
  /**
310
386
  * Lightweight has-many relationship state holder for frontend model instances.
311
- * @template {FrontendModelClass} S
312
- * @template {FrontendModelClass} T
387
+ * @template {FrontendModelBase} S
388
+ * @template {FrontendModelBase} T
389
+ * @template {Record<string, FrontendModelAttributeValue>} [TargetCreateAttributes=Record<string, FrontendModelAttributeValue>]
313
390
  */
314
391
  export class FrontendModelHasManyRelationship {
315
392
  /**
316
393
  * Narrows the runtime value to the documented type.
317
- @type {Array<InstanceType<T>>} */
394
+ @type {Array<T>} */
318
395
  _loadedValue
319
396
 
320
397
  /**
321
398
  * Runs constructor.
322
- * @param {InstanceType<S>} model - Parent model.
399
+ * @param {S} model - Parent model.
323
400
  * @param {string} relationshipName - Relationship name.
324
- * @param {T | null} targetModelClass - Target model class.
401
+ * @param {FrontendModelClass<T, Record<string, FrontendModelAttributeValue>, TargetCreateAttributes> | null} targetModelClass - Target model class.
325
402
  */
326
403
  constructor(model, relationshipName, targetModelClass) {
327
404
  this.model = model
@@ -333,11 +410,15 @@ export class FrontendModelHasManyRelationship {
333
410
 
334
411
  /**
335
412
  * Runs set loaded.
336
- * @param {Array<InstanceType<T>>} loadedValue - Loaded relationship value.
413
+ * @param {Array<T>} loadedValue - Loaded relationship value.
337
414
  * @returns {void}
338
415
  */
339
416
  setLoaded(loadedValue) {
340
- this._loadedValue = Array.isArray(loadedValue) ? loadedValue : []
417
+ if (!Array.isArray(loadedValue)) {
418
+ throw new Error(`Expected ${this.model.constructor.name}#${this.relationshipName} to be loaded with an array`)
419
+ }
420
+
421
+ this._loadedValue = loadedValue
341
422
  this._preloaded = true
342
423
  }
343
424
 
@@ -351,7 +432,7 @@ export class FrontendModelHasManyRelationship {
351
432
 
352
433
  /**
353
434
  * Runs loaded.
354
- * @returns {Array<InstanceType<T>>} - Loaded relationship values.
435
+ * @returns {Array<T>} - Loaded relationship values.
355
436
  */
356
437
  loaded() {
357
438
  if (!this._preloaded) {
@@ -361,9 +442,25 @@ export class FrontendModelHasManyRelationship {
361
442
  return this._loadedValue
362
443
  }
363
444
 
445
+ /**
446
+ * Copies loaded value from another has-many relationship helper.
447
+ * @param {FrontendModelRelationship} sourceRelationship - Source relationship helper.
448
+ * @returns {void}
449
+ */
450
+ copyLoadedFrom(sourceRelationship) {
451
+ if (!(sourceRelationship instanceof FrontendModelHasManyRelationship)) {
452
+ throw new Error(`Expected ${this.model.constructor.name}#${this.relationshipName} source relationship to be has-many`)
453
+ }
454
+
455
+ // Narrows the runtime value to the target relationship's documented model type.
456
+ const loadedValue = /** @type {Array<T>} */ (sourceRelationship.loaded())
457
+
458
+ this.setLoaded(loadedValue)
459
+ }
460
+
364
461
  /**
365
462
  * Runs add to loaded.
366
- * @param {Array<InstanceType<T>>} models - Models to append.
463
+ * @param {Array<T>} models - Models to append.
367
464
  * @returns {void}
368
465
  */
369
466
  addToLoaded(models) {
@@ -374,17 +471,16 @@ export class FrontendModelHasManyRelationship {
374
471
 
375
472
  /**
376
473
  * Runs build.
377
- * @param {Record<string, ?>} [attributes] - New model attributes.
378
- * @returns {InstanceType<T>} - Built model.
474
+ * @param {TargetCreateAttributes} [attributes] - New model attributes.
475
+ * @returns {T} - Built model.
379
476
  */
380
- build(attributes = {}) {
477
+ build(attributes = /** @type {TargetCreateAttributes} */ ({})) {
381
478
  if (!this.targetModelClass) {
382
479
  throw new Error(`No target model class configured for ${this.model.constructor.name}#${this.relationshipName}`)
383
480
  }
384
481
 
385
- const model = /**
386
- * Narrows the runtime value to the documented type.
387
- @type {InstanceType<T>} */ (new this.targetModelClass(attributes))
482
+ // Narrows the runtime value to the documented relationship model type.
483
+ const model = /** @type {T} */ (new this.targetModelClass(attributes))
388
484
 
389
485
  this.addToLoaded([model])
390
486
 
@@ -397,25 +493,25 @@ export class FrontendModelHasManyRelationship {
397
493
  * batched into one request via the cohort preloader. The scoped query path
398
494
  * (`Model.where(...).preload([name]).toArray()` directly from user code)
399
495
  * bypasses cohort batching by design.
400
- * @returns {Promise<Array<InstanceType<T>>>} - Loaded relationship models.
496
+ * @returns {Promise<Array<T>>} - Loaded relationship models.
401
497
  */
402
498
  async load() {
403
499
  // Reset so the cohort preloader (or single-record fallback) repopulates.
404
500
  this._preloaded = false
405
501
  this._loadedValue = []
406
502
 
407
- const batched = await /**
408
- * Narrows the runtime value to the documented type.
409
- @type {?} */ (this.model)._tryCohortPreload(this.relationshipName)
503
+ const batched = await this.model._tryCohortPreload(this.relationshipName)
410
504
 
411
505
  if (batched) return this._loadedValue
412
506
 
413
- return /** Narrows the runtime value to the documented type. @type {Promise<Array<InstanceType<T>>>} */ (this.model.loadRelationship(this.relationshipName))
507
+ await this.model.loadRelationship(this.relationshipName)
508
+
509
+ return this.loaded()
414
510
  }
415
511
 
416
512
  /**
417
513
  * Runs to array.
418
- * @returns {Promise<Array<InstanceType<T>>>} - Loaded relationship models.
514
+ * @returns {Promise<Array<T>>} - Loaded relationship models.
419
515
  */
420
516
  async toArray() {
421
517
  if (this.getPreloaded() || this._loadedValue.length > 0) {
@@ -426,6 +522,22 @@ export class FrontendModelHasManyRelationship {
426
522
  }
427
523
  }
428
524
 
525
+ /**
526
+ * Frontend model relationship helper type.
527
+ * @typedef {FrontendModelHasManyRelationship<FrontendModelBase, FrontendModelBase, Record<string, FrontendModelAttributeValue>> | FrontendModelSingularRelationship<FrontendModelBase, FrontendModelBase, Record<string, FrontendModelAttributeValue>>} FrontendModelRelationship
528
+ */
529
+
530
+ /**
531
+ * Copies loaded relationship state between helpers of the same relationship shape.
532
+ * @param {object} args - Arguments.
533
+ * @param {FrontendModelRelationship} args.sourceRelationship - Source relationship helper.
534
+ * @param {FrontendModelRelationship} args.targetRelationship - Target relationship helper.
535
+ * @returns {void}
536
+ */
537
+ function copyLoadedRelationshipValue({sourceRelationship, targetRelationship}) {
538
+ targetRelationship.copyLoadedFrom(sourceRelationship)
539
+ }
540
+
429
541
  /**
430
542
  * Runs relationship type is collection.
431
543
  * @param {string} relationshipType - Relationship type.
@@ -1694,7 +1806,12 @@ function assertDefinedFindByConditionValue(value, keyPath) {
1694
1806
  }
1695
1807
  }
1696
1808
 
1697
- /** Base class for generated frontend model classes. */
1809
+ /**
1810
+ * Base frontend model.
1811
+ * @template {Record<string, FrontendModelAttributeValue>} [Attributes=Record<string, FrontendModelAttributeValue>]
1812
+ * @template {Record<string, FrontendModelAttributeValue>} [CreateAttributes=Record<string, FrontendModelAttributeValue>]
1813
+ * @template {Record<string, FrontendModelAttributeValue>} [UpdateAttributes=Record<string, FrontendModelAttributeValue>]
1814
+ */
1698
1815
  export default class FrontendModelBase {
1699
1816
  /**
1700
1817
  * Narrows the runtime value to the documented type.
@@ -1722,11 +1839,11 @@ export default class FrontendModelBase {
1722
1839
 
1723
1840
  /**
1724
1841
  * Narrows the runtime value to the documented type.
1725
- @type {Record<string, ?>} */
1842
+ @type {Record<string, FrontendModelAttributeValue>} */
1726
1843
  _attributes
1727
1844
  /**
1728
1845
  * Narrows the runtime value to the documented type.
1729
- @type {Record<string, FrontendModelHasManyRelationship<FrontendModelClass, FrontendModelClass> | FrontendModelSingularRelationship<FrontendModelClass, FrontendModelClass>>} */
1846
+ @type {Record<string, FrontendModelHasManyRelationship<FrontendModelBase, FrontendModelBase, Record<string, FrontendModelAttributeValue>> | FrontendModelSingularRelationship<FrontendModelBase, FrontendModelBase, Record<string, FrontendModelAttributeValue>>>} */
1730
1847
  _relationships
1731
1848
  /**
1732
1849
  * Narrows the runtime value to the documented type.
@@ -1751,7 +1868,7 @@ export default class FrontendModelBase {
1751
1868
  _markedForDestruction
1752
1869
  /**
1753
1870
  * Narrows the runtime value to the documented type.
1754
- @type {Record<string, ?>} */
1871
+ @type {Record<string, FrontendModelAttributeValue>} */
1755
1872
  _persistedAttributes
1756
1873
  /**
1757
1874
  * Narrows the runtime value to the documented type.
@@ -1761,9 +1878,9 @@ export default class FrontendModelBase {
1761
1878
 
1762
1879
  /**
1763
1880
  * Runs constructor.
1764
- * @param {Record<string, ?>} [attributes] - Initial attributes.
1881
+ * @param {Attributes | CreateAttributes} [attributes] - Initial attributes.
1765
1882
  */
1766
- constructor(attributes = {}) {
1883
+ constructor(attributes) {
1767
1884
  const ModelClass = frontendModelClassFor(this)
1768
1885
 
1769
1886
  ModelClass.ensureGeneratedAttachmentMethods()
@@ -1775,7 +1892,7 @@ export default class FrontendModelBase {
1775
1892
  this._isNewRecord = true
1776
1893
  this._markedForDestruction = false
1777
1894
  this._persistedAttributes = {}
1778
- this.assignAttributes(attributes)
1895
+ if (attributes) this.assignAttributes(attributes)
1779
1896
  }
1780
1897
 
1781
1898
  /**
@@ -1924,10 +2041,10 @@ export default class FrontendModelBase {
1924
2041
 
1925
2042
  /**
1926
2043
  * Runs attributes.
1927
- * @returns {Record<string, ?>} - Attributes hash.
2044
+ * @returns {Attributes} - Attributes hash.
1928
2045
  */
1929
2046
  attributes() {
1930
- return this._attributes
2047
+ return /** @type {Attributes} */ (this._attributes)
1931
2048
  }
1932
2049
 
1933
2050
  /**
@@ -2010,7 +2127,7 @@ export default class FrontendModelBase {
2010
2127
  /**
2011
2128
  * Runs get relationship by name.
2012
2129
  * @param {string} relationshipName - Relationship name.
2013
- * @returns {FrontendModelHasManyRelationship<FrontendModelClass, FrontendModelClass> | FrontendModelSingularRelationship<FrontendModelClass, FrontendModelClass>} - Relationship state object.
2130
+ * @returns {FrontendModelRelationship} - Relationship state object.
2014
2131
  */
2015
2132
  getRelationshipByName(relationshipName) {
2016
2133
  if (!this._relationships[relationshipName]) {
@@ -2054,7 +2171,7 @@ export default class FrontendModelBase {
2054
2171
  /**
2055
2172
  * Runs load relationship.
2056
2173
  * @param {string} relationshipName - Relationship name.
2057
- * @returns {Promise<?>} - Loaded relationship value.
2174
+ * @returns {Promise<FrontendModelBase | Array<FrontendModelBase> | null>} - Loaded relationship value.
2058
2175
  */
2059
2176
  async loadRelationship(relationshipName) {
2060
2177
  const ModelClass = frontendModelClassFor(this)
@@ -2062,11 +2179,12 @@ export default class FrontendModelBase {
2062
2179
  const reloadedModel = await ModelClass
2063
2180
  .preload([relationshipName])
2064
2181
  .find(id)
2065
- const loadedValue = reloadedModel.getRelationshipByName(relationshipName).loaded()
2182
+ const sourceRelationship = reloadedModel.getRelationshipByName(relationshipName)
2183
+ const targetRelationship = this.getRelationshipByName(relationshipName)
2066
2184
 
2067
- this.getRelationshipByName(relationshipName).setLoaded(loadedValue)
2185
+ copyLoadedRelationshipValue({sourceRelationship, targetRelationship})
2068
2186
 
2069
- return loadedValue
2187
+ return targetRelationship.loaded()
2070
2188
  }
2071
2189
 
2072
2190
  /**
@@ -2087,7 +2205,7 @@ export default class FrontendModelBase {
2087
2205
  /**
2088
2206
  * Runs relationship or load.
2089
2207
  * @param {string} relationshipName - Relationship name.
2090
- * @returns {Promise<?>} - Loaded relationship value.
2208
+ * @returns {Promise<FrontendModelBase | Array<FrontendModelBase> | null>} - Loaded relationship value.
2091
2209
  */
2092
2210
  async relationshipOrLoad(relationshipName) {
2093
2211
  const relationship = this.getRelationshipByName(relationshipName)
@@ -2171,9 +2289,10 @@ export default class FrontendModelBase {
2171
2289
 
2172
2290
  if (!reloaded) continue
2173
2291
 
2174
- const reloadedValue = reloaded.getRelationshipByName(relationshipName).loaded()
2175
-
2176
- sibling.getRelationshipByName(relationshipName).setLoaded(reloadedValue)
2292
+ copyLoadedRelationshipValue({
2293
+ sourceRelationship: reloaded.getRelationshipByName(relationshipName),
2294
+ targetRelationship: sibling.getRelationshipByName(relationshipName)
2295
+ })
2177
2296
  }
2178
2297
 
2179
2298
  // If the caller itself was not populated (record deleted/filtered between
@@ -2188,8 +2307,8 @@ export default class FrontendModelBase {
2188
2307
  /**
2189
2308
  * Runs set relationship.
2190
2309
  * @param {string} relationshipName - Relationship name.
2191
- * @param {?} relationshipValue - Relationship value.
2192
- * @returns {?} - Assigned relationship value.
2310
+ * @param {FrontendModelBase | null | undefined} relationshipValue - Relationship value.
2311
+ * @returns {FrontendModelBase | null | undefined} - Assigned relationship value.
2193
2312
  */
2194
2313
  setRelationship(relationshipName, relationshipValue) {
2195
2314
  const ModelClass = frontendModelClassFor(this)
@@ -2199,23 +2318,27 @@ export default class FrontendModelBase {
2199
2318
  throw new Error(`Unknown relationship: ${ModelClass.name}#${relationshipName}`)
2200
2319
  }
2201
2320
 
2202
- if (relationshipTypeIsCollection(relationshipDefinition.type)) {
2321
+ const relationship = this.getRelationshipByName(relationshipName)
2322
+
2323
+ if (relationship instanceof FrontendModelHasManyRelationship) {
2203
2324
  throw new Error(`Cannot set has-many relationship with setRelationship(): ${ModelClass.name}#${relationshipName}`)
2204
2325
  }
2205
2326
 
2206
- this.getRelationshipByName(relationshipName).setLoaded(relationshipValue)
2327
+ relationship.setLoaded(relationshipValue)
2207
2328
 
2208
2329
  return relationshipValue
2209
2330
  }
2210
2331
 
2211
2332
  /**
2212
2333
  * Runs assign attributes.
2213
- * @param {Record<string, ?>} attributes - Attributes to assign.
2334
+ * @param {Attributes | CreateAttributes | UpdateAttributes | Record<string, FrontendModelAttributeValue>} attributes - Attributes to assign.
2214
2335
  * @returns {void} - No return value.
2215
2336
  */
2216
2337
  assignAttributes(attributes) {
2217
- for (const key in attributes) {
2218
- this.setAttribute(key, attributes[key])
2338
+ const attributeValues = /** @type {Record<string, FrontendModelAttributeValue>} */ (attributes)
2339
+
2340
+ for (const key in attributeValues) {
2341
+ this.setAttribute(key, attributeValues[key])
2219
2342
  }
2220
2343
  }
2221
2344
 
@@ -2812,7 +2935,7 @@ export default class FrontendModelBase {
2812
2935
  * Runs attributes from response.
2813
2936
  * @this {FrontendModelClass}
2814
2937
  * @param {object} response - Response payload.
2815
- * @returns {Record<string, ?>} - Attributes from payload.
2938
+ * @returns {Record<string, FrontendModelAttributeValue>} - Attributes from payload.
2816
2939
  */
2817
2940
  static attributesFromResponse(response) {
2818
2941
  const modelData = this.modelDataFromResponse(response)
@@ -2824,39 +2947,34 @@ export default class FrontendModelBase {
2824
2947
  * Runs model data from response.
2825
2948
  * @this {FrontendModelClass}
2826
2949
  * @param {object} response - Response payload.
2827
- * @returns {{abilities: Record<string, boolean>, attributes: Record<string, ?>, associationCounts: Record<string, number>, queryData: Record<string, ?>, preloadedRelationships: Record<string, ?>, selectedAttributes: Set<string>}} - Attributes, preloaded relationships, association counts, queryData, abilities, and the selected-attributes set.
2950
+ * @returns {{abilities: Record<string, boolean>, attributes: Record<string, FrontendModelAttributeValue>, associationCounts: Record<string, number>, queryData: Record<string, FrontendModelAttributeValue>, preloadedRelationships: Record<string, FrontendModelAttributeValue>, selectedAttributes: Set<string>}} - Attributes, preloaded relationships, association counts, queryData, abilities, and the selected-attributes set.
2828
2951
  */
2829
2952
  static modelDataFromResponse(response) {
2830
2953
  if (!response || typeof response !== "object") {
2831
2954
  throw new Error(`Expected object response but got: ${response}`)
2832
2955
  }
2833
2956
 
2834
- const responseObject = /**
2835
- * Narrows the runtime value to the documented type.
2836
- @type {Record<string, ?>} */ (response)
2957
+ // Narrows the response object to the frontend-model transport value map.
2958
+ const responseObject = /** @type {Record<string, FrontendModelAttributeValue>} */ (response)
2837
2959
 
2838
2960
  /**
2839
2961
  * Defines modelData.
2840
- @type {Record<string, ?>} */
2962
+ @type {Record<string, FrontendModelAttributeValue>} */
2841
2963
  let modelData
2842
2964
 
2843
2965
  if (responseObject.model && typeof responseObject.model === "object") {
2844
- modelData = /**
2845
- * Narrows the runtime value to the documented type.
2846
- @type {Record<string, ?>} */ (responseObject.model)
2966
+ // Narrows the nested model payload to the frontend-model value map.
2967
+ modelData = /** @type {Record<string, FrontendModelAttributeValue>} */ (responseObject.model)
2847
2968
  } else if (responseObject.attributes && typeof responseObject.attributes === "object") {
2848
- modelData = /**
2849
- * Narrows the runtime value to the documented type.
2850
- @type {Record<string, ?>} */ (responseObject.attributes)
2969
+ // Narrows the nested attributes payload to the frontend-model value map.
2970
+ modelData = /** @type {Record<string, FrontendModelAttributeValue>} */ (responseObject.attributes)
2851
2971
  } else {
2852
2972
  modelData = responseObject
2853
2973
  }
2854
2974
 
2855
- const attributes = {...modelData}
2975
+ const attributes = /** @type {Record<string, FrontendModelAttributeValue>} */ ({...modelData})
2856
2976
  const preloadedRelationships = isPlainObject(attributes[PRELOADED_RELATIONSHIPS_KEY])
2857
- ? /**
2858
- * Narrows the runtime value to the documented type.
2859
- @type {Record<string, ?>} */ (attributes[PRELOADED_RELATIONSHIPS_KEY])
2977
+ ? /** @type {Record<string, FrontendModelAttributeValue>} */ (attributes[PRELOADED_RELATIONSHIPS_KEY])
2860
2978
  : {}
2861
2979
  const associationCounts = isPlainObject(attributes[ASSOCIATION_COUNTS_KEY])
2862
2980
  ? /**
@@ -2864,9 +2982,7 @@ export default class FrontendModelBase {
2864
2982
  @type {Record<string, number>} */ (attributes[ASSOCIATION_COUNTS_KEY])
2865
2983
  : {}
2866
2984
  const queryData = isPlainObject(attributes[QUERY_DATA_KEY])
2867
- ? /**
2868
- * Narrows the runtime value to the documented type.
2869
- @type {Record<string, ?>} */ (attributes[QUERY_DATA_KEY])
2985
+ ? /** @type {Record<string, FrontendModelAttributeValue>} */ (attributes[QUERY_DATA_KEY])
2870
2986
  : {}
2871
2987
  const abilities = isPlainObject(attributes[ABILITIES_KEY])
2872
2988
  ? /**
@@ -2902,12 +3018,39 @@ export default class FrontendModelBase {
2902
3018
  const relationship = model.getRelationshipByName(relationshipName)
2903
3019
  const targetModelClass = this.relationshipModelClass(relationshipName)
2904
3020
 
2905
- if (Array.isArray(relationshipPayload)) {
2906
- relationship.setLoaded(relationshipPayload.map((entry) => this.instantiateRelationshipValue(entry, targetModelClass)))
3021
+ if (relationship instanceof FrontendModelHasManyRelationship) {
3022
+ if (!Array.isArray(relationshipPayload)) {
3023
+ throw new Error(`Expected ${this.name}#${relationshipName} payload to be an array`)
3024
+ }
3025
+
3026
+ /** @type {Array<FrontendModelBase>} */
3027
+ const relatedModels = []
3028
+
3029
+ for (const entry of relationshipPayload) {
3030
+ const relatedModel = this.instantiateRelationshipValue(entry, targetModelClass)
3031
+
3032
+ if (!(relatedModel instanceof FrontendModelBase)) {
3033
+ throw new Error(`Expected ${this.name}#${relationshipName} payload entry to instantiate a frontend model`)
3034
+ }
3035
+
3036
+ relatedModels.push(relatedModel)
3037
+ }
3038
+
3039
+ relationship.setLoaded(relatedModels)
2907
3040
  continue
2908
3041
  }
2909
3042
 
2910
- relationship.setLoaded(this.instantiateRelationshipValue(relationshipPayload, targetModelClass))
3043
+ if (Array.isArray(relationshipPayload)) {
3044
+ throw new Error(`Expected ${this.name}#${relationshipName} payload to be singular`)
3045
+ }
3046
+
3047
+ const relatedModel = this.instantiateRelationshipValue(relationshipPayload, targetModelClass)
3048
+
3049
+ if (relatedModel != undefined && !(relatedModel instanceof FrontendModelBase)) {
3050
+ throw new Error(`Expected ${this.name}#${relationshipName} payload to instantiate a frontend model`)
3051
+ }
3052
+
3053
+ relationship.setLoaded(relatedModel)
2911
3054
  }
2912
3055
  }
2913
3056
 
@@ -3422,15 +3565,16 @@ export default class FrontendModelBase {
3422
3565
 
3423
3566
  /**
3424
3567
  * Runs create.
3425
- * @template {FrontendModelClass} T
3426
- * @this {T}
3427
- * @param {Record<string, ?>} [attributes] - Initial attributes.
3428
- * @returns {Promise<InstanceType<T>>} - Persisted model.
3568
+ * @template {FrontendModelBase} Model
3569
+ * @template {Record<string, FrontendModelAttributeValue>} ModelAttributes
3570
+ * @template {Record<string, FrontendModelAttributeValue>} ModelCreateAttributes
3571
+ * @this {FrontendModelClass<Model, ModelAttributes, ModelCreateAttributes>}
3572
+ * @param {ModelCreateAttributes} [attributes] - Initial attributes.
3573
+ * @returns {Promise<Model>} - Persisted model.
3429
3574
  */
3430
- static async create(attributes = {}) {
3431
- const model = /**
3432
- * Narrows the runtime value to the documented type.
3433
- @type {InstanceType<T>} */ (new this(attributes))
3575
+ static async create(attributes) {
3576
+ // Narrows the constructed instance to the receiver's documented model type.
3577
+ const model = /** @type {Model} */ (new this(attributes))
3434
3578
 
3435
3579
  await model.save()
3436
3580
 
@@ -3601,13 +3745,13 @@ export default class FrontendModelBase {
3601
3745
 
3602
3746
  /**
3603
3747
  * Runs update.
3604
- * @param {Record<string, ?>} [newAttributes] - New values to assign before update.
3748
+ * @param {UpdateAttributes} [newAttributes] - New values to assign before update.
3605
3749
  * @returns {Promise<this>} - Updated model.
3606
3750
  */
3607
- async update(newAttributes = {}) {
3608
- this.assignAttributes(newAttributes)
3751
+ async update(newAttributes) {
3752
+ if (newAttributes) this.assignAttributes(newAttributes)
3609
3753
 
3610
- return await this.save()
3754
+ return /** @type {this} */ (await this.save())
3611
3755
  }
3612
3756
 
3613
3757
  /**
@@ -3693,12 +3837,12 @@ export default class FrontendModelBase {
3693
3837
  * fields the caller actually changed — avoiding strict permit rejections on
3694
3838
  * framework-managed fields like `id`, `createdAt`, `updatedAt`, or owner
3695
3839
  * foreign keys that the resource never lists in `permittedParams`.
3696
- * @returns {Record<string, ?>} - Changed attributes hash.
3840
+ * @returns {Record<string, FrontendModelAttributeValue>} - Changed attributes hash.
3697
3841
  */
3698
3842
  _changedAttributesForSave() {
3699
3843
  /**
3700
3844
  * Changed attributes.
3701
- @type {Record<string, ?>} */
3845
+ @type {Record<string, FrontendModelAttributeValue>} */
3702
3846
  const changedAttributes = {}
3703
3847
 
3704
3848
  for (const [attributeName, [previousValue, currentValue]] of Object.entries(this.changes())) {
@@ -75,9 +75,10 @@ export default class FrontendModelPreloader {
75
75
  if (!reloadedModel) continue
76
76
 
77
77
  for (const relationshipName of topLevelRelationships) {
78
- const value = reloadedModel.getRelationshipByName(relationshipName).loaded()
78
+ const sourceRelationship = reloadedModel.getRelationshipByName(relationshipName)
79
+ const targetRelationship = model.getRelationshipByName(relationshipName)
79
80
 
80
- model.getRelationshipByName(relationshipName).setLoaded(value)
81
+ targetRelationship.copyLoadedFrom(sourceRelationship)
81
82
  }
82
83
  }
83
84
  }