velocious 1.0.445 → 1.0.446
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.
- package/build/database/record/index.js +37 -37
- package/build/environment-handlers/node/cli/commands/generate/base-models.js +67 -1
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +169 -0
- package/build/frontend-model-controller.js +44 -12
- package/build/frontend-model-resource/base-resource.js +519 -129
- package/build/frontend-models/base.js +324 -118
- package/build/frontend-models/websocket-channel.js +39 -3
- package/build/src/database/record/index.d.ts +37 -37
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +38 -38
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts +13 -0
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +59 -2
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +74 -0
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +155 -1
- package/build/src/frontend-model-controller.d.ts +2 -1
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +38 -14
- package/build/src/frontend-model-resource/base-resource.d.ts +196 -21
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +467 -112
- package/build/src/frontend-models/base.d.ts +77 -4
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +278 -116
- package/build/src/frontend-models/websocket-channel.d.ts +8 -0
- package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-channel.js +35 -4
- package/package.json +1 -1
- package/src/database/record/index.js +37 -37
- package/src/environment-handlers/node/cli/commands/generate/base-models.js +67 -1
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +169 -0
- package/src/frontend-model-controller.js +44 -12
- package/src/frontend-model-resource/base-resource.js +519 -129
- package/src/frontend-models/base.js +324 -118
- package/src/frontend-models/websocket-channel.js +39 -3
|
@@ -24,6 +24,10 @@ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQuer
|
|
|
24
24
|
* Defines this typedef.
|
|
25
25
|
* @typedef {{type: "hasOne" | "hasMany"}} FrontendModelAttachmentDefinition
|
|
26
26
|
*/
|
|
27
|
+
/**
|
|
28
|
+
* Attachment input accepted by frontend-model attachment helpers before normalization.
|
|
29
|
+
* @typedef {Record<string, ?> | {arrayBuffer: () => Promise<ArrayBuffer>, type?: string, name?: string} | null | undefined} FrontendModelAttachmentInput
|
|
30
|
+
*/
|
|
27
31
|
/**
|
|
28
32
|
* Defines this typedef.
|
|
29
33
|
* @typedef {{attributes?: string[], builtInCollectionCommands?: string[], builtInMemberCommands?: string[], collectionCommands?: string[], commands?: string[], memberCommands?: string[], attachments?: Record<string, FrontendModelAttachmentDefinition>, modelName?: string, nestedAttributes?: Record<string, {allowDestroy?: boolean, limit?: number}>, primaryKey?: string, relationships?: string[]}} FrontendModelResourceConfig
|
|
@@ -614,6 +618,17 @@ function frontendModelPayloadContainsAttachmentUpload(value) {
|
|
|
614
618
|
return Object.values(value).some((entry) => frontendModelPayloadContainsAttachmentUpload(entry))
|
|
615
619
|
}
|
|
616
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Returns the concrete frontend-model class for an instance.
|
|
623
|
+
* @param {FrontendModelBase} model - Frontend model instance.
|
|
624
|
+
* @returns {typeof FrontendModelBase} Concrete frontend-model class.
|
|
625
|
+
*/
|
|
626
|
+
function frontendModelClassFor(model) {
|
|
627
|
+
const constructorValue = model.constructor
|
|
628
|
+
|
|
629
|
+
return /** @type {typeof FrontendModelBase} */ (constructorValue)
|
|
630
|
+
}
|
|
631
|
+
|
|
617
632
|
/**
|
|
618
633
|
* Runs normalize frontend attachment input.
|
|
619
634
|
* @param {?} input - Attachment input.
|
|
@@ -691,6 +706,12 @@ async function normalizeFrontendAttachmentInput(input) {
|
|
|
691
706
|
* Frontend-model attachment helper for one attachment name.
|
|
692
707
|
*/
|
|
693
708
|
export class FrontendModelAttachmentHandle {
|
|
709
|
+
/**
|
|
710
|
+
* Pending attachment inputs queued for the next model save.
|
|
711
|
+
* @type {FrontendModelAttachmentInput[]}
|
|
712
|
+
*/
|
|
713
|
+
pendingInputs = []
|
|
714
|
+
|
|
694
715
|
/**
|
|
695
716
|
* Runs constructor.
|
|
696
717
|
* @param {object} args - Options.
|
|
@@ -702,15 +723,70 @@ export class FrontendModelAttachmentHandle {
|
|
|
702
723
|
this.attachmentName = attachmentName
|
|
703
724
|
}
|
|
704
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Queue attachment input for the parent model's next save.
|
|
728
|
+
* @param {FrontendModelAttachmentInput | FrontendModelAttachmentInput[]} input - Attachment input.
|
|
729
|
+
* @returns {void}
|
|
730
|
+
*/
|
|
731
|
+
queueAttach(input) {
|
|
732
|
+
const ModelClass = frontendModelClassFor(this.model)
|
|
733
|
+
const attachmentDefinition = ModelClass.attachmentDefinition(this.attachmentName)
|
|
734
|
+
|
|
735
|
+
if (attachmentDefinition?.type === "hasOne") {
|
|
736
|
+
if (Array.isArray(input)) {
|
|
737
|
+
const lastInput = input[input.length - 1]
|
|
738
|
+
|
|
739
|
+
this.pendingInputs = typeof lastInput === "undefined" ? [] : [lastInput]
|
|
740
|
+
} else {
|
|
741
|
+
this.pendingInputs = [input]
|
|
742
|
+
}
|
|
743
|
+
return
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (Array.isArray(input)) {
|
|
747
|
+
this.pendingInputs.push(...input)
|
|
748
|
+
} else {
|
|
749
|
+
this.pendingInputs.push(input)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Whether this attachment has queued inputs for the next model save.
|
|
755
|
+
* @returns {boolean} Whether any pending inputs exist.
|
|
756
|
+
*/
|
|
757
|
+
hasPendingAttachments() {
|
|
758
|
+
return this.pendingInputs.length > 0
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Builds the save payload for queued attachment inputs.
|
|
763
|
+
* @returns {Promise<Record<string, ?> | Record<string, ?>[] | undefined>} Normalized attachment payload.
|
|
764
|
+
*/
|
|
765
|
+
async pendingAttachmentsPayload() {
|
|
766
|
+
if (this.pendingInputs.length === 0) return undefined
|
|
767
|
+
|
|
768
|
+
const ModelClass = frontendModelClassFor(this.model)
|
|
769
|
+
const attachmentDefinition = ModelClass.attachmentDefinition(this.attachmentName)
|
|
770
|
+
|
|
771
|
+
if (attachmentDefinition?.type === "hasMany") {
|
|
772
|
+
return await Promise.all(this.pendingInputs.map(async (input) => await normalizeFrontendAttachmentInput(input)))
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return await normalizeFrontendAttachmentInput(this.pendingInputs[this.pendingInputs.length - 1])
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Clears queued attachment inputs after a successful model save. */
|
|
779
|
+
clearPendingAttachments() {
|
|
780
|
+
this.pendingInputs = []
|
|
781
|
+
}
|
|
782
|
+
|
|
705
783
|
/**
|
|
706
784
|
* Runs attach.
|
|
707
785
|
* @param {?} input - Attachment input.
|
|
708
786
|
* @returns {Promise<void>} - Resolves when attached.
|
|
709
787
|
*/
|
|
710
788
|
async attach(input) {
|
|
711
|
-
const ModelClass =
|
|
712
|
-
* Narrows the runtime value to the documented type.
|
|
713
|
-
@type {typeof FrontendModelBase} */ (this.model.constructor)
|
|
789
|
+
const ModelClass = frontendModelClassFor(this.model)
|
|
714
790
|
const normalizedInput = await normalizeFrontendAttachmentInput(input)
|
|
715
791
|
const response = await ModelClass.executeCommand("attach", {
|
|
716
792
|
attachment: normalizedInput,
|
|
@@ -727,9 +803,7 @@ export class FrontendModelAttachmentHandle {
|
|
|
727
803
|
* @returns {Promise<FrontendModelAttachmentDownload | null>} - Downloaded attachment payload.
|
|
728
804
|
*/
|
|
729
805
|
async download(attachmentId) {
|
|
730
|
-
const ModelClass =
|
|
731
|
-
* Narrows the runtime value to the documented type.
|
|
732
|
-
@type {typeof FrontendModelBase} */ (this.model.constructor)
|
|
806
|
+
const ModelClass = frontendModelClassFor(this.model)
|
|
733
807
|
const response = await ModelClass.executeCommand("download", frontendModelAttachmentCommandPayload(this, attachmentId))
|
|
734
808
|
const attachmentPayload = response.attachment
|
|
735
809
|
|
|
@@ -755,9 +829,7 @@ export class FrontendModelAttachmentHandle {
|
|
|
755
829
|
* @returns {Promise<string | null>} - Resolvable attachment URL.
|
|
756
830
|
*/
|
|
757
831
|
async url(attachmentId) {
|
|
758
|
-
const ModelClass =
|
|
759
|
-
* Narrows the runtime value to the documented type.
|
|
760
|
-
@type {typeof FrontendModelBase} */ (this.model.constructor)
|
|
832
|
+
const ModelClass = frontendModelClassFor(this.model)
|
|
761
833
|
const response = await ModelClass.executeCommand("url", frontendModelAttachmentCommandPayload(this, attachmentId))
|
|
762
834
|
|
|
763
835
|
if (typeof response.url === "string" && response.url.length > 0) {
|
|
@@ -772,9 +844,7 @@ export class FrontendModelAttachmentHandle {
|
|
|
772
844
|
* @returns {string} - Download URL for this attachment on the configured backend.
|
|
773
845
|
*/
|
|
774
846
|
downloadUrl() {
|
|
775
|
-
const ModelClass =
|
|
776
|
-
* Narrows the runtime value to the documented type.
|
|
777
|
-
@type {typeof FrontendModelBase} */ (this.model.constructor)
|
|
847
|
+
const ModelClass = frontendModelClassFor(this.model)
|
|
778
848
|
const commandName = ModelClass.commandName("download")
|
|
779
849
|
const resourcePath = ModelClass.resourcePath()
|
|
780
850
|
const commandUrl = frontendModelCommandUrl(resourcePath, commandName)
|
|
@@ -1654,6 +1724,11 @@ export default class FrontendModelBase {
|
|
|
1654
1724
|
* Narrows the runtime value to the documented type.
|
|
1655
1725
|
@type {Record<string, FrontendModelAttachmentHandle>} */
|
|
1656
1726
|
_attachments
|
|
1727
|
+
/**
|
|
1728
|
+
* Rails-style nested attribute payloads queued for the next save.
|
|
1729
|
+
* @type {Record<string, ?>}
|
|
1730
|
+
*/
|
|
1731
|
+
_pendingNestedAttributes
|
|
1657
1732
|
/**
|
|
1658
1733
|
* Narrows the runtime value to the documented type.
|
|
1659
1734
|
@type {Set<string> | null} */
|
|
@@ -1681,14 +1756,13 @@ export default class FrontendModelBase {
|
|
|
1681
1756
|
* @param {Record<string, ?>} [attributes] - Initial attributes.
|
|
1682
1757
|
*/
|
|
1683
1758
|
constructor(attributes = {}) {
|
|
1684
|
-
const ModelClass =
|
|
1685
|
-
* Narrows the runtime value to the documented type.
|
|
1686
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
1759
|
+
const ModelClass = frontendModelClassFor(this)
|
|
1687
1760
|
|
|
1688
1761
|
ModelClass.ensureGeneratedAttachmentMethods()
|
|
1689
1762
|
this._attributes = {}
|
|
1690
1763
|
this._relationships = {}
|
|
1691
1764
|
this._attachments = {}
|
|
1765
|
+
this._pendingNestedAttributes = {}
|
|
1692
1766
|
this._selectedAttributes = null
|
|
1693
1767
|
this._isNewRecord = true
|
|
1694
1768
|
this._markedForDestruction = false
|
|
@@ -1810,6 +1884,23 @@ export default class FrontendModelBase {
|
|
|
1810
1884
|
return definitions[relationshipName] || null
|
|
1811
1885
|
}
|
|
1812
1886
|
|
|
1887
|
+
/**
|
|
1888
|
+
* Resolves a Rails-style nested attributes key to a configured relationship.
|
|
1889
|
+
* @this {typeof FrontendModelBase}
|
|
1890
|
+
* @param {string} attributeName - Candidate attribute name, such as `tasksAttributes`.
|
|
1891
|
+
* @returns {string | null} Relationship name when nested attributes are configured.
|
|
1892
|
+
*/
|
|
1893
|
+
static nestedAttributesRelationshipName(attributeName) {
|
|
1894
|
+
if (!attributeName.endsWith("Attributes")) return null
|
|
1895
|
+
|
|
1896
|
+
const relationshipName = attributeName.slice(0, -"Attributes".length)
|
|
1897
|
+
const nestedAttributesConfig = this.resourceConfig().nestedAttributes || {}
|
|
1898
|
+
|
|
1899
|
+
return Object.prototype.hasOwnProperty.call(nestedAttributesConfig, relationshipName)
|
|
1900
|
+
? relationshipName
|
|
1901
|
+
: null
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1813
1904
|
/**
|
|
1814
1905
|
* Runs relationship model class.
|
|
1815
1906
|
* @this {typeof FrontendModelBase}
|
|
@@ -1915,9 +2006,7 @@ export default class FrontendModelBase {
|
|
|
1915
2006
|
*/
|
|
1916
2007
|
getRelationshipByName(relationshipName) {
|
|
1917
2008
|
if (!this._relationships[relationshipName]) {
|
|
1918
|
-
const ModelClass =
|
|
1919
|
-
* Narrows the runtime value to the documented type.
|
|
1920
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
2009
|
+
const ModelClass = frontendModelClassFor(this)
|
|
1921
2010
|
const relationshipDefinition = ModelClass.relationshipDefinition(relationshipName)
|
|
1922
2011
|
const targetModelClass = ModelClass.relationshipModelClass(relationshipName)
|
|
1923
2012
|
|
|
@@ -1937,9 +2026,7 @@ export default class FrontendModelBase {
|
|
|
1937
2026
|
* @returns {FrontendModelAttachmentHandle} - Attachment helper.
|
|
1938
2027
|
*/
|
|
1939
2028
|
getAttachmentByName(attachmentName) {
|
|
1940
|
-
const ModelClass =
|
|
1941
|
-
* Narrows the runtime value to the documented type.
|
|
1942
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
2029
|
+
const ModelClass = frontendModelClassFor(this)
|
|
1943
2030
|
const attachmentDefinition = ModelClass.attachmentDefinition(attachmentName)
|
|
1944
2031
|
|
|
1945
2032
|
if (!attachmentDefinition) {
|
|
@@ -1962,9 +2049,7 @@ export default class FrontendModelBase {
|
|
|
1962
2049
|
* @returns {Promise<?>} - Loaded relationship value.
|
|
1963
2050
|
*/
|
|
1964
2051
|
async loadRelationship(relationshipName) {
|
|
1965
|
-
const ModelClass =
|
|
1966
|
-
* Narrows the runtime value to the documented type.
|
|
1967
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
2052
|
+
const ModelClass = frontendModelClassFor(this)
|
|
1968
2053
|
const id = this.primaryKeyValue()
|
|
1969
2054
|
const reloadedModel = await ModelClass
|
|
1970
2055
|
.preload([relationshipName])
|
|
@@ -2024,9 +2109,7 @@ export default class FrontendModelBase {
|
|
|
2024
2109
|
async _tryCohortPreload(relationshipName) {
|
|
2025
2110
|
if (!FrontendModelBase.getAutoload()) return false
|
|
2026
2111
|
|
|
2027
|
-
const ModelClass =
|
|
2028
|
-
* Narrows the runtime value to the documented type.
|
|
2029
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
2112
|
+
const ModelClass = frontendModelClassFor(this)
|
|
2030
2113
|
const cohort = this._loadCohort
|
|
2031
2114
|
|
|
2032
2115
|
if (!cohort || cohort.length <= 1) return false
|
|
@@ -2101,9 +2184,7 @@ export default class FrontendModelBase {
|
|
|
2101
2184
|
* @returns {?} - Assigned relationship value.
|
|
2102
2185
|
*/
|
|
2103
2186
|
setRelationship(relationshipName, relationshipValue) {
|
|
2104
|
-
const ModelClass =
|
|
2105
|
-
* Narrows the runtime value to the documented type.
|
|
2106
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
2187
|
+
const ModelClass = frontendModelClassFor(this)
|
|
2107
2188
|
const relationshipDefinition = ModelClass.relationshipDefinition(relationshipName)
|
|
2108
2189
|
|
|
2109
2190
|
if (!relationshipDefinition) {
|
|
@@ -2152,9 +2233,7 @@ export default class FrontendModelBase {
|
|
|
2152
2233
|
* @returns {number | string} - Primary key value.
|
|
2153
2234
|
*/
|
|
2154
2235
|
primaryKeyValue() {
|
|
2155
|
-
const ModelClass =
|
|
2156
|
-
* Narrows the runtime value to the documented type.
|
|
2157
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
2236
|
+
const ModelClass = frontendModelClassFor(this)
|
|
2158
2237
|
const value = this.readAttribute(ModelClass.primaryKey())
|
|
2159
2238
|
|
|
2160
2239
|
if (value === undefined || value === null) {
|
|
@@ -2296,6 +2375,19 @@ export default class FrontendModelBase {
|
|
|
2296
2375
|
* @returns {?} - Assigned value.
|
|
2297
2376
|
*/
|
|
2298
2377
|
setAttribute(attributeName, newValue) {
|
|
2378
|
+
const ModelClass = frontendModelClassFor(this)
|
|
2379
|
+
const nestedAttributesRelationshipName = ModelClass.nestedAttributesRelationshipName(attributeName)
|
|
2380
|
+
|
|
2381
|
+
if (nestedAttributesRelationshipName) {
|
|
2382
|
+
this._pendingNestedAttributes[nestedAttributesRelationshipName] = newValue
|
|
2383
|
+
return newValue
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
if (ModelClass.attachmentDefinition(attributeName)) {
|
|
2387
|
+
this.getAttachmentByName(attributeName).queueAttach(newValue)
|
|
2388
|
+
return newValue
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2299
2391
|
const previousValue = this._attributes[attributeName]
|
|
2300
2392
|
|
|
2301
2393
|
this._attributes[attributeName] = newValue
|
|
@@ -2330,9 +2422,7 @@ export default class FrontendModelBase {
|
|
|
2330
2422
|
_invalidateRelationshipsForAttribute(attributeName) {
|
|
2331
2423
|
if (!this._relationships || Object.keys(this._relationships).length === 0) return
|
|
2332
2424
|
|
|
2333
|
-
const ModelClass =
|
|
2334
|
-
* Narrows the runtime value to the documented type.
|
|
2335
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
2425
|
+
const ModelClass = frontendModelClassFor(this)
|
|
2336
2426
|
const definitions = typeof ModelClass.relationshipDefinitions === "function" ? ModelClass.relationshipDefinitions() : {}
|
|
2337
2427
|
|
|
2338
2428
|
for (const relationshipName of Object.keys(this._relationships)) {
|
|
@@ -3099,9 +3189,7 @@ export default class FrontendModelBase {
|
|
|
3099
3189
|
const self = /**
|
|
3100
3190
|
* Narrows the runtime value to the documented type.
|
|
3101
3191
|
@type {?} */ (this)
|
|
3102
|
-
const ModelClass =
|
|
3103
|
-
* Narrows the runtime value to the documented type.
|
|
3104
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3192
|
+
const ModelClass = frontendModelClassFor(this)
|
|
3105
3193
|
const sub = ensureFrontendModelEventSubscription(ModelClass)
|
|
3106
3194
|
const id = String(self.id())
|
|
3107
3195
|
const entry = {callback, ...frontendModelEventOptionsPayload(ModelClass, options)}
|
|
@@ -3133,9 +3221,7 @@ export default class FrontendModelBase {
|
|
|
3133
3221
|
const self = /**
|
|
3134
3222
|
* Narrows the runtime value to the documented type.
|
|
3135
3223
|
@type {?} */ (this)
|
|
3136
|
-
const ModelClass =
|
|
3137
|
-
* Narrows the runtime value to the documented type.
|
|
3138
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3224
|
+
const ModelClass = frontendModelClassFor(this)
|
|
3139
3225
|
|
|
3140
3226
|
assertNoDestroyEventFilter(ModelClass, options)
|
|
3141
3227
|
|
|
@@ -3511,50 +3597,9 @@ export default class FrontendModelBase {
|
|
|
3511
3597
|
* @returns {Promise<this>} - Updated model.
|
|
3512
3598
|
*/
|
|
3513
3599
|
async update(newAttributes = {}) {
|
|
3514
|
-
|
|
3515
|
-
* Narrows the runtime value to the documented type.
|
|
3516
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3517
|
-
const attachmentDefinitions = ModelClass.attachmentDefinitions()
|
|
3518
|
-
/**
|
|
3519
|
-
* Regular attributes.
|
|
3520
|
-
@type {Record<string, ?>} */
|
|
3521
|
-
const regularAttributes = {}
|
|
3522
|
-
/**
|
|
3523
|
-
* Pending attachments.
|
|
3524
|
-
@type {Array<{attachmentName: string, value: ?}>} */
|
|
3525
|
-
const pendingAttachments = []
|
|
3526
|
-
|
|
3527
|
-
for (const [attributeName, attributeValue] of Object.entries(newAttributes)) {
|
|
3528
|
-
if (attachmentDefinitions[attributeName]) {
|
|
3529
|
-
if (attributeValue !== undefined && attributeValue !== null) {
|
|
3530
|
-
pendingAttachments.push({attachmentName: attributeName, value: attributeValue})
|
|
3531
|
-
}
|
|
3532
|
-
} else {
|
|
3533
|
-
regularAttributes[attributeName] = attributeValue
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
|
-
|
|
3537
|
-
if (Object.keys(regularAttributes).length > 0) {
|
|
3538
|
-
this.assignAttributes(regularAttributes)
|
|
3539
|
-
const changedAttributes = Object.fromEntries(
|
|
3540
|
-
Object.entries(this.changes()).map(([attributeName, [, currentValue]]) => [attributeName, currentValue])
|
|
3541
|
-
)
|
|
3542
|
-
|
|
3543
|
-
const response = await ModelClass.executeCommand("update", {
|
|
3544
|
-
attributes: changedAttributes,
|
|
3545
|
-
id: this.primaryKeyValue()
|
|
3546
|
-
})
|
|
3547
|
-
|
|
3548
|
-
this.assignAttributes(ModelClass.attributesFromResponse(response))
|
|
3549
|
-
this.setIsNewRecord(false)
|
|
3550
|
-
this._persistedAttributes = cloneFrontendModelAttributes(this.attributes())
|
|
3551
|
-
}
|
|
3552
|
-
|
|
3553
|
-
for (const pendingAttachment of pendingAttachments) {
|
|
3554
|
-
await this.getAttachmentByName(pendingAttachment.attachmentName).attach(pendingAttachment.value)
|
|
3555
|
-
}
|
|
3600
|
+
this.assignAttributes(newAttributes)
|
|
3556
3601
|
|
|
3557
|
-
return this
|
|
3602
|
+
return await this.save()
|
|
3558
3603
|
}
|
|
3559
3604
|
|
|
3560
3605
|
/**
|
|
@@ -3563,9 +3608,7 @@ export default class FrontendModelBase {
|
|
|
3563
3608
|
* @returns {Promise<void>} - Resolves when attached.
|
|
3564
3609
|
*/
|
|
3565
3610
|
async attach(attachmentInput) {
|
|
3566
|
-
const ModelClass =
|
|
3567
|
-
* Narrows the runtime value to the documented type.
|
|
3568
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3611
|
+
const ModelClass = frontendModelClassFor(this)
|
|
3569
3612
|
const attachmentDefinitions = ModelClass.attachmentDefinitions()
|
|
3570
3613
|
const attachmentNames = Object.keys(attachmentDefinitions)
|
|
3571
3614
|
let attachmentName = attachmentNames[0]
|
|
@@ -3597,9 +3640,7 @@ export default class FrontendModelBase {
|
|
|
3597
3640
|
* @returns {Promise<this>} - Saved model.
|
|
3598
3641
|
*/
|
|
3599
3642
|
async save() {
|
|
3600
|
-
const ModelClass =
|
|
3601
|
-
* Narrows the runtime value to the documented type.
|
|
3602
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3643
|
+
const ModelClass = frontendModelClassFor(this)
|
|
3603
3644
|
const isNew = this.isNewRecord()
|
|
3604
3645
|
const commandType = isNew ? "create" : "update"
|
|
3605
3646
|
/**
|
|
@@ -3613,17 +3654,25 @@ export default class FrontendModelBase {
|
|
|
3613
3654
|
payload.id = this.primaryKeyValue()
|
|
3614
3655
|
}
|
|
3615
3656
|
|
|
3616
|
-
const nestedAttributes = this._buildNestedAttributesPayload()
|
|
3657
|
+
const nestedAttributes = await this._buildNestedAttributesPayload()
|
|
3617
3658
|
|
|
3618
3659
|
if (nestedAttributes && Object.keys(nestedAttributes).length > 0) {
|
|
3619
3660
|
payload.nestedAttributes = nestedAttributes
|
|
3620
3661
|
}
|
|
3621
3662
|
|
|
3663
|
+
const attachments = await this._buildAttachmentsPayload()
|
|
3664
|
+
|
|
3665
|
+
if (Object.keys(attachments).length > 0) {
|
|
3666
|
+
payload.attachments = attachments
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3622
3669
|
const response = await ModelClass.executeCommand(commandType, payload)
|
|
3623
3670
|
|
|
3624
3671
|
this.assignAttributes(ModelClass.attributesFromResponse(response))
|
|
3625
3672
|
this.setIsNewRecord(false)
|
|
3626
3673
|
this._persistedAttributes = cloneFrontendModelAttributes(this.attributes())
|
|
3674
|
+
this._pendingNestedAttributes = {}
|
|
3675
|
+
this._clearPendingAttachments()
|
|
3627
3676
|
|
|
3628
3677
|
this._reconcileNestedAttributesFromResponse(response)
|
|
3629
3678
|
|
|
@@ -3668,15 +3717,39 @@ export default class FrontendModelBase {
|
|
|
3668
3717
|
* @returns {Promise<void>} - Resolves when destroyed on backend.
|
|
3669
3718
|
*/
|
|
3670
3719
|
async destroy() {
|
|
3671
|
-
const ModelClass =
|
|
3672
|
-
* Narrows the runtime value to the documented type.
|
|
3673
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3720
|
+
const ModelClass = frontendModelClassFor(this)
|
|
3674
3721
|
|
|
3675
3722
|
await ModelClass.executeCommand("destroy", {
|
|
3676
3723
|
id: this.primaryKeyValue()
|
|
3677
3724
|
})
|
|
3678
3725
|
}
|
|
3679
3726
|
|
|
3727
|
+
/**
|
|
3728
|
+
* Builds the attachment payload queued on this model for the next save.
|
|
3729
|
+
* @returns {Promise<Record<string, ?>>} Attachment payload keyed by attachment name.
|
|
3730
|
+
*/
|
|
3731
|
+
async _buildAttachmentsPayload() {
|
|
3732
|
+
/** @type {Record<string, ?>} */
|
|
3733
|
+
const payload = {}
|
|
3734
|
+
|
|
3735
|
+
for (const attachmentName of Object.keys(this._attachments)) {
|
|
3736
|
+
const attachmentPayload = await this._attachments[attachmentName].pendingAttachmentsPayload()
|
|
3737
|
+
|
|
3738
|
+
if (attachmentPayload !== undefined) {
|
|
3739
|
+
payload[attachmentName] = attachmentPayload
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
return payload
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
/** Clears queued attachment inputs after a successful save. */
|
|
3747
|
+
_clearPendingAttachments() {
|
|
3748
|
+
for (const attachmentName of Object.keys(this._attachments)) {
|
|
3749
|
+
this._attachments[attachmentName].clearPendingAttachments()
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3680
3753
|
/**
|
|
3681
3754
|
* Walks relationships declared in this resource's `nestedAttributes` config
|
|
3682
3755
|
* and builds the per-relationship payload of dirty children for a parent save.
|
|
@@ -3689,12 +3762,10 @@ export default class FrontendModelBase {
|
|
|
3689
3762
|
*
|
|
3690
3763
|
* Loaded but untouched records are omitted so nested save preserves Rails-style
|
|
3691
3764
|
* "children not referenced in payload are left alone" semantics.
|
|
3692
|
-
* @returns {Record<string, Array<Record<string,
|
|
3765
|
+
* @returns {Promise<Record<string, Array<Record<string, ?>>>>} - Per-relationship list of nested-attribute entries.
|
|
3693
3766
|
*/
|
|
3694
|
-
_buildNestedAttributesPayload() {
|
|
3695
|
-
const ModelClass =
|
|
3696
|
-
* Narrows the runtime value to the documented type.
|
|
3697
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3767
|
+
async _buildNestedAttributesPayload() {
|
|
3768
|
+
const ModelClass = frontendModelClassFor(this)
|
|
3698
3769
|
const resourceConfig = typeof ModelClass.resourceConfig === "function" ? ModelClass.resourceConfig() : null
|
|
3699
3770
|
const nestedAttributesConfig = resourceConfig?.nestedAttributes
|
|
3700
3771
|
|
|
@@ -3706,20 +3777,34 @@ export default class FrontendModelBase {
|
|
|
3706
3777
|
const payload = {}
|
|
3707
3778
|
|
|
3708
3779
|
for (const relationshipName of Object.keys(nestedAttributesConfig)) {
|
|
3780
|
+
/** @type {Array<Record<string, ?>>} */
|
|
3781
|
+
const entries = []
|
|
3709
3782
|
const relationship = this._relationships[relationshipName]
|
|
3710
3783
|
|
|
3711
|
-
if (
|
|
3712
|
-
|
|
3784
|
+
if (relationship instanceof FrontendModelHasManyRelationship && Array.isArray(relationship._loadedValue)) {
|
|
3785
|
+
for (const child of relationship._loadedValue) {
|
|
3786
|
+
const childEntry = await child._nestedAttributesEntryForParentSave()
|
|
3713
3787
|
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3788
|
+
if (childEntry) entries.push(childEntry)
|
|
3789
|
+
}
|
|
3790
|
+
} else if (relationship instanceof FrontendModelSingularRelationship && relationship.getPreloaded()) {
|
|
3791
|
+
const child = relationship.loaded()
|
|
3718
3792
|
|
|
3719
|
-
|
|
3720
|
-
|
|
3793
|
+
if (child instanceof FrontendModelBase) {
|
|
3794
|
+
const childEntry = await child._nestedAttributesEntryForParentSave()
|
|
3721
3795
|
|
|
3722
|
-
|
|
3796
|
+
if (childEntry) entries.push(childEntry)
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
if (Object.prototype.hasOwnProperty.call(this._pendingNestedAttributes, relationshipName)) {
|
|
3801
|
+
entries.push(
|
|
3802
|
+
...await this._nestedAttributesPayloadForSubmittedValue(
|
|
3803
|
+
ModelClass,
|
|
3804
|
+
relationshipName,
|
|
3805
|
+
this._pendingNestedAttributes[relationshipName]
|
|
3806
|
+
)
|
|
3807
|
+
)
|
|
3723
3808
|
}
|
|
3724
3809
|
|
|
3725
3810
|
if (entries.length > 0) {
|
|
@@ -3734,41 +3819,164 @@ export default class FrontendModelBase {
|
|
|
3734
3819
|
* Builds the payload entry for this child when walked by a parent's
|
|
3735
3820
|
* `_buildNestedAttributesPayload`. Returns `null` when the child has no
|
|
3736
3821
|
* dirty state and no dirty descendants, so the parent can omit it.
|
|
3737
|
-
* @returns {Record<string, ?> | null} - Nested-attribute entry or null if clean.
|
|
3822
|
+
* @returns {Promise<Record<string, ?> | null>} - Nested-attribute entry or null if clean.
|
|
3738
3823
|
*/
|
|
3739
|
-
_nestedAttributesEntryForParentSave() {
|
|
3824
|
+
async _nestedAttributesEntryForParentSave() {
|
|
3740
3825
|
if (this.markedForDestruction()) {
|
|
3741
3826
|
if (this.isNewRecord()) return null
|
|
3742
3827
|
return {id: this.primaryKeyValue(), _destroy: true}
|
|
3743
3828
|
}
|
|
3744
3829
|
|
|
3745
|
-
const nestedAttributes = this._buildNestedAttributesPayload()
|
|
3830
|
+
const nestedAttributes = await this._buildNestedAttributesPayload()
|
|
3746
3831
|
const hasNestedDirty = Object.keys(nestedAttributes).length > 0
|
|
3832
|
+
const attachments = await this._buildAttachmentsPayload()
|
|
3833
|
+
const hasAttachments = Object.keys(attachments).length > 0
|
|
3747
3834
|
|
|
3748
3835
|
if (this.isNewRecord()) {
|
|
3749
3836
|
/**
|
|
3750
3837
|
* Entry.
|
|
3751
3838
|
@type {Record<string, ?>} */
|
|
3752
|
-
const entry = {
|
|
3839
|
+
const entry = {}
|
|
3840
|
+
const attributes = this._changedAttributesForSave()
|
|
3753
3841
|
|
|
3842
|
+
if (Object.keys(attributes).length > 0) entry.attributes = attributes
|
|
3843
|
+
if (hasAttachments) entry.attachments = attachments
|
|
3754
3844
|
if (hasNestedDirty) entry.nestedAttributes = nestedAttributes
|
|
3755
3845
|
|
|
3756
3846
|
return entry
|
|
3757
3847
|
}
|
|
3758
3848
|
|
|
3759
|
-
if (!this.isChanged() && !hasNestedDirty) return null
|
|
3849
|
+
if (!this.isChanged() && !hasNestedDirty && !hasAttachments) return null
|
|
3760
3850
|
|
|
3761
3851
|
/**
|
|
3762
3852
|
* Entry.
|
|
3763
3853
|
@type {Record<string, ?>} */
|
|
3764
3854
|
const entry = {id: this.primaryKeyValue()}
|
|
3765
3855
|
|
|
3766
|
-
if (this.isChanged()) entry.attributes = this.
|
|
3856
|
+
if (this.isChanged()) entry.attributes = this._changedAttributesForSave()
|
|
3857
|
+
if (hasAttachments) entry.attachments = attachments
|
|
3767
3858
|
if (hasNestedDirty) entry.nestedAttributes = nestedAttributes
|
|
3768
3859
|
|
|
3769
3860
|
return entry
|
|
3770
3861
|
}
|
|
3771
3862
|
|
|
3863
|
+
/**
|
|
3864
|
+
* Builds nested entries from a Rails-style submitted `*Attributes` value.
|
|
3865
|
+
* @param {typeof FrontendModelBase} ModelClass - Parent model class.
|
|
3866
|
+
* @param {string} relationshipName - Nested relationship name.
|
|
3867
|
+
* @param {?} value - Submitted nested attributes value.
|
|
3868
|
+
* @returns {Promise<Array<Record<string, ?>>>} Nested entries for the transport payload.
|
|
3869
|
+
*/
|
|
3870
|
+
async _nestedAttributesPayloadForSubmittedValue(ModelClass, relationshipName, value) {
|
|
3871
|
+
const relationshipDefinition = ModelClass.relationshipDefinition(relationshipName)
|
|
3872
|
+
const TargetModelClass = ModelClass.relationshipModelClass(relationshipName)
|
|
3873
|
+
|
|
3874
|
+
if (!relationshipDefinition) {
|
|
3875
|
+
throw new Error(`Unknown nested relationship: ${ModelClass.name}#${relationshipName}`)
|
|
3876
|
+
}
|
|
3877
|
+
if (!TargetModelClass) {
|
|
3878
|
+
throw new Error(`No target model class configured for ${ModelClass.name}#${relationshipName}`)
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
if (relationshipTypeIsCollection(relationshipDefinition.type)) {
|
|
3882
|
+
if (!Array.isArray(value)) {
|
|
3883
|
+
throw new Error(`${ModelClass.name}#${relationshipName}Attributes must be an array`)
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
return await Promise.all(
|
|
3887
|
+
value.map(async (entry) => await this._nestedAttributesEntryPayloadForSubmittedValue(TargetModelClass, entry))
|
|
3888
|
+
)
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
if (value == null) return []
|
|
3892
|
+
if (Array.isArray(value)) {
|
|
3893
|
+
throw new Error(`${ModelClass.name}#${relationshipName}Attributes must be an object`)
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
return [await this._nestedAttributesEntryPayloadForSubmittedValue(TargetModelClass, value)]
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
/**
|
|
3900
|
+
* Converts one submitted Rails-style nested attributes object into transport payload shape.
|
|
3901
|
+
* @param {typeof FrontendModelBase} ModelClass - Nested child model class.
|
|
3902
|
+
* @param {?} submittedEntry - Submitted nested attributes entry.
|
|
3903
|
+
* @returns {Promise<Record<string, ?>>} Transport nested-attributes entry.
|
|
3904
|
+
*/
|
|
3905
|
+
async _nestedAttributesEntryPayloadForSubmittedValue(ModelClass, submittedEntry) {
|
|
3906
|
+
if (!frontendAttachmentValueIsPlainObject(submittedEntry)) {
|
|
3907
|
+
throw new Error(`${ModelClass.name} nested attributes entries must be objects`)
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
/** @type {Record<string, ?>} */
|
|
3911
|
+
const entry = {}
|
|
3912
|
+
/** @type {Record<string, ?>} */
|
|
3913
|
+
const attributes = {}
|
|
3914
|
+
/** @type {Record<string, ?>} */
|
|
3915
|
+
const attachments = {}
|
|
3916
|
+
/** @type {Record<string, Array<Record<string, ?>>>} */
|
|
3917
|
+
const nestedAttributes = {}
|
|
3918
|
+
|
|
3919
|
+
for (const [attributeName, value] of Object.entries(submittedEntry)) {
|
|
3920
|
+
if (attributeName === "id" || attributeName === "_destroy") {
|
|
3921
|
+
entry[attributeName] = value
|
|
3922
|
+
continue
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
const nestedRelationshipName = ModelClass.nestedAttributesRelationshipName(attributeName)
|
|
3926
|
+
|
|
3927
|
+
if (nestedRelationshipName) {
|
|
3928
|
+
nestedAttributes[nestedRelationshipName] = await this._nestedAttributesPayloadForSubmittedValue(
|
|
3929
|
+
ModelClass,
|
|
3930
|
+
nestedRelationshipName,
|
|
3931
|
+
value
|
|
3932
|
+
)
|
|
3933
|
+
continue
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
if (ModelClass.attachmentDefinition(attributeName)) {
|
|
3937
|
+
attachments[attributeName] = await this._attachmentPayloadForSubmittedValue(ModelClass, attributeName, value)
|
|
3938
|
+
continue
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
attributes[attributeName] = value
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
if (Object.keys(attributes).length > 0) entry.attributes = attributes
|
|
3945
|
+
if (Object.keys(attachments).length > 0) entry.attachments = attachments
|
|
3946
|
+
if (Object.keys(nestedAttributes).length > 0) entry.nestedAttributes = nestedAttributes
|
|
3947
|
+
|
|
3948
|
+
return entry
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
/**
|
|
3952
|
+
* Normalizes a submitted attachment value for transport.
|
|
3953
|
+
* @param {typeof FrontendModelBase} ModelClass - Model class owning the attachment.
|
|
3954
|
+
* @param {string} attachmentName - Attachment name.
|
|
3955
|
+
* @param {?} value - Submitted attachment value.
|
|
3956
|
+
* @returns {Promise<Record<string, ?> | Record<string, ?>[]>} Normalized attachment payload.
|
|
3957
|
+
*/
|
|
3958
|
+
async _attachmentPayloadForSubmittedValue(ModelClass, attachmentName, value) {
|
|
3959
|
+
const attachmentDefinition = ModelClass.attachmentDefinition(attachmentName)
|
|
3960
|
+
|
|
3961
|
+
if (attachmentDefinition?.type === "hasMany") {
|
|
3962
|
+
const values = Array.isArray(value) ? value : [value]
|
|
3963
|
+
|
|
3964
|
+
return await Promise.all(values.map(async (entry) => await normalizeFrontendAttachmentInput(entry)))
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
if (Array.isArray(value)) {
|
|
3968
|
+
const lastValue = value[value.length - 1]
|
|
3969
|
+
|
|
3970
|
+
if (lastValue === undefined) {
|
|
3971
|
+
throw new Error(`${ModelClass.name}#${attachmentName} attachment array cannot be empty`)
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
return await normalizeFrontendAttachmentInput(lastValue)
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
return await normalizeFrontendAttachmentInput(value)
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3772
3980
|
/**
|
|
3773
3981
|
* After a parent save with `nestedAttributes`, the server response includes
|
|
3774
3982
|
* preloaded versions of the affected relationships. This replaces the local
|
|
@@ -3779,9 +3987,7 @@ export default class FrontendModelBase {
|
|
|
3779
3987
|
* @returns {void}
|
|
3780
3988
|
*/
|
|
3781
3989
|
_reconcileNestedAttributesFromResponse(response) {
|
|
3782
|
-
const ModelClass =
|
|
3783
|
-
* Narrows the runtime value to the documented type.
|
|
3784
|
-
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3990
|
+
const ModelClass = frontendModelClassFor(this)
|
|
3785
3991
|
const resourceConfig = typeof ModelClass.resourceConfig === "function" ? ModelClass.resourceConfig() : null
|
|
3786
3992
|
const nestedAttributesConfig = resourceConfig?.nestedAttributes
|
|
3787
3993
|
|