velocious 1.0.454 → 1.0.455

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 (85) hide show
  1. package/README.md +2 -1
  2. package/build/authorization/ability.js +3 -6
  3. package/build/authorization/base-resource.js +7 -9
  4. package/build/configuration-types.js +3 -3
  5. package/build/configuration.js +12 -17
  6. package/build/database/drivers/base.js +3 -3
  7. package/build/database/pool/base.js +2 -1
  8. package/build/database/query/preloader/ensure-model-class-initialized.js +1 -6
  9. package/build/database/record/attachments/handle.js +32 -0
  10. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +25 -26
  11. package/build/frontend-model-controller.js +167 -88
  12. package/build/frontend-model-resource/base-resource.js +133 -31
  13. package/build/frontend-models/base.js +30 -6
  14. package/build/frontend-models/model-registry.js +1 -1
  15. package/build/frontend-models/query.js +3 -9
  16. package/build/frontend-models/resource-definition.js +1 -0
  17. package/build/frontend-models/transport-serialization.js +2 -3
  18. package/build/frontend-models/websocket-channel.js +7 -12
  19. package/build/frontend-models/websocket-publishers.js +11 -67
  20. package/build/routes/hooks/frontend-model-command-route-hook.js +1 -1
  21. package/build/src/authorization/ability.d.ts.map +1 -1
  22. package/build/src/authorization/ability.js +4 -8
  23. package/build/src/authorization/base-resource.d.ts +2 -2
  24. package/build/src/authorization/base-resource.d.ts.map +1 -1
  25. package/build/src/authorization/base-resource.js +7 -9
  26. package/build/src/configuration-types.d.ts +6 -6
  27. package/build/src/configuration-types.d.ts.map +1 -1
  28. package/build/src/configuration-types.js +4 -4
  29. package/build/src/configuration.d.ts.map +1 -1
  30. package/build/src/configuration.js +13 -18
  31. package/build/src/database/drivers/base.js +4 -4
  32. package/build/src/database/pool/base.d.ts +2 -1
  33. package/build/src/database/pool/base.d.ts.map +1 -1
  34. package/build/src/database/pool/base.js +3 -2
  35. package/build/src/database/query/preloader/ensure-model-class-initialized.d.ts.map +1 -1
  36. package/build/src/database/query/preloader/ensure-model-class-initialized.js +2 -6
  37. package/build/src/database/record/attachments/handle.d.ts +13 -0
  38. package/build/src/database/record/attachments/handle.d.ts.map +1 -1
  39. package/build/src/database/record/attachments/handle.js +29 -1
  40. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +6 -0
  41. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  42. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +25 -24
  43. package/build/src/frontend-model-controller.d.ts +70 -26
  44. package/build/src/frontend-model-controller.d.ts.map +1 -1
  45. package/build/src/frontend-model-controller.js +144 -87
  46. package/build/src/frontend-model-resource/base-resource.d.ts +192 -16
  47. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  48. package/build/src/frontend-model-resource/base-resource.js +124 -33
  49. package/build/src/frontend-models/base.d.ts +14 -1
  50. package/build/src/frontend-models/base.d.ts.map +1 -1
  51. package/build/src/frontend-models/base.js +28 -7
  52. package/build/src/frontend-models/model-registry.js +2 -2
  53. package/build/src/frontend-models/query.d.ts.map +1 -1
  54. package/build/src/frontend-models/query.js +4 -10
  55. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  56. package/build/src/frontend-models/resource-definition.js +2 -1
  57. package/build/src/frontend-models/transport-serialization.d.ts +2 -4
  58. package/build/src/frontend-models/transport-serialization.d.ts.map +1 -1
  59. package/build/src/frontend-models/transport-serialization.js +3 -4
  60. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  61. package/build/src/frontend-models/websocket-channel.js +8 -13
  62. package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
  63. package/build/src/frontend-models/websocket-publishers.js +12 -60
  64. package/build/src/routes/hooks/frontend-model-command-route-hook.js +2 -2
  65. package/package.json +1 -1
  66. package/scripts/test-browser.js +2 -2
  67. package/src/authorization/ability.js +3 -6
  68. package/src/authorization/base-resource.js +7 -9
  69. package/src/configuration-types.js +3 -3
  70. package/src/configuration.js +12 -17
  71. package/src/database/drivers/base.js +3 -3
  72. package/src/database/pool/base.js +2 -1
  73. package/src/database/query/preloader/ensure-model-class-initialized.js +1 -6
  74. package/src/database/record/attachments/handle.js +32 -0
  75. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +25 -26
  76. package/src/frontend-model-controller.js +167 -88
  77. package/src/frontend-model-resource/base-resource.js +133 -31
  78. package/src/frontend-models/base.js +30 -6
  79. package/src/frontend-models/model-registry.js +1 -1
  80. package/src/frontend-models/query.js +3 -9
  81. package/src/frontend-models/resource-definition.js +1 -0
  82. package/src/frontend-models/transport-serialization.js +2 -3
  83. package/src/frontend-models/websocket-channel.js +7 -12
  84. package/src/frontend-models/websocket-publishers.js +11 -67
  85. package/src/routes/hooks/frontend-model-command-route-hook.js +1 -1
package/README.md CHANGED
@@ -376,6 +376,7 @@ export default new Configuration({
376
376
  ```
377
377
 
378
378
  `frontendModels` entries must be `FrontendModelBaseResource` subclasses. Built-in CRUD/find/index/serialize behavior lives in the base class, and app resources override only the pieces they actually need.
379
+ Resource-level index customization should prefer `indexQuery()` or the pagination/search/sort hooks over replacing `records()`, so built-in pluck and aggregate count support can keep using the same query. See [`docs/frontend-model-resources.md`](docs/frontend-model-resources.md) for the resource extension points.
379
380
 
380
381
  Resources expose the full CRUD ability set (`create`, `destroy`, `read`, `update`) by default. To restrict the API surface — for example to a read-only resource — declare an explicit subset:
381
382
 
@@ -446,7 +447,7 @@ Frontend-model `group(...)` is attribute/path based and does not accept raw SQL
446
447
  Frontend-model `where(...)` supports nested relationship descriptors (for example `Task.where({project: {creatingUser: {reference: "owner-b"}}})`) and does not accept raw SQL fragments.
447
448
  Frontend-model `joins(...)` supports relationship-object descriptors only (for example `Task.joins({project: {creatingUser: true}})`) and rejects raw SQL join strings.
448
449
  Frontend-model `distinct(...)` only accepts booleans (`true` by default) and is applied server-side through the backend query API.
449
- Frontend-model `pluck(...)` validates attribute/path descriptors against configured model metadata and does not accept SQL fragments.
450
+ Frontend-model `pluck(...)` validates attribute/path descriptors against configured resource/model metadata and does not accept SQL fragments or hidden raw model columns when the resource declares an explicit attribute list.
450
451
  Frontend-model query fields are limited to attributes exposed by the backend resource. Use `{name: "attributeName", selectedByDefault: false}` for fields that may be selected or filtered explicitly but should stay out of default payloads.
451
452
 
452
453
  When backend payloads include `__preloadedRelationships`, nested frontend-model relationships are hydrated recursively. Relationship methods can use `getRelationshipByName("relationship").loaded()` and will throw when a relationship was not preloaded.
@@ -72,7 +72,7 @@ export default class VelociousAuthorizationAbility {
72
72
  _resolveResourcesFromConfiguration() {
73
73
  const configuration = this.context?.configuration
74
74
 
75
- if (!configuration || typeof configuration.getBackendProjects !== "function") {
75
+ if (!configuration) {
76
76
  return []
77
77
  }
78
78
 
@@ -85,12 +85,10 @@ export default class VelociousAuthorizationAbility {
85
85
  for (const backendProject of backendProjects) {
86
86
  const frontendModels = backendProject.frontendModels
87
87
 
88
- if (!frontendModels || typeof frontendModels !== "object") continue
88
+ if (!frontendModels) continue
89
89
 
90
90
  for (const resourceDefinition of Object.values(frontendModels)) {
91
- if (typeof resourceDefinition === "function" && typeof resourceDefinition.modelClass === "function") {
92
- resolved.push(resourceDefinition)
93
- }
91
+ resolved.push(resourceDefinition)
94
92
  }
95
93
  }
96
94
 
@@ -173,7 +171,6 @@ export default class VelociousAuthorizationAbility {
173
171
  for (const ResourceClass of this.resources) {
174
172
  const resourceModelClass = ResourceClass.modelClass()
175
173
 
176
- if (!resourceModelClass) continue
177
174
  if (resourceModelClass !== modelClass) continue
178
175
 
179
176
  const resourceInstance = new ResourceClass({
@@ -22,9 +22,13 @@ export default class AuthorizationBaseResource {
22
22
 
23
23
  /**
24
24
  * Runs model class.
25
- * @returns {typeof import("../database/record/index.js").default | undefined} - Model class handled by this resource.
25
+ * @returns {typeof import("../database/record/index.js").default} - Model class handled by this resource.
26
26
  */
27
27
  static modelClass() {
28
+ if (!this.ModelClass) {
29
+ throw new Error(`${this.name} must define static ModelClass before calling ability helpers.`)
30
+ }
31
+
28
32
  return this.ModelClass
29
33
  }
30
34
 
@@ -69,15 +73,9 @@ export default class AuthorizationBaseResource {
69
73
  * @returns {typeof import("../database/record/index.js").default} - Model class handled by this resource.
70
74
  */
71
75
  requiredModelClass() {
72
- const modelClass = /**
73
- * Narrows the runtime value to the documented type.
74
- * @type {typeof AuthorizationBaseResource} */ (this.constructor).modelClass()
75
-
76
- if (!modelClass) {
77
- throw new Error(`${this.constructor.name} must define static ModelClass before calling ability helpers.`)
78
- }
76
+ const ResourceClass = /** @type {typeof AuthorizationBaseResource} */ (this.constructor)
79
77
 
80
- return modelClass
78
+ return ResourceClass.modelClass()
81
79
  }
82
80
 
83
81
  /**
@@ -216,7 +216,7 @@
216
216
  * @typedef {object} ClientErrorPayloadContext
217
217
  * @property {string} controller - Controller class name.
218
218
  * @property {string} [action] - Controller action or endpoint label.
219
- * @property {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url" | "custom-command"} [commandType] - Frontend-model command type.
219
+ * @property {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url" | "custom-command"} [commandType] - Frontend-model command type.
220
220
  * @property {boolean} [expectedError] - Whether the error is an expected user-flow failure.
221
221
  * @property {boolean} [frontendModelEndpoint] - Whether the error came from the frontend-model endpoint.
222
222
  * @property {string} [model] - Frontend-model name from the failed request.
@@ -342,10 +342,10 @@
342
342
 
343
343
  /**
344
344
  * @typedef {object} FrontendModelResourceServerConfiguration
345
- * @property {function({action: "index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url", controller: import("./controller.js").default, params: Record<string, ?>, modelClass: typeof import("./database/record/index.js").default}) : (boolean | void | Promise<boolean | void>)} [beforeAction] - Optional callback run before built-in frontend actions.
345
+ * @property {function({action: "index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url", controller: import("./controller.js").default, params: Record<string, ?>, modelClass: typeof import("./database/record/index.js").default}) : (boolean | void | Promise<boolean | void>)} [beforeAction] - Optional callback run before built-in frontend actions.
346
346
  * @property {function({action: "index", controller: import("./controller.js").default, params: Record<string, ?>, modelClass: typeof import("./database/record/index.js").default}) : Promise<import("./database/record/index.js").default[]>} [records] - Records loader for frontendIndex.
347
347
  * @property {function({action: "index" | "find" | "create" | "update", controller: import("./controller.js").default, params: Record<string, ?>, modelClass: typeof import("./database/record/index.js").default, model: import("./database/record/index.js").default}) : Record<string, ?> | Promise<Record<string, ?>>} [serialize] - Record serializer for response payloads.
348
- * @property {function({action: "find" | "update" | "destroy" | "attach" | "download" | "url", controller: import("./controller.js").default, params: Record<string, ?>, modelClass: typeof import("./database/record/index.js").default, id: string | number}) : Promise<import("./database/record/index.js").default | null>} [find] - Record loader for find/update/destroy/attach/download/url actions.
348
+ * @property {function({action: "find" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url", controller: import("./controller.js").default, params: Record<string, ?>, modelClass: typeof import("./database/record/index.js").default, id: string | number}) : Promise<import("./database/record/index.js").default | null>} [find] - Record loader for find/update/destroy/attach/download/url actions.
349
349
  * @property {function({action: "create", controller: import("./controller.js").default, params: Record<string, ?>, modelClass: typeof import("./database/record/index.js").default, attributes: Record<string, ?>}) : Promise<import("./database/record/index.js").default>} [create] - Custom create callback.
350
350
  * @property {function({action: "update", controller: import("./controller.js").default, params: Record<string, ?>, modelClass: typeof import("./database/record/index.js").default, model: import("./database/record/index.js").default, attributes: Record<string, ?>}) : Promise<import("./database/record/index.js").default | void>} [update] - Custom update callback.
351
351
  * @property {function({action: "destroy", controller: import("./controller.js").default, params: Record<string, ?>, modelClass: typeof import("./database/record/index.js").default, model: import("./database/record/index.js").default}) : Promise<void>} [destroy] - Custom destroy callback.
@@ -18,7 +18,7 @@ import Ability from "./authorization/ability.js"
18
18
  import EventEmitter from "./utils/event-emitter.js"
19
19
  import VelociousWebsocketChannelSubscribers from "./http-server/websocket-channel-subscribers.js"
20
20
  import {CurrentConfigurationNotSetError, currentConfiguration, setCurrentConfiguration} from "./current-configuration.js"
21
- import {frontendModelResourceConfigurationFromDefinition, frontendModelResourcesForBackendProject} from "./frontend-models/resource-definition.js"
21
+ import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcesForBackendProject} from "./frontend-models/resource-definition.js"
22
22
  import PluginRoutes from "./routes/plugin-routes.js"
23
23
  import restArgsError from "./utils/rest-args-error.js"
24
24
  import {withTrackedStack} from "./utils/with-tracked-stack.js"
@@ -1673,12 +1673,13 @@ export default class VelociousConfiguration {
1673
1673
  throw new Error(`Resource for ${modelName} defines relationships as an object. Use an array instead: static relationships = ${JSON.stringify(Object.keys(resourceConfig.relationships))}`)
1674
1674
  }
1675
1675
 
1676
- const modelClass = /**
1677
- * Types the following value.
1678
- * @type {typeof import("./database/record/index.js").default | undefined} */ (this.modelClasses[modelName])
1676
+ const resourceClass = frontendModelResourceClassFromDefinition(resourceDefinition)
1679
1677
 
1680
- if (!modelClass) continue
1678
+ if (!resourceClass) {
1679
+ throw new Error(`Frontend model resource for ${modelName} must be a FrontendModelBaseResource subclass.`)
1680
+ }
1681
1681
 
1682
+ const modelClass = resourceClass.modelClass()
1682
1683
  const existingRelationships = modelClass.getRelationshipsMap()
1683
1684
 
1684
1685
  for (const relationshipName of resourceConfig.relationships) {
@@ -2567,27 +2568,21 @@ export default class VelociousConfiguration {
2567
2568
  return
2568
2569
  }
2569
2570
 
2571
+ /** @type {Set<typeof import("./database/pool/base.js").default>} */
2570
2572
  const constructors = new Set()
2571
2573
 
2572
2574
  this._closeDatabaseConnectionsPromise = (async () => {
2573
2575
  for (const pool of Object.values(this.databasePools)) {
2574
2576
  if (!pool) continue
2575
2577
 
2576
- if (typeof pool.closeAll === "function") {
2577
- await pool.closeAll()
2578
- }
2579
-
2580
- const poolConstructor = /**
2581
- * Types the following value.
2582
- * @type {{clearGlobalConnections?: (configuration: VelociousConfiguration) => void}} */ (pool.constructor)
2578
+ await pool.closeAll()
2583
2579
 
2584
- if (typeof poolConstructor?.clearGlobalConnections === "function") {
2585
- constructors.add(poolConstructor)
2586
- }
2580
+ const PoolClass = /** @type {typeof import("./database/pool/base.js").default} */ (pool.constructor)
2581
+ constructors.add(PoolClass)
2587
2582
  }
2588
2583
 
2589
- for (const constructor of constructors) {
2590
- constructor.clearGlobalConnections?.(this)
2584
+ for (const PoolClass of constructors) {
2585
+ PoolClass.clearGlobalConnections(this)
2591
2586
  }
2592
2587
 
2593
2588
  // Allow models to be re-initialized after connections are closed.
@@ -1178,7 +1178,7 @@ export default class VelociousDatabaseDriversBase {
1178
1178
  * @returns {boolean} - Whether query logging is enabled for this driver.
1179
1179
  */
1180
1180
  _queryLoggingEnabled() {
1181
- if (typeof this.configuration?.getQueryLoggingEnabled !== "function") return true
1181
+ if (!this.configuration) return true
1182
1182
  if (!this.configuration.getQueryLoggingEnabled()) return false
1183
1183
 
1184
1184
  const logger = new Logger("SQL", {configuration: this.configuration})
@@ -1213,9 +1213,9 @@ export default class VelociousDatabaseDriversBase {
1213
1213
  _querySourceLine(sourceStack) {
1214
1214
  if (!sourceStack) return undefined
1215
1215
 
1216
- const applicationDirectory = typeof this.configuration?.getDirectoryIfAvailable === "function"
1216
+ const applicationDirectory = this.configuration
1217
1217
  ? this.configuration.getDirectoryIfAvailable()
1218
- : this.configuration?.getDirectory?.()
1218
+ : undefined
1219
1219
 
1220
1220
  if (!applicationDirectory) return undefined
1221
1221
 
@@ -90,9 +90,10 @@ class VelociousDatabasePoolBase {
90
90
 
91
91
  /**
92
92
  * Clears any global connections for the given configuration.
93
+ * @param {import("../../configuration.js").default} configuration - Configuration owning the pool.
93
94
  * @returns {void} - No return value.
94
95
  */
95
- static clearGlobalConnections() {}
96
+ static clearGlobalConnections(configuration) { void configuration }
96
97
 
97
98
  /**
98
99
  * Runs constructor.
@@ -9,10 +9,5 @@
9
9
  export default async function ensureModelClassInitialized(modelClass, configuration) {
10
10
  if (modelClass.isInitialized()) return
11
11
 
12
- if (typeof modelClass.ensureInitialized === "function") {
13
- await modelClass.ensureInitialized({configuration})
14
- return
15
- }
16
-
17
- await modelClass.initializeRecord({configuration})
12
+ await modelClass.ensureInitialized({configuration})
18
13
  }
@@ -170,6 +170,38 @@ export default class RecordAttachmentHandle {
170
170
  return downloads
171
171
  }
172
172
 
173
+ /**
174
+ * Runs list metadata. Returns metadata (no content bytes) for every attachment
175
+ * under this (record, name), so callers can enumerate has-many attachments
176
+ * without downloading their content.
177
+ * @returns {Promise<Array<{byteSize: number, contentType: string | null, filename: string, id: string, url: string | null}>>} - Attachment metadata entries.
178
+ */
179
+ async listMetadata() {
180
+ if (!this.model.isPersisted()) return []
181
+
182
+ const store = recordAttachmentsStoreForModel(this.model)
183
+ const rows = await store.findMany({model: this.model, name: this.name})
184
+ /**
185
+ * Metadata entries.
186
+ * @type {Array<{byteSize: number, contentType: string | null, filename: string, id: string, url: string | null}>} */
187
+ const entries = []
188
+
189
+ for (const row of rows) {
190
+ const url = await store.attachmentRowUrl({model: this.model, name: this.name, row})
191
+ const byteSize = Number(row.byte_size)
192
+
193
+ entries.push({
194
+ byteSize: Number.isFinite(byteSize) ? byteSize : 0,
195
+ contentType: row.content_type || null,
196
+ filename: row.filename || "attachment.bin",
197
+ id: row.id,
198
+ url
199
+ })
200
+ }
201
+
202
+ return entries
203
+ }
204
+
173
205
  /**
174
206
  * Runs url.
175
207
  * @param {string} [id] - Optional attachment id for has-many attachments.
@@ -123,7 +123,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
123
123
  const fileContent = await this.buildModelFileContent({
124
124
  className,
125
125
  importPath,
126
- modelClass: resourceClass?.ModelClass || configuration.getModelClasses()[className],
126
+ modelClass: resourceClass ? resourceClass.modelClass() : configuration.getModelClasses()[className],
127
127
  modelConfig,
128
128
  resourceClass
129
129
  })
@@ -817,22 +817,17 @@ export default class DbGenerateFrontendModels extends BaseCommand {
817
817
  * @returns {FrontendModelGeneratorPermitSpec} - Permitted params spec.
818
818
  */
819
819
  permittedParamsForGenerator(resourceClass, action) {
820
- if (!resourceClass || typeof resourceClass !== "function") return []
821
-
822
- const prototypeWithMethod = /**
823
- * Resource prototype.
824
- * @type {{permittedParams?: (arg?: object) => FrontendModelGeneratorPermitSpec}}
825
- */ (resourceClass.prototype)
826
-
827
- if (typeof prototypeWithMethod?.permittedParams !== "function") return []
820
+ if (!resourceClass) return []
828
821
 
829
822
  try {
823
+ const modelClass = resourceClass.modelClass()
824
+
830
825
  const instance = new resourceClass({
831
826
  ability: undefined,
832
827
  context: {},
833
828
  locals: {},
834
- modelClass: resourceClass.ModelClass,
835
- modelName: resourceClass.ModelClass?.getModelName?.() || resourceClass.name,
829
+ modelClass,
830
+ modelName: modelClass.getModelName(),
836
831
  params: {},
837
832
  resourceConfiguration: /**
838
833
  * Resource configuration.
@@ -860,24 +855,19 @@ export default class DbGenerateFrontendModels extends BaseCommand {
860
855
  * @returns {string[]} - Relationship names that accept nested writes (empty when none).
861
856
  */
862
857
  nestedRelationshipNamesForGenerator(resourceClass) {
863
- if (!resourceClass || typeof resourceClass !== "function") return []
864
-
865
- const prototypeWithMethod = /**
866
- * Resource prototype.
867
- * @type {{permittedParams?: (arg?: object) => FrontendModelGeneratorPermitSpec}}
868
- */ (resourceClass.prototype)
869
-
870
- if (typeof prototypeWithMethod?.permittedParams !== "function") return []
858
+ if (!resourceClass) return []
871
859
 
872
860
  let spec
873
861
 
874
862
  try {
863
+ const modelClass = resourceClass.modelClass()
864
+
875
865
  const instance = new resourceClass({
876
866
  ability: undefined,
877
867
  context: {},
878
868
  locals: {},
879
- modelClass: resourceClass.ModelClass,
880
- modelName: resourceClass.ModelClass?.getModelName?.() || resourceClass.name,
869
+ modelClass,
870
+ modelName: modelClass.getModelName(),
881
871
  params: {},
882
872
  resourceConfiguration: /**
883
873
  * Resource configuration.
@@ -1073,6 +1063,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1073
1063
  */
1074
1064
  frontendAttributeConfigForGeneratedAttribute({attributeConfig, attributeName, modelClass}) {
1075
1065
  if (!this.frontendAttributeIsModelPrimaryKey({attributeName, modelClass})) return attributeConfig
1066
+ if (this.frontendAttributeConfigHasNullability(attributeConfig)) return attributeConfig
1076
1067
 
1077
1068
  return {...attributeConfig, null: false}
1078
1069
  }
@@ -1141,6 +1132,18 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1141
1132
  || typeof attributeConfig?.jsDocType == "string"
1142
1133
  }
1143
1134
 
1135
+ /**
1136
+ * Runs frontend attribute config has nullability.
1137
+ * @param {FrontendAttributeConfig | null | undefined} attributeConfig - Attribute config.
1138
+ * @returns {boolean} - Whether the config declares nullability.
1139
+ */
1140
+ frontendAttributeConfigHasNullability(attributeConfig) {
1141
+ if (!attributeConfig || typeof attributeConfig !== "object") return false
1142
+ if (Object.prototype.hasOwnProperty.call(attributeConfig, "null")) return true
1143
+
1144
+ return typeof attributeConfig.getNull == "function"
1145
+ }
1146
+
1144
1147
  /**
1145
1148
  * Runs js doc type for frontend attribute.
1146
1149
  * @param {object} args - Arguments.
@@ -1832,11 +1835,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1832
1835
  * @returns {{autoload: boolean, relationshipName: string, targetClassName: string, targetFileName: string, type: "belongsTo" | "hasOne" | "hasMany"}} Inferred relationship definition.
1833
1836
  */
1834
1837
  inferredRelationshipDefinition({className, relationshipName, resourceClass}) {
1835
- const modelClass = resourceClass?.ModelClass || this.getConfiguration().getModelClass(className)
1836
-
1837
- if (!modelClass) {
1838
- throw new Error(`Could not find backend model class '${className}' for relationship '${relationshipName}'`)
1839
- }
1838
+ const modelClass = resourceClass ? resourceClass.modelClass() : this.getConfiguration().getModelClass(className)
1840
1839
 
1841
1840
  const relationship = modelClass.getRelationshipByName(relationshipName)
1842
1841
  const relationshipType = relationship.getType()