velocious 1.0.445 → 1.0.447
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/README.md +1 -1
- package/build/configuration-types.js +2 -2
- package/build/database/pool/async-tracked-multi-connection.js +3 -1
- package/build/database/record/index.js +38 -38
- 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 +417 -203
- package/build/frontend-models/preloader.js +7 -7
- package/build/frontend-models/query.js +18 -18
- package/build/frontend-models/use-created-event.js +1 -1
- package/build/frontend-models/use-destroyed-event.js +1 -1
- package/build/frontend-models/use-model-class-event.js +1 -1
- package/build/frontend-models/use-updated-event.js +1 -1
- package/build/frontend-models/websocket-channel.js +39 -3
- package/build/routes/resolver.js +17 -14
- package/build/src/configuration-types.d.ts +6 -6
- package/build/src/configuration-types.js +3 -3
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +5 -2
- package/build/src/database/record/index.d.ts +38 -38
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +39 -39
- 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 +232 -149
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +371 -201
- package/build/src/frontend-models/preloader.d.ts +10 -10
- package/build/src/frontend-models/preloader.d.ts.map +1 -1
- package/build/src/frontend-models/preloader.js +8 -8
- package/build/src/frontend-models/query.d.ts +8 -8
- package/build/src/frontend-models/query.d.ts.map +1 -1
- package/build/src/frontend-models/query.js +19 -19
- package/build/src/frontend-models/use-created-event.d.ts +2 -2
- package/build/src/frontend-models/use-created-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-created-event.js +2 -2
- package/build/src/frontend-models/use-destroyed-event.d.ts +1 -1
- package/build/src/frontend-models/use-destroyed-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-destroyed-event.js +2 -2
- package/build/src/frontend-models/use-model-class-event.d.ts +1 -1
- package/build/src/frontend-models/use-model-class-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-model-class-event.js +2 -2
- package/build/src/frontend-models/use-updated-event.d.ts +1 -1
- package/build/src/frontend-models/use-updated-event.d.ts.map +1 -1
- package/build/src/frontend-models/use-updated-event.js +2 -2
- 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/build/src/routes/resolver.d.ts.map +1 -1
- package/build/src/routes/resolver.js +7 -4
- package/build/src/utils/model-scope.d.ts +4 -4
- package/build/src/utils/model-scope.d.ts.map +1 -1
- package/build/src/utils/model-scope.js +3 -3
- package/build/src/utils/ransack.d.ts +1 -1
- package/build/src/utils/ransack.d.ts.map +1 -1
- package/build/src/utils/ransack.js +2 -2
- package/build/utils/model-scope.js +2 -2
- package/build/utils/ransack.js +1 -1
- package/package.json +1 -1
- package/src/configuration-types.js +2 -2
- package/src/database/pool/async-tracked-multi-connection.js +3 -1
- package/src/database/record/index.js +38 -38
- 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 +417 -203
- package/src/frontend-models/preloader.js +7 -7
- package/src/frontend-models/query.js +18 -18
- package/src/frontend-models/use-created-event.js +1 -1
- package/src/frontend-models/use-destroyed-event.js +1 -1
- package/src/frontend-models/use-model-class-event.js +1 -1
- package/src/frontend-models/use-updated-event.js +1 -1
- package/src/frontend-models/websocket-channel.js +39 -3
- package/src/routes/resolver.js +17 -14
- package/src/utils/model-scope.js +2 -2
- package/src/utils/ransack.js +1 -1
|
@@ -115,12 +115,48 @@ export default class DbGenerateModel extends BaseCommand {
|
|
|
115
115
|
velociousPath = "velocious/build/src"
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
const columns = await table.getColumns()
|
|
119
|
+
const writeAttributeTypeName = `${modelNameCamelized}WriteAttributes`
|
|
120
|
+
const nestedWriteAttributes = this.nestedWriteAttributesForModel({modelClass})
|
|
121
|
+
|
|
118
122
|
fileContent += `import DatabaseRecord from "${velociousPath}/database/record/index.js"\n\n`
|
|
123
|
+
fileContent += "/**\n"
|
|
124
|
+
fileContent += ` * Attributes accepted when creating or updating ${modelNameCamelized} records.\n`
|
|
125
|
+
fileContent += ` * @typedef {object} ${writeAttributeTypeName}\n`
|
|
126
|
+
for (const column of columns) {
|
|
127
|
+
const deburredColumnName = deburrColumnName(column.getName())
|
|
128
|
+
const camelizedColumnName = inflection.camelize(deburredColumnName, true)
|
|
129
|
+
const setterJsdocType = this.jsDocSetterTypeFromColumn(column, modelClass)
|
|
130
|
+
|
|
131
|
+
if (setterJsdocType) {
|
|
132
|
+
fileContent += ` * @property {${setterJsdocType}${column.getNull() ? " | null" : ""}} [${camelizedColumnName}] - Value for the ${camelizedColumnName} attribute.\n`
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const nestedWriteAttribute of nestedWriteAttributes) {
|
|
136
|
+
fileContent += ` * @property {${nestedWriteAttribute.propertyType}} [${nestedWriteAttribute.propertyName}] - Nested ${nestedWriteAttribute.relationshipName} attributes.\n`
|
|
137
|
+
}
|
|
138
|
+
fileContent += " */\n\n"
|
|
119
139
|
|
|
120
140
|
const hasManyRelationFilePath = `${velociousPath}/database/record/instance-relationships/has-many.js`
|
|
121
141
|
|
|
122
142
|
fileContent += `export default class ${modelNameCamelized}Base extends DatabaseRecord {\n`
|
|
123
143
|
|
|
144
|
+
fileContent += " /**\n"
|
|
145
|
+
fileContent += ` * Creates a ${modelNameCamelized} record.\n`
|
|
146
|
+
fileContent += ` * @template {typeof ${modelNameCamelized}Base} T\n`
|
|
147
|
+
fileContent += " * @this {T}\n"
|
|
148
|
+
fileContent += ` * @param {${writeAttributeTypeName}} [attributes] - Attributes for the new record.\n`
|
|
149
|
+
fileContent += " * @returns {Promise<InstanceType<T>>} - Persisted record.\n"
|
|
150
|
+
fileContent += " */\n"
|
|
151
|
+
fileContent += " static async create(attributes) { return /** @type {Promise<InstanceType<T>>} */ (super.create(attributes)) }\n\n"
|
|
152
|
+
|
|
153
|
+
fileContent += " /**\n"
|
|
154
|
+
fileContent += ` * Updates this ${modelNameCamelized} record.\n`
|
|
155
|
+
fileContent += ` * @param {${writeAttributeTypeName}} attributes - Attributes to assign before saving.\n`
|
|
156
|
+
fileContent += " * @returns {Promise<void>} - Resolves when the record is saved.\n"
|
|
157
|
+
fileContent += " */\n"
|
|
158
|
+
fileContent += " async update(attributes) { return await super.update(attributes) }\n\n"
|
|
159
|
+
|
|
124
160
|
// --- getModelClass() override (fixes polymorphic typing in JS/JSDoc) ---
|
|
125
161
|
if (await fileExists(sourceModelFullFilePath)) {
|
|
126
162
|
// Model file exists (e.g. src/models/ticket.js) → return typeof Ticket
|
|
@@ -138,7 +174,6 @@ export default class DbGenerateModel extends BaseCommand {
|
|
|
138
174
|
fileContent += ` getModelClass() { return /** @type {typeof ${modelNameCamelized}Base} */ (this.constructor) }\n\n`
|
|
139
175
|
}
|
|
140
176
|
|
|
141
|
-
const columns = await table.getColumns()
|
|
142
177
|
let methodsCount = 0
|
|
143
178
|
|
|
144
179
|
for (const column of columns) {
|
|
@@ -416,4 +451,35 @@ export default class DbGenerateModel extends BaseCommand {
|
|
|
416
451
|
|
|
417
452
|
return this.jsDocTypeFromColumn(column, modelClass)
|
|
418
453
|
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Runs nested write attributes for model.
|
|
457
|
+
* @param {object} args - Arguments.
|
|
458
|
+
* @param {typeof import("../../../../../database/record/index.js").default} args.modelClass - Model class.
|
|
459
|
+
* @returns {Array<{propertyName: string, propertyType: string, relationshipName: string}>} - Nested write attributes.
|
|
460
|
+
*/
|
|
461
|
+
nestedWriteAttributesForModel({modelClass}) {
|
|
462
|
+
const acceptedNestedAttributes = modelClass._acceptedNestedAttributes || {}
|
|
463
|
+
const nestedWriteAttributes = []
|
|
464
|
+
|
|
465
|
+
for (const relationshipName of Object.keys(acceptedNestedAttributes)) {
|
|
466
|
+
const relationship = modelClass.getRelationshipByName(relationshipName)
|
|
467
|
+
const relationshipType = relationship.getType()
|
|
468
|
+
const targetModelClass = relationship.getTargetModelClass()
|
|
469
|
+
|
|
470
|
+
if (!targetModelClass) throw new Error(`Relationship '${relationshipName}' on '${modelClass.getModelName()}' has no target model class`)
|
|
471
|
+
|
|
472
|
+
const targetModelFileName = inflection.dasherize(inflection.underscore(targetModelClass.getModelName()))
|
|
473
|
+
const targetWriteTypeName = `${inflection.camelize(targetModelClass.getModelName().replaceAll("-", "_"))}WriteAttributes`
|
|
474
|
+
const nestedType = `import("./${targetModelFileName}.js").${targetWriteTypeName}${acceptedNestedAttributes[relationshipName]?.allowDestroy ? " & {_destroy?: boolean}" : ""}`
|
|
475
|
+
|
|
476
|
+
nestedWriteAttributes.push({
|
|
477
|
+
propertyName: `${relationshipName}Attributes`,
|
|
478
|
+
propertyType: relationshipType == "hasMany" ? `Array<${nestedType}>` : nestedType,
|
|
479
|
+
relationshipName
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return nestedWriteAttributes
|
|
484
|
+
}
|
|
419
485
|
}
|
|
@@ -228,7 +228,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
228
228
|
? modelConfig.attachments
|
|
229
229
|
: {}
|
|
230
230
|
const attributesTypeName = `${className}Attributes`
|
|
231
|
+
const createAttributesTypeName = `${className}CreateAttributes`
|
|
232
|
+
const updateAttributesTypeName = `${className}UpdateAttributes`
|
|
231
233
|
const attributeNames = attributes.map((attribute) => attribute.name)
|
|
234
|
+
const permittedCreateParams = this.permittedParamsForGenerator(resourceClass || null, "create")
|
|
235
|
+
const permittedUpdateParams = this.permittedParamsForGenerator(resourceClass || null, "update")
|
|
236
|
+
const nestedWriteTypes = this.nestedWriteTypesForModel({className, permittedParams: permittedCreateParams.concat(permittedUpdateParams), relationships})
|
|
232
237
|
const builtInCollectionCommands = {
|
|
233
238
|
create: modelConfig.builtInCollectionCommands.create || "create",
|
|
234
239
|
index: modelConfig.builtInCollectionCommands.index || "index"
|
|
@@ -268,8 +273,32 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
268
273
|
fileContent += ` * @property {${attribute.jsDocType}} ${attribute.name} - Attribute value.\n`
|
|
269
274
|
}
|
|
270
275
|
fileContent += " */\n"
|
|
276
|
+
for (const nestedWriteType of nestedWriteTypes) {
|
|
277
|
+
fileContent += "/**\n"
|
|
278
|
+
fileContent += ` * Attributes accepted for nested ${nestedWriteType.relationshipName} writes.\n`
|
|
279
|
+
fileContent += ` * @typedef {object} ${nestedWriteType.typeName}\n`
|
|
280
|
+
for (const nestedAttribute of nestedWriteType.attributes) {
|
|
281
|
+
fileContent += ` * @property {${nestedAttribute.type}} [${nestedAttribute.name}] - Nested ${nestedAttribute.name} value.\n`
|
|
282
|
+
}
|
|
283
|
+
fileContent += " */\n"
|
|
284
|
+
}
|
|
285
|
+
fileContent += this.writeAttributesTypedef({attributes, attributesTypeName, nestedWriteTypes, permittedParams: permittedCreateParams, typeName: createAttributesTypeName})
|
|
286
|
+
fileContent += this.writeAttributesTypedef({attributes, attributesTypeName, nestedWriteTypes, permittedParams: permittedUpdateParams, typeName: updateAttributesTypeName})
|
|
271
287
|
fileContent += `/** Frontend model for ${className}. */\n`
|
|
272
288
|
fileContent += `export default class ${className} extends FrontendModelBase {\n`
|
|
289
|
+
fileContent += " /**\n"
|
|
290
|
+
fileContent += ` * Creates a ${className}.\n`
|
|
291
|
+
fileContent += ` * @param {${createAttributesTypeName}} [attributes] - Attributes for the new model.\n`
|
|
292
|
+
fileContent += ` * @returns {Promise<${className}>} - Persisted model.\n`
|
|
293
|
+
fileContent += " */\n"
|
|
294
|
+
fileContent += ` static async create(attributes = {}) { return /** @type {Promise<${className}>} */ (super.create(attributes)) }\n\n`
|
|
295
|
+
|
|
296
|
+
fileContent += " /**\n"
|
|
297
|
+
fileContent += ` * Updates this ${className}.\n`
|
|
298
|
+
fileContent += ` * @param {${updateAttributesTypeName}} [newAttributes] - Attributes to assign before saving.\n`
|
|
299
|
+
fileContent += ` * @returns {Promise<${className}>} - Updated model.\n`
|
|
300
|
+
fileContent += " */\n"
|
|
301
|
+
fileContent += ` async update(newAttributes = {}) { return /** @type {Promise<${className}>} */ (super.update(newAttributes)) }\n\n`
|
|
273
302
|
fileContent += " /** @returns {FrontendModelResourceConfig} - Resource config. */\n"
|
|
274
303
|
fileContent += " static resourceConfig() {\n"
|
|
275
304
|
fileContent += " return {\n"
|
|
@@ -522,6 +551,146 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
522
551
|
return content
|
|
523
552
|
}
|
|
524
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Runs write attributes typedef.
|
|
556
|
+
* @param {object} args - Arguments.
|
|
557
|
+
* @param {Array<{jsDocType: string, name: string}>} args.attributes - Generated read attributes.
|
|
558
|
+
* @param {string} args.attributesTypeName - Generated read attributes typedef name.
|
|
559
|
+
* @param {Array<{attributes: Array<{name: string, type: string}>, relationshipName: string, typeName: string}>} args.nestedWriteTypes - Nested write typedefs.
|
|
560
|
+
* @param {Array<string | Record<string, ?>>} args.permittedParams - Resource permitted params spec.
|
|
561
|
+
* @param {string} args.typeName - Typedef name.
|
|
562
|
+
* @returns {string} - Generated typedef source.
|
|
563
|
+
*/
|
|
564
|
+
writeAttributesTypedef({attributes, attributesTypeName, nestedWriteTypes, permittedParams, typeName}) {
|
|
565
|
+
let output = "/**\n"
|
|
566
|
+
|
|
567
|
+
output += ` * Attributes accepted by ${typeName}.\n`
|
|
568
|
+
output += ` * @typedef {object} ${typeName}\n`
|
|
569
|
+
|
|
570
|
+
const attributesByName = new Map(attributes.map((attribute) => [attribute.name, attribute]))
|
|
571
|
+
const nestedWriteTypesByKey = new Map(nestedWriteTypes.map((nestedWriteType) => [`${nestedWriteType.relationshipName}Attributes`, nestedWriteType]))
|
|
572
|
+
|
|
573
|
+
for (const entry of permittedParams) {
|
|
574
|
+
if (typeof entry == "string") {
|
|
575
|
+
const attribute = attributesByName.get(entry)
|
|
576
|
+
const type = attribute ? `${attributesTypeName}[${JSON.stringify(attribute.name)}]` : "?"
|
|
577
|
+
|
|
578
|
+
output += ` * @property {${type}} [${entry}] - Permitted ${entry} value.\n`
|
|
579
|
+
} else if (entry && typeof entry == "object" && !Array.isArray(entry)) {
|
|
580
|
+
for (const key of Object.keys(entry)) {
|
|
581
|
+
const nestedWriteType = nestedWriteTypesByKey.get(key)
|
|
582
|
+
const type = nestedWriteType ? `Array<${nestedWriteType.typeName}>` : "Array<Record<string, ?>>"
|
|
583
|
+
|
|
584
|
+
output += ` * @property {${type}} [${key}] - Permitted nested ${key} values.\n`
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
output += " */\n"
|
|
590
|
+
|
|
591
|
+
return output
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Runs nested write types for model.
|
|
596
|
+
* @param {object} args - Arguments.
|
|
597
|
+
* @param {string} args.className - Frontend model class name.
|
|
598
|
+
* @param {Array<string | Record<string, ?>>} args.permittedParams - Combined permitted params specs.
|
|
599
|
+
* @param {Array<{autoload: boolean, relationshipName: string, targetClassName: string, targetFileName: string, type: "belongsTo" | "hasOne" | "hasMany"}>} args.relationships - Generated relationships.
|
|
600
|
+
* @returns {Array<{attributes: Array<{name: string, type: string}>, relationshipName: string, typeName: string}>} - Nested write typedefs.
|
|
601
|
+
*/
|
|
602
|
+
nestedWriteTypesForModel({className, permittedParams, relationships}) {
|
|
603
|
+
const relationshipsByName = new Map(relationships.map((relationship) => [relationship.relationshipName, relationship]))
|
|
604
|
+
const nestedWriteTypesByName = new Map()
|
|
605
|
+
|
|
606
|
+
for (const entry of permittedParams) {
|
|
607
|
+
if (!entry || typeof entry != "object" || Array.isArray(entry)) continue
|
|
608
|
+
|
|
609
|
+
for (const key of Object.keys(entry)) {
|
|
610
|
+
if (!key.endsWith("Attributes")) continue
|
|
611
|
+
const relationshipName = key.slice(0, -"Attributes".length)
|
|
612
|
+
const nestedSpec = entry[key]
|
|
613
|
+
const relationship = relationshipsByName.get(relationshipName)
|
|
614
|
+
let targetModelClass
|
|
615
|
+
|
|
616
|
+
if (relationship) {
|
|
617
|
+
try {
|
|
618
|
+
targetModelClass = this.getConfiguration().getModelClass(relationship.targetClassName)
|
|
619
|
+
} catch {
|
|
620
|
+
targetModelClass = undefined
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (nestedWriteTypesByName.has(relationshipName)) continue
|
|
625
|
+
|
|
626
|
+
nestedWriteTypesByName.set(relationshipName, {
|
|
627
|
+
attributes: this.nestedWriteAttributesForSpec({nestedSpec, targetModelClass}),
|
|
628
|
+
relationshipName,
|
|
629
|
+
typeName: `${className}${inflection.camelize(relationshipName)}NestedAttributes`
|
|
630
|
+
})
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return Array.from(nestedWriteTypesByName.values())
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Runs nested write attributes for spec.
|
|
639
|
+
* @param {object} args - Arguments.
|
|
640
|
+
* @param {?} args.nestedSpec - Nested permit spec.
|
|
641
|
+
* @param {typeof import("../../../../../database/record/index.js").default | undefined} args.targetModelClass - Target backend model class.
|
|
642
|
+
* @returns {Array<{name: string, type: string}>} - Nested write attributes.
|
|
643
|
+
*/
|
|
644
|
+
nestedWriteAttributesForSpec({nestedSpec, targetModelClass}) {
|
|
645
|
+
if (!Array.isArray(nestedSpec)) return []
|
|
646
|
+
|
|
647
|
+
return nestedSpec.filter((entry) => typeof entry == "string").map((attributeName) => {
|
|
648
|
+
const attributeConfig = this.frontendAttributeConfigForModelAttribute({attributeName, modelClass: targetModelClass})
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
name: attributeName,
|
|
652
|
+
type: attributeConfig ? this.jsDocTypeForFrontendAttribute({attributeConfig}) : "?"
|
|
653
|
+
}
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Runs permitted params for generator.
|
|
659
|
+
* @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} resourceClass - Resource class.
|
|
660
|
+
* @param {"create" | "update"} action - Write action.
|
|
661
|
+
* @returns {Array<string | Record<string, ?>>} - Permitted params spec.
|
|
662
|
+
*/
|
|
663
|
+
permittedParamsForGenerator(resourceClass, action) {
|
|
664
|
+
if (!resourceClass || typeof resourceClass !== "function") return []
|
|
665
|
+
|
|
666
|
+
const prototypeWithMethod = /**
|
|
667
|
+
* Resource prototype.
|
|
668
|
+
* @type {{permittedParams?: (arg?: object) => Array<string | Record<string, ?>>}}
|
|
669
|
+
*/ (resourceClass.prototype)
|
|
670
|
+
|
|
671
|
+
if (typeof prototypeWithMethod?.permittedParams !== "function") return []
|
|
672
|
+
|
|
673
|
+
try {
|
|
674
|
+
const instance = new resourceClass({
|
|
675
|
+
ability: undefined,
|
|
676
|
+
context: {},
|
|
677
|
+
locals: {},
|
|
678
|
+
modelClass: resourceClass.ModelClass,
|
|
679
|
+
modelName: resourceClass.ModelClass?.getModelName?.() || resourceClass.name,
|
|
680
|
+
params: {},
|
|
681
|
+
resourceConfiguration: /**
|
|
682
|
+
* Resource configuration.
|
|
683
|
+
* @type {import("../../../../../configuration-types.js").FrontendModelResourceConfiguration}
|
|
684
|
+
*/ ({attributes: []})
|
|
685
|
+
})
|
|
686
|
+
const spec = instance.permittedParams({action, ability: undefined, locals: {}, params: {}})
|
|
687
|
+
|
|
688
|
+
return Array.isArray(spec) ? spec : []
|
|
689
|
+
} catch (error) {
|
|
690
|
+
throw new Error(`Failed to invoke ${resourceClass.name}.permittedParams() while generating frontend model write types: ${error instanceof Error ? error.message : String(error)}`, {cause: error})
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
525
694
|
/**
|
|
526
695
|
* Invokes a backend resource's `permittedParams()` instance method at
|
|
527
696
|
* generation time and extracts the relationship names that accept
|
|
@@ -465,22 +465,45 @@ function frontendModelAttachmentParams(params) {
|
|
|
465
465
|
/**
|
|
466
466
|
* Extract mutation attributes shared by create and update commands.
|
|
467
467
|
* @param {Record<string, ?>} params - Frontend-model request params.
|
|
468
|
-
* @returns {{attributes: Record<string, ?>, nestedAttributes: Record<string, ?> | null} | string} - Mutation attributes or validation error message.
|
|
468
|
+
* @returns {{attributes: Record<string, ?>, attachments: Record<string, ?> | null, nestedAttributes: Record<string, ?> | null} | string} - Mutation attributes or validation error message.
|
|
469
469
|
*/
|
|
470
470
|
function frontendModelMutationAttributes(params) {
|
|
471
471
|
const attributes = params.attributes
|
|
472
472
|
|
|
473
|
-
if (!attributes
|
|
473
|
+
if (!isPlainObject(attributes)) {
|
|
474
474
|
return "Expected model attributes."
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
+
/** @type {Record<string, ?>} */
|
|
478
|
+
const regularAttributes = {}
|
|
479
|
+
/** @type {Record<string, ?>} */
|
|
480
|
+
const nestedAttributes = {}
|
|
481
|
+
|
|
482
|
+
for (const [attributeName, value] of Object.entries(attributes)) {
|
|
483
|
+
if (attributeName.endsWith("Attributes")) {
|
|
484
|
+
const relationshipName = attributeName.slice(0, -"Attributes".length)
|
|
485
|
+
|
|
486
|
+
if (!relationshipName) return `Invalid nested attributes key: ${attributeName}`
|
|
487
|
+
nestedAttributes[relationshipName] = value
|
|
488
|
+
} else {
|
|
489
|
+
regularAttributes[attributeName] = value
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (params.nestedAttributes !== undefined) {
|
|
494
|
+
if (!isPlainObject(params.nestedAttributes)) return "Expected nestedAttributes to be an object."
|
|
495
|
+
|
|
496
|
+
Object.assign(nestedAttributes, params.nestedAttributes)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (params.attachments !== undefined && !isPlainObject(params.attachments)) {
|
|
500
|
+
return "Expected attachments to be an object."
|
|
501
|
+
}
|
|
502
|
+
|
|
477
503
|
return {
|
|
478
|
-
attributes,
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
* Types the following value.
|
|
482
|
-
@type {Record<string, ?>} */ (params.nestedAttributes)
|
|
483
|
-
: null
|
|
504
|
+
attributes: regularAttributes,
|
|
505
|
+
attachments: params.attachments === undefined ? null : params.attachments,
|
|
506
|
+
nestedAttributes: Object.keys(nestedAttributes).length > 0 ? nestedAttributes : null
|
|
484
507
|
}
|
|
485
508
|
}
|
|
486
509
|
|
|
@@ -1036,11 +1059,12 @@ export default class FrontendModelController extends Controller {
|
|
|
1036
1059
|
* Runs frontend model create record.
|
|
1037
1060
|
* @param {Record<string, ?>} attributes - Create attributes.
|
|
1038
1061
|
* @param {Record<string, ?> | null} [nestedAttributes] - Optional nested-attribute payload for cascading writes.
|
|
1062
|
+
* @param {Record<string, ?> | null} [attachments] - Optional attachment payloads keyed by attachment name.
|
|
1039
1063
|
* @returns {Promise<import("./database/record/index.js").default | null>} - Created model when authorized.
|
|
1040
1064
|
*/
|
|
1041
|
-
async frontendModelCreateRecord(attributes, nestedAttributes = null) {
|
|
1065
|
+
async frontendModelCreateRecord(attributes, nestedAttributes = null, attachments = null) {
|
|
1042
1066
|
const resource = this.frontendModelResourceInstance()
|
|
1043
|
-
const model = await resource.create(attributes, {nestedAttributes, controller: this})
|
|
1067
|
+
const model = await resource.create(attributes, {attachments, nestedAttributes, controller: this})
|
|
1044
1068
|
|
|
1045
1069
|
const authorizedModels = await this.frontendModelFilterAuthorizedModels({action: "create", models: [model]})
|
|
1046
1070
|
|
|
@@ -2943,7 +2967,11 @@ export default class FrontendModelController extends Controller {
|
|
|
2943
2967
|
const mutationAttributes = frontendModelMutationAttributes(params)
|
|
2944
2968
|
if (typeof mutationAttributes === "string") return this.frontendModelErrorPayload(mutationAttributes)
|
|
2945
2969
|
|
|
2946
|
-
const model = await this.frontendModelCreateRecord(
|
|
2970
|
+
const model = await this.frontendModelCreateRecord(
|
|
2971
|
+
mutationAttributes.attributes,
|
|
2972
|
+
mutationAttributes.nestedAttributes,
|
|
2973
|
+
mutationAttributes.attachments
|
|
2974
|
+
)
|
|
2947
2975
|
|
|
2948
2976
|
if (!model) {
|
|
2949
2977
|
return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
|
|
@@ -3056,7 +3084,11 @@ export default class FrontendModelController extends Controller {
|
|
|
3056
3084
|
return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
|
|
3057
3085
|
}
|
|
3058
3086
|
|
|
3059
|
-
const updatedModel = await resource.update(model, mutationAttributes.attributes, {
|
|
3087
|
+
const updatedModel = await resource.update(model, mutationAttributes.attributes, {
|
|
3088
|
+
attachments: mutationAttributes.attachments,
|
|
3089
|
+
controller: this,
|
|
3090
|
+
nestedAttributes: mutationAttributes.nestedAttributes
|
|
3091
|
+
})
|
|
3060
3092
|
const serializedModel = await resource.serialize(updatedModel, "update")
|
|
3061
3093
|
|
|
3062
3094
|
return frontendModelSerializedModelSuccess(serializedModel)
|