velocious 1.0.447 → 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 (69) hide show
  1. package/README.md +1 -1
  2. package/build/configuration-types.js +2 -0
  3. package/build/database/pool/async-tracked-multi-connection.js +123 -8
  4. package/build/database/pool/base.js +14 -1
  5. package/build/database/record/index.js +13 -8
  6. package/build/environment-handlers/node/cli/commands/generate/base-models.js +1 -16
  7. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +125 -73
  8. package/build/frontend-model-controller.js +9 -0
  9. package/build/frontend-model-resource/base-resource.js +266 -53
  10. package/build/frontend-models/base.js +241 -97
  11. package/build/frontend-models/preloader.js +3 -2
  12. package/build/src/background-jobs/job-record.d.ts +2 -1
  13. package/build/src/background-jobs/job-record.d.ts.map +1 -1
  14. package/build/src/configuration-types.d.ts +10 -0
  15. package/build/src/configuration-types.d.ts.map +1 -1
  16. package/build/src/configuration-types.js +3 -1
  17. package/build/src/database/pool/async-tracked-multi-connection.d.ts +60 -4
  18. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  19. package/build/src/database/pool/async-tracked-multi-connection.js +113 -9
  20. package/build/src/database/pool/base.d.ts +38 -1
  21. package/build/src/database/pool/base.d.ts.map +1 -1
  22. package/build/src/database/pool/base.js +14 -2
  23. package/build/src/database/query/preloader/belongs-to.d.ts +2 -2
  24. package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
  25. package/build/src/database/query/preloader/has-many.d.ts +1 -1
  26. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  27. package/build/src/database/query/preloader/has-one.d.ts +2 -2
  28. package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
  29. package/build/src/database/query/preloader.d.ts +1 -1
  30. package/build/src/database/query/preloader.d.ts.map +1 -1
  31. package/build/src/database/record/attachments/handle.d.ts +1 -1
  32. package/build/src/database/record/attachments/handle.d.ts.map +1 -1
  33. package/build/src/database/record/index.d.ts +23 -13
  34. package/build/src/database/record/index.d.ts.map +1 -1
  35. package/build/src/database/record/index.js +14 -9
  36. package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
  37. package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +2 -15
  38. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +89 -32
  39. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  40. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +123 -72
  41. package/build/src/frontend-model-controller.d.ts.map +1 -1
  42. package/build/src/frontend-model-controller.js +8 -1
  43. package/build/src/frontend-model-resource/base-resource.d.ts +203 -64
  44. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  45. package/build/src/frontend-model-resource/base-resource.js +237 -54
  46. package/build/src/frontend-models/base.d.ts +173 -110
  47. package/build/src/frontend-models/base.d.ts.map +1 -1
  48. package/build/src/frontend-models/base.js +218 -102
  49. package/build/src/frontend-models/preloader.d.ts.map +1 -1
  50. package/build/src/frontend-models/preloader.js +4 -3
  51. package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  52. package/build/src/testing/expect.d.ts +6 -0
  53. package/build/src/testing/expect.d.ts.map +1 -1
  54. package/build/src/testing/expect.js +9 -1
  55. package/build/testing/browser-frontend-model-event-hook-scenarios.js +1 -1
  56. package/build/testing/expect.js +9 -0
  57. package/package.json +1 -1
  58. package/src/configuration-types.js +2 -0
  59. package/src/database/pool/async-tracked-multi-connection.js +123 -8
  60. package/src/database/pool/base.js +14 -1
  61. package/src/database/record/index.js +13 -8
  62. package/src/environment-handlers/node/cli/commands/generate/base-models.js +1 -16
  63. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +125 -73
  64. package/src/frontend-model-controller.js +9 -0
  65. package/src/frontend-model-resource/base-resource.js +266 -53
  66. package/src/frontend-models/base.js +241 -97
  67. package/src/frontend-models/preloader.js +3 -2
  68. package/src/testing/browser-frontend-model-event-hook-scenarios.js +1 -1
  69. package/src/testing/expect.js +9 -0
@@ -222,6 +222,10 @@ class TenantDatabaseScopeError extends Error {
222
222
  }
223
223
  }
224
224
 
225
+ /**
226
+ * Base database record.
227
+ * @template {Record<string, ?>} [WriteAttributes=Record<string, ?>]
228
+ */
225
229
  class VelociousDatabaseRecord {
226
230
  /**
227
231
  * Narrows the runtime value to the documented type.
@@ -1150,17 +1154,18 @@ class VelociousDatabaseRecord {
1150
1154
 
1151
1155
  /**
1152
1156
  * Runs create.
1153
- * @template {typeof VelociousDatabaseRecord} MC
1154
- * @this {MC}
1155
- * @param {Record<string, ?>} [attributes] - Attributes.
1156
- * @returns {Promise<InstanceType<MC>>} - Resolves with the create.
1157
+ * @template {Record<string, ?>} CreateAttributes
1158
+ * @template {VelociousDatabaseRecord<CreateAttributes>} Model
1159
+ * @this {{new (changes?: CreateAttributes): Model} & typeof VelociousDatabaseRecord}
1160
+ * @param {CreateAttributes} [attributes] - Attributes.
1161
+ * @returns {Promise<Model>} - Resolves with the create.
1157
1162
  */
1158
1163
  static async create(attributes) {
1159
1164
  await this.ensureInitialized()
1160
1165
 
1161
1166
  const record = /**
1162
1167
  * Narrows the runtime value to the documented type.
1163
- @type {InstanceType<MC>} */ (new this(attributes))
1168
+ @type {Model} */ (new this(attributes))
1164
1169
 
1165
1170
  await record.save()
1166
1171
 
@@ -3353,9 +3358,9 @@ class VelociousDatabaseRecord {
3353
3358
 
3354
3359
  /**
3355
3360
  * Runs constructor.
3356
- * @param {Record<string, ?>} changes - Changes.
3361
+ * @param {WriteAttributes} changes - Changes.
3357
3362
  */
3358
- constructor(changes = {}) {
3363
+ constructor(changes = /** @type {WriteAttributes} */ ({})) {
3359
3364
  this.getModelClass()._assertHasBeenInitialized()
3360
3365
  this._attributes = {}
3361
3366
  this._changes = {}
@@ -4256,7 +4261,7 @@ class VelociousDatabaseRecord {
4256
4261
 
4257
4262
  /**
4258
4263
  * Assigns the attributes to the record and saves it.
4259
- * @param {object} attributesToAssign - The attributes to assign to the record.
4264
+ * @param {WriteAttributes} attributesToAssign - The attributes to assign to the record.
4260
4265
  */
4261
4266
  async update(attributesToAssign) {
4262
4267
  if (attributesToAssign) this.assign(attributesToAssign)
@@ -139,24 +139,9 @@ export default class DbGenerateModel extends BaseCommand {
139
139
 
140
140
  const hasManyRelationFilePath = `${velociousPath}/database/record/instance-relationships/has-many.js`
141
141
 
142
+ fileContent += `/** @augments {DatabaseRecord<${writeAttributeTypeName}>} */\n`
142
143
  fileContent += `export default class ${modelNameCamelized}Base extends DatabaseRecord {\n`
143
144
 
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
-
160
145
  // --- getModelClass() override (fixes polymorphic typing in JS/JSDoc) ---
161
146
  if (await fileExists(sourceModelFullFilePath)) {
162
147
  // Model file exists (e.g. src/models/ticket.js) → return typeof Ticket
@@ -4,6 +4,22 @@ import path from "node:path"
4
4
  import * as inflection from "inflection"
5
5
  import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcesForBackendProject} from "../../../../../frontend-models/resource-definition.js"
6
6
 
7
+ /**
8
+ * Attribute metadata used for generated frontend-model JSDoc.
9
+ * @typedef {object} FrontendAttributeConfig
10
+ * @property {string} [type] - Column type.
11
+ * @property {string} [columnType] - Column type.
12
+ * @property {string} [sqlType] - SQL type.
13
+ * @property {string} [dataType] - Data type.
14
+ * @property {string} [name] - Attribute name when configured as an array entry.
15
+ * @property {boolean} [null] - Whether null is allowed.
16
+ * @property {() => string} [getType] - Returns column type.
17
+ * @property {() => boolean} [getNull] - Returns whether null is allowed.
18
+ */
19
+ /**
20
+ * Permit spec returned by frontend-model resources during generation.
21
+ * @typedef {Array<string | Record<string, object>>} FrontendModelGeneratorPermitSpec
22
+ */
7
23
 
8
24
  /** Node CLI command that generates frontend model classes from backend project resource config. */
9
25
  export default class DbGenerateFrontendModels extends BaseCommand {
@@ -124,8 +140,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
124
140
  * @param {object} args - Arguments.
125
141
  * @param {Set<string>} args.availableFrontendModelClassNames - Available frontend model class names in backend project.
126
142
  * @param {string} args.className - Model class name.
127
- * @param {Record<string, ?>} args.modelConfig - Model configuration.
128
- * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass]
143
+ * @param {import("../../../../../configuration-types.js").NormalizedFrontendModelResourceConfiguration} args.modelConfig - Model configuration.
144
+ * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass] - Resource class.
129
145
  * @returns {void} - No return value.
130
146
  */
131
147
  validateModelConfig({availableFrontendModelClassNames, className, modelConfig, resourceClass}) {
@@ -135,11 +151,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
135
151
  throw new Error(`Model '${className}' is missing required 'abilities' config`)
136
152
  }
137
153
 
138
- const readActions = ["index", "find"]
139
-
140
- for (const action of readActions) {
141
- const abilityAction = abilities[action]
154
+ const readActions = [
155
+ {action: "index", abilityAction: abilities.index},
156
+ {action: "find", abilityAction: abilities.find}
157
+ ]
142
158
 
159
+ for (const {action, abilityAction} of readActions) {
143
160
  if (typeof abilityAction !== "string" || abilityAction.length < 1) {
144
161
  throw new Error(`Model '${className}' is missing required abilities.${action} config`)
145
162
  }
@@ -169,7 +186,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
169
186
 
170
187
  /**
171
188
  * Runs available frontend model class names.
172
- * @param {Record<string, ?>} resources - Resource configuration keyed by model name.
189
+ * @param {Record<string, import("../../../../../configuration-types.js").FrontendModelResourceDefinition>} resources - Resource configuration keyed by model name.
173
190
  * @returns {Set<string>} - Available frontend model class names.
174
191
  */
175
192
  availableFrontendModelClassNames(resources) {
@@ -217,8 +234,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
217
234
  * @param {string} args.className - Model class name.
218
235
  * @param {string} args.importPath - Base class import path.
219
236
  * @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
220
- * @param {Record<string, ?>} args.modelConfig - Model configuration.
221
- * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass]
237
+ * @param {import("../../../../../configuration-types.js").NormalizedFrontendModelResourceConfiguration} args.modelConfig - Model configuration.
238
+ * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass] - Resource class.
222
239
  * @returns {string} - Generated file content.
223
240
  */
224
241
  buildModelFileContent({className, importPath, modelClass, modelConfig, resourceClass}) {
@@ -234,6 +251,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
234
251
  const permittedCreateParams = this.permittedParamsForGenerator(resourceClass || null, "create")
235
252
  const permittedUpdateParams = this.permittedParamsForGenerator(resourceClass || null, "update")
236
253
  const nestedWriteTypes = this.nestedWriteTypesForModel({className, permittedParams: permittedCreateParams.concat(permittedUpdateParams), relationships})
254
+ const usesTransportValue = attributes.some((attribute) => attribute.jsDocType.includes("FrontendModelTransportValue"))
255
+ || nestedWriteTypes.some((nestedWriteType) => nestedWriteType.attributes.some((attribute) => attribute.type.includes("FrontendModelTransportValue")))
237
256
  const builtInCollectionCommands = {
238
257
  create: modelConfig.builtInCollectionCommands.create || "create",
239
258
  index: modelConfig.builtInCollectionCommands.index || "index"
@@ -265,6 +284,16 @@ export default class DbGenerateFrontendModels extends BaseCommand {
265
284
  fileContent += ` * Frontend model resource config.\n`
266
285
  fileContent += ` * @typedef {import("${importPath}").FrontendModelResourceConfig} FrontendModelResourceConfig\n`
267
286
  fileContent += " */\n"
287
+ fileContent += "/**\n"
288
+ fileContent += " * Fallback attribute value type for generated fields without narrower metadata.\n"
289
+ fileContent += ` * @typedef {import("${importPath}").FrontendModelAttributeValue} FrontendModelAttributeValue\n`
290
+ fileContent += " */\n"
291
+ if (usesTransportValue) {
292
+ fileContent += "/**\n"
293
+ fileContent += " * Value supported by frontend-model transport serialization and deserialization.\n"
294
+ fileContent += ` * @typedef {import("${importPath}").FrontendModelTransportValue} FrontendModelTransportValue\n`
295
+ fileContent += " */\n"
296
+ }
268
297
  fileContent += "\n"
269
298
  fileContent += "/**\n"
270
299
  fileContent += ` * ${attributesTypeName} type.\n`
@@ -284,21 +313,11 @@ export default class DbGenerateFrontendModels extends BaseCommand {
284
313
  }
285
314
  fileContent += this.writeAttributesTypedef({attributes, attributesTypeName, nestedWriteTypes, permittedParams: permittedCreateParams, typeName: createAttributesTypeName})
286
315
  fileContent += this.writeAttributesTypedef({attributes, attributesTypeName, nestedWriteTypes, permittedParams: permittedUpdateParams, typeName: updateAttributesTypeName})
287
- fileContent += `/** Frontend model for ${className}. */\n`
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`
316
+ fileContent += "/**\n"
317
+ fileContent += ` * Frontend model for ${className}.\n`
318
+ fileContent += ` * @augments {FrontendModelBase<${attributesTypeName}, ${createAttributesTypeName}, ${updateAttributesTypeName}>}\n`
319
+ fileContent += " */\n"
320
+ fileContent += `class ${className} extends FrontendModelBase {\n`
302
321
  fileContent += " /** @returns {FrontendModelResourceConfig} - Resource config. */\n"
303
322
  fileContent += " static resourceConfig() {\n"
304
323
  fileContent += " return {\n"
@@ -416,8 +435,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
416
435
  fileContent += "\n"
417
436
  fileContent += " /**\n"
418
437
  fileContent += ` * Runs ${methodName}.\n`
419
- fileContent += " * @param {...?} commandArguments - Custom command arguments.\n"
420
- fileContent += " * @returns {Promise<Record<string, ?>>} - Command response.\n"
438
+ fileContent += " * @param {...FrontendModelAttributeValue} commandArguments - Custom command arguments.\n"
439
+ fileContent += " * @returns {Promise<Record<string, FrontendModelAttributeValue>>} - Command response.\n"
421
440
  fileContent += " */\n"
422
441
  fileContent += ` static async ${methodName}(...commandArguments) {\n`
423
442
  fileContent += " return await this.executeCustomCommand({\n"
@@ -433,8 +452,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
433
452
  fileContent += "\n"
434
453
  fileContent += " /**\n"
435
454
  fileContent += ` * Runs ${methodName}.\n`
436
- fileContent += " * @param {...?} commandArguments - Custom command arguments.\n"
437
- fileContent += " * @returns {Promise<Record<string, ?>>} - Command response.\n"
455
+ fileContent += " * @param {...FrontendModelAttributeValue} commandArguments - Custom command arguments.\n"
456
+ fileContent += " * @returns {Promise<Record<string, FrontendModelAttributeValue>>} - Command response.\n"
438
457
  fileContent += " */\n"
439
458
  fileContent += ` async ${methodName}(...commandArguments) {\n`
440
459
  fileContent += ` return await ${className}.executeCustomCommand({\n`
@@ -450,71 +469,91 @@ export default class DbGenerateFrontendModels extends BaseCommand {
450
469
  for (const relationship of relationships) {
451
470
  const relationshipNameCamelized = inflection.camelize(relationship.relationshipName)
452
471
  const targetImportPath = `./${relationship.targetFileName}.js`
472
+ const targetInstanceType = `import(${JSON.stringify(targetImportPath)}).${relationship.targetClassName}`
473
+ const targetCreateAttributesType = `import(${JSON.stringify(targetImportPath)}).${relationship.targetClassName}CreateAttributes`
453
474
 
454
475
  if (relationship.type == "hasMany") {
476
+ fileContent += "\n"
477
+ fileContent += " /**\n"
478
+ fileContent += ` * Returns ${relationship.relationshipName} relationship helper.\n`
479
+ fileContent += ` * @returns {import(${JSON.stringify(importPath)}).FrontendModelHasManyRelationship<${className}, ${targetInstanceType}, ${targetCreateAttributesType}>} - Relationship helper.\n`
480
+ fileContent += " */\n"
481
+ fileContent += ` ${relationship.relationshipName}Relationship() { return /** @type {import(${JSON.stringify(importPath)}).FrontendModelHasManyRelationship<${className}, ${targetInstanceType}, ${targetCreateAttributesType}>} */ (this.getRelationshipByName(${JSON.stringify(relationship.relationshipName)})) }\n`
482
+
455
483
  fileContent += "\n"
456
484
  fileContent += " /**\n"
457
485
  fileContent += ` * Returns ${relationship.relationshipName}.\n`
458
- fileContent += ` * @returns {import(${JSON.stringify(importPath)}).FrontendModelHasManyRelationship<typeof import(${JSON.stringify(`./${inflection.dasherize(inflection.underscore(className))}.js`)}).default, typeof import(${JSON.stringify(targetImportPath)}).default>} - Relationship helper.\n`
486
+ fileContent += ` * @returns {import(${JSON.stringify(importPath)}).FrontendModelHasManyRelationship<${className}, ${targetInstanceType}, ${targetCreateAttributesType}>} - Relationship helper.\n`
459
487
  fileContent += " */\n"
460
- fileContent += ` ${relationship.relationshipName}() { return /** @type {import(${JSON.stringify(importPath)}).FrontendModelHasManyRelationship<typeof import(${JSON.stringify(`./${inflection.dasherize(inflection.underscore(className))}.js`)}).default, typeof import(${JSON.stringify(targetImportPath)}).default>} */ (this.getRelationshipByName(${JSON.stringify(relationship.relationshipName)})) }\n`
488
+ fileContent += ` ${relationship.relationshipName}() { return this.${relationship.relationshipName}Relationship() }\n`
461
489
 
462
490
  fileContent += "\n"
463
491
  fileContent += " /**\n"
464
492
  fileContent += ` * Returns loaded ${relationship.relationshipName}.\n`
465
- fileContent += ` * @returns {Array<import(${JSON.stringify(targetImportPath)}).default>} - Loaded related models.\n`
493
+ fileContent += ` * @returns {Array<${targetInstanceType}>} - Loaded related models.\n`
466
494
  fileContent += " */\n"
467
- fileContent += ` ${relationship.relationshipName}Loaded() { return /** @type {Array<import(${JSON.stringify(targetImportPath)}).default>} */ (this.getRelationshipByName(${JSON.stringify(relationship.relationshipName)}).loaded()) }\n`
495
+ fileContent += ` ${relationship.relationshipName}Loaded() { return this.${relationship.relationshipName}Relationship().loaded() }\n`
468
496
 
469
497
  fileContent += "\n"
470
498
  fileContent += " /**\n"
471
499
  fileContent += ` * Loads ${relationship.relationshipName}.\n`
472
- fileContent += ` * @returns {Promise<Array<import(${JSON.stringify(targetImportPath)}).default>>} - Loaded related models.\n`
500
+ fileContent += ` * @returns {Promise<Array<${targetInstanceType}>>} - Loaded related models.\n`
473
501
  fileContent += " */\n"
474
- fileContent += ` async load${relationshipNameCamelized}() { return /** @type {Promise<Array<import(${JSON.stringify(targetImportPath)}).default>>} */ (this.loadRelationship(${JSON.stringify(relationship.relationshipName)})) }\n`
502
+ fileContent += ` async load${relationshipNameCamelized}() { return await this.${relationship.relationshipName}Relationship().load() }\n`
475
503
  } else {
504
+ fileContent += "\n"
505
+ fileContent += " /**\n"
506
+ fileContent += ` * Returns ${relationship.relationshipName} relationship helper.\n`
507
+ fileContent += ` * @returns {import(${JSON.stringify(importPath)}).FrontendModelSingularRelationship<${className}, ${targetInstanceType}, ${targetCreateAttributesType}>} - Relationship helper.\n`
508
+ fileContent += " */\n"
509
+ fileContent += ` ${relationship.relationshipName}Relationship() { return /** @type {import(${JSON.stringify(importPath)}).FrontendModelSingularRelationship<${className}, ${targetInstanceType}, ${targetCreateAttributesType}>} */ (this.getRelationshipByName(${JSON.stringify(relationship.relationshipName)})) }\n`
510
+
476
511
  fileContent += "\n"
477
512
  fileContent += " /**\n"
478
513
  fileContent += ` * Returns ${relationship.relationshipName}.\n`
479
- fileContent += ` * @returns {import(${JSON.stringify(targetImportPath)}).default | null} - Loaded related model.\n`
514
+ fileContent += ` * @returns {${targetInstanceType} | null} - Loaded related model.\n`
480
515
  fileContent += " */\n"
481
- fileContent += ` ${relationship.relationshipName}() { return /** @type {import(${JSON.stringify(targetImportPath)}).default | null} */ (this.getRelationshipByName(${JSON.stringify(relationship.relationshipName)}).loaded()) }\n`
516
+ fileContent += ` ${relationship.relationshipName}() { return this.${relationship.relationshipName}Relationship().loaded() }\n`
482
517
 
483
518
  fileContent += "\n"
484
519
  fileContent += " /**\n"
485
520
  fileContent += ` * Builds ${relationship.relationshipName}.\n`
486
- fileContent += ` * @param {Record<string, ?>} [attributes] - Attributes for the new related model.\n`
487
- fileContent += ` * @returns {import(${JSON.stringify(targetImportPath)}).default} - Built related model.\n`
521
+ fileContent += ` * @param {${targetCreateAttributesType}} [attributes] - Attributes for the new related model.\n`
522
+ fileContent += ` * @returns {${targetInstanceType}} - Built related model.\n`
488
523
  fileContent += " */\n"
489
- fileContent += ` build${relationshipNameCamelized}(attributes = {}) { return /** @type {import(${JSON.stringify(targetImportPath)}).default} */ (this.getRelationshipByName(${JSON.stringify(relationship.relationshipName)}).build(attributes)) }\n`
524
+ fileContent += ` build${relationshipNameCamelized}(attributes = {}) { return this.${relationship.relationshipName}Relationship().build(attributes) }\n`
490
525
 
491
526
  fileContent += "\n"
492
527
  fileContent += " /**\n"
493
528
  fileContent += ` * Loads ${relationship.relationshipName}.\n`
494
- fileContent += ` * @returns {Promise<import(${JSON.stringify(targetImportPath)}).default | null>} - Loaded related model.\n`
529
+ fileContent += ` * @returns {Promise<${targetInstanceType} | null>} - Loaded related model.\n`
495
530
  fileContent += " */\n"
496
- fileContent += ` async load${relationshipNameCamelized}() { return /** @type {Promise<import(${JSON.stringify(targetImportPath)}).default | null>} */ (this.loadRelationship(${JSON.stringify(relationship.relationshipName)})) }\n`
531
+ fileContent += ` async load${relationshipNameCamelized}() { return await this.${relationship.relationshipName}Relationship().load() }\n`
497
532
 
498
533
  fileContent += "\n"
499
534
  fileContent += " /**\n"
500
535
  fileContent += ` * Returns or loads ${relationship.relationshipName}.\n`
501
- fileContent += ` * @returns {Promise<import(${JSON.stringify(targetImportPath)}).default | null>} - Loaded related model.\n`
536
+ fileContent += ` * @returns {Promise<${targetInstanceType} | null>} - Loaded related model.\n`
502
537
  fileContent += " */\n"
503
- fileContent += ` async ${relationship.relationshipName}OrLoad() { return /** @type {Promise<import(${JSON.stringify(targetImportPath)}).default | null>} */ (this.relationshipOrLoad(${JSON.stringify(relationship.relationshipName)})) }\n`
538
+ fileContent += ` async ${relationship.relationshipName}OrLoad() { return await this.${relationship.relationshipName}Relationship().orLoad() }\n`
504
539
 
505
540
  fileContent += "\n"
506
541
  fileContent += " /**\n"
507
542
  fileContent += ` * Sets ${relationship.relationshipName}.\n`
508
- fileContent += ` * @param {import(${JSON.stringify(targetImportPath)}).default | null} model - Related model.\n`
509
- fileContent += ` * @returns {import(${JSON.stringify(targetImportPath)}).default | null} - Assigned related model.\n`
543
+ fileContent += ` * @param {${targetInstanceType} | null} model - Related model.\n`
544
+ fileContent += " * @returns {void}\n"
510
545
  fileContent += " */\n"
511
- fileContent += ` set${relationshipNameCamelized}(model) { return /** @type {import(${JSON.stringify(targetImportPath)}).default | null} */ (this.setRelationship(${JSON.stringify(relationship.relationshipName)}, model)) }\n`
546
+ fileContent += ` set${relationshipNameCamelized}(model) { this.${relationship.relationshipName}Relationship().setLoaded(model) }\n`
512
547
  }
513
548
  }
514
549
 
515
550
  fileContent += "}\n"
516
551
  fileContent += "\n"
517
552
  fileContent += `FrontendModelBase.registerModel(${className})\n`
553
+ fileContent += "\n"
554
+ fileContent += `export {${className}}\n`
555
+ fileContent += "\n"
556
+ fileContent += `export default /** @type {import(${JSON.stringify(importPath)}).FrontendModelClass<${className}, ${attributesTypeName}, ${createAttributesTypeName}>} */ (${className})\n`
518
557
 
519
558
  return fileContent
520
559
  }
@@ -557,7 +596,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
557
596
  * @param {Array<{jsDocType: string, name: string}>} args.attributes - Generated read attributes.
558
597
  * @param {string} args.attributesTypeName - Generated read attributes typedef name.
559
598
  * @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.
599
+ * @param {Array<string | Record<string, object>>} args.permittedParams - Resource permitted params spec.
561
600
  * @param {string} args.typeName - Typedef name.
562
601
  * @returns {string} - Generated typedef source.
563
602
  */
@@ -573,13 +612,13 @@ export default class DbGenerateFrontendModels extends BaseCommand {
573
612
  for (const entry of permittedParams) {
574
613
  if (typeof entry == "string") {
575
614
  const attribute = attributesByName.get(entry)
576
- const type = attribute ? `${attributesTypeName}[${JSON.stringify(attribute.name)}]` : "?"
615
+ const type = attribute ? `${attributesTypeName}[${JSON.stringify(attribute.name)}]` : "FrontendModelAttributeValue"
577
616
 
578
617
  output += ` * @property {${type}} [${entry}] - Permitted ${entry} value.\n`
579
618
  } else if (entry && typeof entry == "object" && !Array.isArray(entry)) {
580
619
  for (const key of Object.keys(entry)) {
581
620
  const nestedWriteType = nestedWriteTypesByKey.get(key)
582
- const type = nestedWriteType ? `Array<${nestedWriteType.typeName}>` : "Array<Record<string, ?>>"
621
+ const type = nestedWriteType ? `Array<${nestedWriteType.typeName}>` : "Array<object>"
583
622
 
584
623
  output += ` * @property {${type}} [${key}] - Permitted nested ${key} values.\n`
585
624
  }
@@ -595,7 +634,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
595
634
  * Runs nested write types for model.
596
635
  * @param {object} args - Arguments.
597
636
  * @param {string} args.className - Frontend model class name.
598
- * @param {Array<string | Record<string, ?>>} args.permittedParams - Combined permitted params specs.
637
+ * @param {FrontendModelGeneratorPermitSpec} args.permittedParams - Combined permitted params specs.
599
638
  * @param {Array<{autoload: boolean, relationshipName: string, targetClassName: string, targetFileName: string, type: "belongsTo" | "hasOne" | "hasMany"}>} args.relationships - Generated relationships.
600
639
  * @returns {Array<{attributes: Array<{name: string, type: string}>, relationshipName: string, typeName: string}>} - Nested write typedefs.
601
640
  */
@@ -637,7 +676,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
637
676
  /**
638
677
  * Runs nested write attributes for spec.
639
678
  * @param {object} args - Arguments.
640
- * @param {?} args.nestedSpec - Nested permit spec.
679
+ * @param {Array<string | Record<string, object>> | object | string | null | undefined} args.nestedSpec - Nested permit spec.
641
680
  * @param {typeof import("../../../../../database/record/index.js").default | undefined} args.targetModelClass - Target backend model class.
642
681
  * @returns {Array<{name: string, type: string}>} - Nested write attributes.
643
682
  */
@@ -649,7 +688,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
649
688
 
650
689
  return {
651
690
  name: attributeName,
652
- type: attributeConfig ? this.jsDocTypeForFrontendAttribute({attributeConfig}) : "?"
691
+ type: attributeConfig ? this.jsDocTypeForFrontendAttribute({attributeConfig}) : "FrontendModelAttributeValue"
653
692
  }
654
693
  })
655
694
  }
@@ -658,14 +697,14 @@ export default class DbGenerateFrontendModels extends BaseCommand {
658
697
  * Runs permitted params for generator.
659
698
  * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} resourceClass - Resource class.
660
699
  * @param {"create" | "update"} action - Write action.
661
- * @returns {Array<string | Record<string, ?>>} - Permitted params spec.
700
+ * @returns {FrontendModelGeneratorPermitSpec} - Permitted params spec.
662
701
  */
663
702
  permittedParamsForGenerator(resourceClass, action) {
664
703
  if (!resourceClass || typeof resourceClass !== "function") return []
665
704
 
666
705
  const prototypeWithMethod = /**
667
706
  * Resource prototype.
668
- * @type {{permittedParams?: (arg?: object) => Array<string | Record<string, ?>>}}
707
+ * @type {{permittedParams?: (arg?: object) => FrontendModelGeneratorPermitSpec}}
669
708
  */ (resourceClass.prototype)
670
709
 
671
710
  if (typeof prototypeWithMethod?.permittedParams !== "function") return []
@@ -708,7 +747,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
708
747
 
709
748
  const prototypeWithMethod = /**
710
749
  * Resource prototype.
711
- * @type {{permittedParams?: (arg?: object) => Array<string | Record<string, ?>>}}
750
+ * @type {{permittedParams?: (arg?: object) => FrontendModelGeneratorPermitSpec}}
712
751
  */ (resourceClass.prototype)
713
752
 
714
753
  if (typeof prototypeWithMethod?.permittedParams !== "function") return []
@@ -818,7 +857,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
818
857
  * Runs attribute definitions for model.
819
858
  * @param {object} args - Arguments.
820
859
  * @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
821
- * @param {Record<string, ?>} args.modelConfig - Model configuration.
860
+ * @param {import("../../../../../configuration-types.js").NormalizedFrontendModelResourceConfiguration} args.modelConfig - Model configuration.
822
861
  * @returns {Array<{jsDocType: string, name: string}>} - Attribute definitions.
823
862
  */
824
863
  attributeDefinitionsForModel({modelClass, modelConfig}) {
@@ -838,13 +877,25 @@ export default class DbGenerateFrontendModels extends BaseCommand {
838
877
  }
839
878
 
840
879
  if (Array.isArray(attributes)) {
841
- return attributes.map((entry) => {
842
- const attributeName = typeof entry === "string" ? entry : entry.name
880
+ return attributes.map((attributeDefinition) => {
881
+ /** @type {FrontendAttributeConfig | null} */
882
+ let attributeConfig = null
883
+ let attributeName
884
+
885
+ if (typeof attributeDefinition == "string") {
886
+ attributeName = attributeDefinition
887
+ attributeConfig = this.frontendAttributeConfigForModelAttribute({attributeName, modelClass})
888
+ } else if (attributeDefinition && typeof attributeDefinition == "object" && !Array.isArray(attributeDefinition)) {
889
+ attributeConfig = /** @type {FrontendAttributeConfig} */ (attributeDefinition)
890
+ attributeName = attributeConfig.name
891
+ }
892
+
893
+ if (typeof attributeName != "string" || attributeName.length < 1) {
894
+ throw new Error(`Expected frontend model attribute array entries to be strings or objects with a name, got: ${JSON.stringify(attributeDefinition)}`)
895
+ }
843
896
 
844
897
  return {
845
- jsDocType: this.jsDocTypeForFrontendAttribute({
846
- attributeConfig: this.frontendAttributeConfigForModelAttribute({attributeName, modelClass})
847
- }),
898
+ jsDocType: this.jsDocTypeForFrontendAttribute({attributeConfig}),
848
899
  name: attributeName
849
900
  }
850
901
  })
@@ -856,9 +907,10 @@ export default class DbGenerateFrontendModels extends BaseCommand {
856
907
 
857
908
  return Object.keys(attributes).map((attributeName) => {
858
909
  const attributeConfig = attributes[attributeName]
910
+ const normalizedAttributeConfig = attributeConfig && typeof attributeConfig === "object" ? attributeConfig : null
859
911
 
860
912
  return {
861
- jsDocType: this.jsDocTypeForFrontendAttribute({attributeConfig}),
913
+ jsDocType: this.jsDocTypeForFrontendAttribute({attributeConfig: normalizedAttributeConfig}),
862
914
  name: attributeName
863
915
  }
864
916
  })
@@ -867,7 +919,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
867
919
  /**
868
920
  * Runs js doc type for frontend attribute.
869
921
  * @param {object} args - Arguments.
870
- * @param {?} args.attributeConfig - Attribute configuration value.
922
+ * @param {FrontendAttributeConfig | null | undefined} args.attributeConfig - Attribute configuration value.
871
923
  * @returns {string} - JSDoc type.
872
924
  */
873
925
  jsDocTypeForFrontendAttribute({attributeConfig}) {
@@ -882,12 +934,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
882
934
 
883
935
  /**
884
936
  * Runs js doc type for frontend attribute base type.
885
- * @param {?} attributeConfig - Attribute configuration value.
937
+ * @param {FrontendAttributeConfig | null | undefined} attributeConfig - Attribute configuration value.
886
938
  * @returns {string} - Non-nullable JSDoc type.
887
939
  */
888
940
  jsDocTypeForFrontendAttributeBaseType(attributeConfig) {
889
941
  if (!attributeConfig || typeof attributeConfig !== "object") {
890
- return "any"
942
+ return "FrontendModelAttributeValue"
891
943
  }
892
944
 
893
945
  const type = this.frontendAttributeTypeValue(attributeConfig)
@@ -895,7 +947,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
895
947
  if (type == "boolean") {
896
948
  return "boolean"
897
949
  } else if (type == "json" || type == "jsonb") {
898
- return "Record<string, any>"
950
+ return "FrontendModelTransportValue"
899
951
  } else if (type && ["blob", "char", "nvarchar", "varchar", "text", "longtext", "uuid", "character varying"].includes(type)) {
900
952
  return "string"
901
953
  } else if (type && ["bit", "bigint", "decimal", "double", "double precision", "float", "int", "integer", "numeric", "real", "smallint", "tinyint"].includes(type)) {
@@ -903,13 +955,13 @@ export default class DbGenerateFrontendModels extends BaseCommand {
903
955
  } else if (type && ["date", "datetime", "timestamp", "timestamp without time zone", "timestamptz"].includes(type)) {
904
956
  return "Date"
905
957
  } else {
906
- return "any"
958
+ return "FrontendModelAttributeValue"
907
959
  }
908
960
  }
909
961
 
910
962
  /**
911
963
  * Runs frontend attribute can be null.
912
- * @param {?} attributeConfig - Attribute configuration value.
964
+ * @param {FrontendAttributeConfig | null | undefined} attributeConfig - Attribute configuration value.
913
965
  * @returns {boolean} - Whether the attribute allows null values.
914
966
  */
915
967
  frontendAttributeCanBeNull(attributeConfig) {
@@ -926,7 +978,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
926
978
 
927
979
  /**
928
980
  * Runs frontend attribute type value.
929
- * @param {?} attributeConfig - Attribute configuration value.
981
+ * @param {FrontendAttributeConfig | null | undefined} attributeConfig - Attribute configuration value.
930
982
  * @returns {string | null} - Normalized column type.
931
983
  */
932
984
  frontendAttributeTypeValue(attributeConfig) {
@@ -952,7 +1004,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
952
1004
  * @param {object} args - Arguments.
953
1005
  * @param {string} args.attributeName - Frontend model attribute name.
954
1006
  * @param {typeof import("../../../../../database/record/index.js").default | undefined} args.modelClass - Backend model class.
955
- * @returns {?} - Attribute config inferred from the backend model when available.
1007
+ * @returns {FrontendAttributeConfig | null} - Attribute config inferred from the backend model when available.
956
1008
  */
957
1009
  frontendAttributeConfigForModelAttribute({attributeName, modelClass}) {
958
1010
  if (!modelClass) {
@@ -972,8 +1024,8 @@ export default class DbGenerateFrontendModels extends BaseCommand {
972
1024
  * Runs relationships for model.
973
1025
  * @param {object} args - Arguments.
974
1026
  * @param {string} args.className - Model class name.
975
- * @param {Record<string, ?>} args.modelConfig - Model configuration.
976
- * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass]
1027
+ * @param {import("../../../../../configuration-types.js").NormalizedFrontendModelResourceConfiguration} args.modelConfig - Model configuration.
1028
+ * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass] - Resource class.
977
1029
  * @returns {Array<{autoload: boolean, relationshipName: string, targetClassName: string, targetFileName: string, type: "belongsTo" | "hasOne" | "hasMany"}>} - Relationships.
978
1030
  */
979
1031
  relationshipsForModel({className, modelConfig, resourceClass}) {
@@ -995,7 +1047,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
995
1047
  * @param {object} args - Arguments.
996
1048
  * @param {string} args.className - Model class name.
997
1049
  * @param {string} args.relationshipName - Relationship name.
998
- * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass]
1050
+ * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null} [args.resourceClass] - Resource class.
999
1051
  * @returns {{autoload: boolean, relationshipName: string, targetClassName: string, targetFileName: string, type: "belongsTo" | "hasOne" | "hasMany"}} Inferred relationship definition.
1000
1052
  */
1001
1053
  inferredRelationshipDefinition({className, relationshipName, resourceClass}) {
@@ -2605,8 +2605,17 @@ export default class FrontendModelController extends Controller {
2605
2605
  * Types the following value.
2606
2606
  @type {typeof import("./database/record/index.js").default} */ (model.constructor)
2607
2607
  const relationshipsMap = modelClass.getRelationshipsMap()
2608
+ const resource = this._serializationResourceInstanceForModel(model)
2609
+ const resourceConfiguration = resource ? resource.resourceConfiguration() : null
2610
+ const exposedRelationships = new Set(
2611
+ resourceConfiguration && Array.isArray(resourceConfiguration.relationships)
2612
+ ? resourceConfiguration.relationships
2613
+ : []
2614
+ )
2608
2615
 
2609
2616
  for (const relationshipName in relationshipsMap) {
2617
+ if (!exposedRelationships.has(relationshipName)) continue
2618
+
2610
2619
  const relationship = model.getRelationshipByName(relationshipName)
2611
2620
 
2612
2621
  if (!relationship.getPreloaded()) continue