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
@@ -5,7 +5,7 @@ import Controller from "./controller.js"
5
5
  import FrontendModelBaseResource from "./frontend-model-resource/base-resource.js"
6
6
  import Response from "./http-server/client/response.js"
7
7
  import {frontendModelResourcesWithBuiltInsForBackendProject} from "./frontend-models/built-in-resources.js"
8
- import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcePath} from "./frontend-models/resource-definition.js"
8
+ import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcePath, frontendModelResourcesForBackendProject} from "./frontend-models/resource-definition.js"
9
9
  import {normalizeGroup as normalizeQueryGroup, normalizeJoins as normalizeQueryJoins, normalizePluck as normalizeQueryPluck, normalizePreload as normalizeQueryPreload, normalizeSearchOperator as normalizeQuerySearchOperator, normalizeSort as normalizeQuerySort} from "./frontend-models/query.js"
10
10
  import {assignSafeProperty, deserializeFrontendModelTransportValue, isBackendModelInstance, serializeFrontendModelTransportValue} from "./frontend-models/transport-serialization.js"
11
11
  import RoutesResolver from "./routes/resolver.js"
@@ -149,6 +149,14 @@ function normalizeFrontendModelSelect(select, rootModelName = null) {
149
149
  * }} FrontendModelEndpointErrorContext
150
150
  */
151
151
 
152
+ /**
153
+ * FrontendModelIndexQueryOptions type.
154
+ * @typedef {object} FrontendModelIndexQueryOptions
155
+ * @property {boolean} [includePagination] - Whether frontend-model pagination params should be applied.
156
+ * @property {boolean} [includeSort] - Whether frontend-model sort params should be applied.
157
+ * @property {import("./frontend-model-resource/base-resource.js").default} [resource] - Resource providing query hooks.
158
+ */
159
+
152
160
  const frontendModelJoinedPathsSymbol = Symbol("frontendModelJoinedPaths")
153
161
  const frontendModelGroupedColumnsSymbol = Symbol("frontendModelGroupedColumns")
154
162
  const frontendModelWhereNoMatchSymbol = Symbol("frontendModelWhereNoMatch")
@@ -772,14 +780,10 @@ export default class FrontendModelController extends Controller {
772
780
  /**
773
781
  * Runs frontend model resource model class.
774
782
  * @param {{modelName: string, resourceClass: import("./configuration-types.js").FrontendModelResourceClassType}} frontendModelResource - Frontend model resource configuration.
775
- * @returns {typeof import("./database/record/index.js").default | null} - Backing record class, when available.
783
+ * @returns {typeof import("./database/record/index.js").default} - Backing record class.
776
784
  */
777
785
  frontendModelResourceModelClass(frontendModelResource) {
778
- const resourceModelClass = frontendModelResource.resourceClass.ModelClass
779
-
780
- if (resourceModelClass) return resourceModelClass
781
-
782
- return this.getConfiguration().getModelClasses()[frontendModelResource.modelName] || null
786
+ return frontendModelResource.resourceClass.modelClass()
783
787
  }
784
788
 
785
789
  /**
@@ -791,18 +795,7 @@ export default class FrontendModelController extends Controller {
791
795
 
792
796
  if (!frontendModelResource) return null
793
797
 
794
- const resourceModelClass = this.frontendModelResourceModelClass(frontendModelResource)
795
-
796
- if (resourceModelClass) return resourceModelClass
797
-
798
- const modelClasses = this.getConfiguration().getModelClasses()
799
- const modelClass = modelClasses[frontendModelResource.modelName]
800
-
801
- if (!modelClass) {
802
- throw new Error(`Frontend model '${frontendModelResource.modelName}' is configured for '${this.frontendModelParams().controller}', but no model class was registered. Registered models: ${Object.keys(modelClasses).join(", ")}`)
803
- }
804
-
805
- return modelClass
798
+ return this.frontendModelResourceModelClass(frontendModelResource)
806
799
  }
807
800
 
808
801
  /**
@@ -835,13 +828,7 @@ export default class FrontendModelController extends Controller {
835
828
  async ensureFrontendModelRecordClassInitialized(modelClass) {
836
829
  if (!modelClass || modelClass.isInitialized()) return
837
830
 
838
- const configuration = this.getConfiguration()
839
-
840
- if (typeof modelClass.ensureInitialized === "function") {
841
- await modelClass.ensureInitialized({configuration})
842
- } else if (typeof modelClass.initializeRecord === "function") {
843
- await modelClass.initializeRecord({configuration})
844
- }
831
+ await modelClass.ensureInitialized({configuration: this.getConfiguration()})
845
832
  }
846
833
 
847
834
  /**
@@ -1000,7 +987,7 @@ export default class FrontendModelController extends Controller {
1000
987
 
1001
988
  /**
1002
989
  * Runs frontend model ability action.
1003
- * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
990
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} action - Frontend action.
1004
991
  * @returns {string} - Ability action configured for the frontend action.
1005
992
  */
1006
993
  frontendModelAbilityAction(action) {
@@ -1018,7 +1005,7 @@ export default class FrontendModelController extends Controller {
1018
1005
 
1019
1006
  const abilityKey = action === "attach"
1020
1007
  ? "update"
1021
- : ((action === "download" || action === "url") ? "find" : action)
1008
+ : ((action === "download" || action === "url" || action === "attachmentList") ? "find" : action)
1022
1009
  const abilityAction = abilities[abilityKey]
1023
1010
 
1024
1011
  if (typeof abilityAction !== "string" || abilityAction.length < 1) {
@@ -1030,7 +1017,7 @@ export default class FrontendModelController extends Controller {
1030
1017
 
1031
1018
  /**
1032
1019
  * Runs frontend model ability authorized query.
1033
- * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
1020
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} action - Frontend action.
1034
1021
  * @returns {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} - Authorized query for the action.
1035
1022
  */
1036
1023
  frontendModelAbilityAuthorizedQuery(action) {
@@ -1041,7 +1028,7 @@ export default class FrontendModelController extends Controller {
1041
1028
 
1042
1029
  /**
1043
1030
  * Runs frontend model authorized query.
1044
- * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
1031
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} action - Frontend action.
1045
1032
  * @returns {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} - Authorized query for the action.
1046
1033
  */
1047
1034
  frontendModelAuthorizedQuery(action) {
@@ -1071,7 +1058,7 @@ export default class FrontendModelController extends Controller {
1071
1058
  /**
1072
1059
  * Runs frontend model filter authorized models.
1073
1060
  * @param {object} args - Arguments.
1074
- * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} args.action - Frontend action.
1061
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} args.action - Frontend action.
1075
1062
  * @param {import("./database/record/index.js").default[]} args.models - Candidate models.
1076
1063
  * @returns {Promise<import("./database/record/index.js").default[]>} - Authorized models.
1077
1064
  */
@@ -1091,7 +1078,7 @@ export default class FrontendModelController extends Controller {
1091
1078
 
1092
1079
  /**
1093
1080
  * Runs run frontend model before action.
1094
- * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
1081
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} action - Frontend action.
1095
1082
  * @returns {Promise<boolean>} - Whether action should continue.
1096
1083
  */
1097
1084
  async runFrontendModelBeforeAction(action) {
@@ -1102,7 +1089,7 @@ export default class FrontendModelController extends Controller {
1102
1089
 
1103
1090
  /**
1104
1091
  * Runs frontend model find record.
1105
- * @param {"find" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
1092
+ * @param {"find" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} action - Frontend action.
1106
1093
  * @param {string | number} id - Record id.
1107
1094
  * @returns {Promise<import("./database/record/index.js").default | null>} - Located model record.
1108
1095
  */
@@ -1253,7 +1240,11 @@ export default class FrontendModelController extends Controller {
1253
1240
  */
1254
1241
  frontendModelPluck() {
1255
1242
  try {
1256
- return normalizeQueryPluck(this.frontendModelParams().pluck)
1243
+ const pluck = normalizeQueryPluck(this.frontendModelParams().pluck)
1244
+
1245
+ this.validateFrontendModelPluckDefinitions(pluck)
1246
+
1247
+ return pluck
1257
1248
  } catch (error) {
1258
1249
  throw frontendModelValidationErrorForError(error)
1259
1250
  }
@@ -1301,9 +1292,7 @@ export default class FrontendModelController extends Controller {
1301
1292
  * Resolve an entry from the frontend-model `abilities` payload to
1302
1293
  * its backend model class by looking up the resource by modelName
1303
1294
  * across all configured backend projects. Returns null when no
1304
- * resource matches the spec entry is then silently ignored so a
1305
- * caller requesting abilities for a model they cannot resolve does
1306
- * not crash the request.
1295
+ * resource matches the user-provided ability entry.
1307
1296
  * @param {string} modelName
1308
1297
  * @returns {typeof import("./database/record/index.js").default | null}
1309
1298
  */
@@ -1311,23 +1300,20 @@ export default class FrontendModelController extends Controller {
1311
1300
  if (typeof modelName !== "string" || modelName.length === 0) return null
1312
1301
 
1313
1302
  const configuration = this.getConfiguration()
1314
- const backendProjects = configuration?.getBackendProjects?.() ?? []
1303
+ const backendProjects = configuration.getBackendProjects()
1315
1304
 
1316
1305
  for (const backendProject of backendProjects) {
1317
- const frontendModels = backendProject?.frontendModels
1318
- if (!frontendModels || typeof frontendModels !== "object") continue
1319
-
1306
+ const frontendModels = frontendModelResourcesForBackendProject(backendProject)
1320
1307
  const resourceDefinition = frontendModels[modelName]
1308
+
1321
1309
  if (!resourceDefinition) continue
1322
1310
 
1323
1311
  const resourceClass = frontendModelResourceClassFromDefinition(resourceDefinition)
1324
- if (!resourceClass) continue
1325
-
1326
- const modelClass = typeof resourceClass.modelClass === "function"
1327
- ? resourceClass.modelClass()
1328
- : resourceClass.ModelClass
1312
+ if (!resourceClass) {
1313
+ throw new Error(`Frontend model '${modelName}' resource definition must be a FrontendModelBaseResource subclass.`)
1314
+ }
1329
1315
 
1330
- if (typeof modelClass === "function") return modelClass
1316
+ return resourceClass.modelClass()
1331
1317
  }
1332
1318
 
1333
1319
  return null
@@ -1361,24 +1347,15 @@ export default class FrontendModelController extends Controller {
1361
1347
  if (seen.has(record)) return
1362
1348
  seen.add(record)
1363
1349
 
1364
- const ModelClass = typeof record.getModelClass === "function"
1365
- ? record.getModelClass()
1366
- : null
1367
- if (ModelClass && typeof ModelClass.getModelName === "function" && ModelClass.getModelName() === modelName) {
1350
+ const ModelClass = record.getModelClass()
1351
+ if (ModelClass.getModelName() === modelName) {
1368
1352
  out.push(record)
1369
1353
  }
1370
1354
 
1371
- const relationshipsMap = typeof ModelClass?.getRelationshipsMap === "function"
1372
- ? ModelClass.getRelationshipsMap()
1373
- : null
1374
- if (!relationshipsMap) return
1355
+ const relationshipsMap = ModelClass.getRelationshipsMap()
1375
1356
 
1376
1357
  for (const relationshipName of Object.keys(relationshipsMap)) {
1377
- const relationship = typeof record.getRelationshipByName === "function"
1378
- ? record.getRelationshipByName(relationshipName)
1379
- : null
1380
- if (!relationship || typeof relationship.getLoadedOrUndefined !== "function") continue
1381
-
1358
+ const relationship = record.getRelationshipByName(relationshipName)
1382
1359
  const loaded = relationship.getLoadedOrUndefined()
1383
1360
  if (loaded === undefined) continue
1384
1361
 
@@ -1512,9 +1489,11 @@ export default class FrontendModelController extends Controller {
1512
1489
 
1513
1490
  /**
1514
1491
  * Runs frontend model index query.
1492
+ * @param {FrontendModelIndexQueryOptions} [options] - Index query options.
1515
1493
  * @returns {import("./database/query/model-class-query.js").default} - Frontend index query with normalized params applied.
1516
1494
  */
1517
- frontendModelIndexQuery() {
1495
+ frontendModelIndexQuery(options = {}) {
1496
+ const {includePagination = true, includeSort = true, resource = this.frontendModelResourceInstance()} = options
1518
1497
  let query = this.frontendModelAuthorizedQuery("index")
1519
1498
  const preload = this.frontendModelPreload()
1520
1499
 
@@ -1527,7 +1506,9 @@ export default class FrontendModelController extends Controller {
1527
1506
  const pagination = this.frontendModelPagination()
1528
1507
  const distinct = this.frontendModelDistinct()
1529
1508
 
1530
- this.applyFrontendModelPagination({pagination, query})
1509
+ if (includePagination) {
1510
+ resource.applyFrontendModelIndexPagination({controller: this, pagination, query})
1511
+ }
1531
1512
 
1532
1513
  if (distinct !== null) {
1533
1514
  query.distinct(distinct)
@@ -1550,7 +1531,7 @@ export default class FrontendModelController extends Controller {
1550
1531
  const searches = this.frontendModelSearches()
1551
1532
 
1552
1533
  for (const search of searches) {
1553
- this.applyFrontendModelSearch({query, search})
1534
+ resource.applyFrontendModelIndexSearch({controller: this, query, search})
1554
1535
  }
1555
1536
 
1556
1537
  const groups = this.frontendModelGroup()
@@ -1565,9 +1546,9 @@ export default class FrontendModelController extends Controller {
1565
1546
 
1566
1547
  const sorts = this.frontendModelSort()
1567
1548
 
1568
- if (sorts.length > 0) {
1549
+ if (includeSort && sorts.length > 0) {
1569
1550
  for (const sort of sorts) {
1570
- this.applyFrontendModelSort({query, sort})
1551
+ resource.applyFrontendModelIndexSort({controller: this, query, sort})
1571
1552
  }
1572
1553
  }
1573
1554
 
@@ -1696,6 +1677,95 @@ export default class FrontendModelController extends Controller {
1696
1677
  })
1697
1678
  }
1698
1679
 
1680
+ /**
1681
+ * Resolves a frontend-model pluck attribute to a database column.
1682
+ * @param {{attributeName: string, modelClass: typeof import("./database/record/index.js").default}} args - Arguments.
1683
+ * @returns {string | undefined} Resolved DB column name.
1684
+ */
1685
+ resolveFrontendModelPluckColumnName({attributeName, modelClass}) {
1686
+ const attributeNames = this.frontendModelResourceAttributeNamesForModelClass(modelClass)
1687
+
1688
+ if (attributeNames && !attributeNames.has(attributeName)) return undefined
1689
+
1690
+ return this.resolveFrontendModelColumnName(modelClass, attributeName)
1691
+ }
1692
+
1693
+ /**
1694
+ * Runs exposed frontend-model resource attribute names for a model class.
1695
+ * @param {typeof import("./database/record/index.js").default} modelClass - Model class.
1696
+ * @returns {Set<string> | null} Exposed resource attribute names, or null when the resource exposes all DB-backed model attributes.
1697
+ */
1698
+ frontendModelResourceAttributeNamesForModelClass(modelClass) {
1699
+ const frontendModelResource = this.frontendModelResourceConfigurationForModelClass(modelClass)
1700
+
1701
+ if (!frontendModelResource) return new Set()
1702
+
1703
+ const attributes = frontendModelResource.resourceConfiguration.attributes
1704
+
1705
+ if (!attributes) return null
1706
+
1707
+ const attributeNames = this.frontendModelResourceAttributeNames(attributes)
1708
+
1709
+ if (attributeNames.size < 1) return null
1710
+
1711
+ return attributeNames
1712
+ }
1713
+
1714
+ /**
1715
+ * Runs exposed frontend-model resource attribute names.
1716
+ * @param {import("./configuration-types.js").FrontendModelResourceConfiguration["attributes"]} attributes - Resource attributes.
1717
+ * @returns {Set<string>} Exposed resource attribute names.
1718
+ */
1719
+ frontendModelResourceAttributeNames(attributes) {
1720
+ /** @type {Set<string>} */
1721
+ const attributeNames = new Set()
1722
+
1723
+ if (Array.isArray(attributes)) {
1724
+ for (const attribute of attributes) {
1725
+ if (typeof attribute === "string") {
1726
+ attributeNames.add(attribute)
1727
+ continue
1728
+ }
1729
+
1730
+ const attributeConfig = /** @type {import("./configuration-types.js").FrontendModelAttributeConfiguration} */ (attribute)
1731
+
1732
+ if (typeof attributeConfig.name !== "string" || attributeConfig.name.length < 1) {
1733
+ throw new Error("Frontend-model resource attribute array entries must be strings or configs with a name.")
1734
+ }
1735
+
1736
+ attributeNames.add(attributeConfig.name)
1737
+ }
1738
+
1739
+ return attributeNames
1740
+ }
1741
+
1742
+ return new Set(Object.keys(attributes))
1743
+ }
1744
+
1745
+ /**
1746
+ * Validates frontend-model pluck definitions against exposed resource attributes.
1747
+ * @param {FrontendModelPluck[]} pluck - Pluck descriptors.
1748
+ * @returns {void}
1749
+ */
1750
+ validateFrontendModelPluckDefinitions(pluck) {
1751
+ const modelClass = this.frontendModelClass()
1752
+
1753
+ for (const pluckEntry of pluck) {
1754
+ const targetModelClass = this.frontendModelSearchTargetModelClass({
1755
+ modelClass,
1756
+ path: pluckEntry.path
1757
+ })
1758
+ const columnName = this.resolveFrontendModelPluckColumnName({
1759
+ attributeName: pluckEntry.column,
1760
+ modelClass: targetModelClass
1761
+ })
1762
+
1763
+ if (!columnName) {
1764
+ throw new Error(`Unknown pluck column "${pluckEntry.column}" for ${targetModelClass.name}`)
1765
+ }
1766
+ }
1767
+ }
1768
+
1699
1769
  /**
1700
1770
  * Runs frontend model search target model class.
1701
1771
  * @param {object} args - Search args.
@@ -1945,6 +2015,8 @@ export default class FrontendModelController extends Controller {
1945
2015
  })
1946
2016
  .filter((entry) => typeof entry === "string")
1947
2017
 
2018
+ if (attributeNames.length === 0) return null
2019
+
1948
2020
  return new Set(attributeNames)
1949
2021
  }
1950
2022
 
@@ -2612,7 +2684,7 @@ export default class FrontendModelController extends Controller {
2612
2684
  }
2613
2685
 
2614
2686
  const configuration = this.getConfiguration()
2615
- const backendProjects = configuration.getBackendProjects?.() || []
2687
+ const backendProjects = configuration.getBackendProjects()
2616
2688
  const modelClassName = /**
2617
2689
  * Types the following value.
2618
2690
  * @type {typeof import("./database/record/index.js").default} */ (model.constructor).getModelName()
@@ -2632,9 +2704,7 @@ export default class FrontendModelController extends Controller {
2632
2704
  * @type {typeof import("./database/record/index.js").default} */ (model.constructor),
2633
2705
  modelName: modelClassName,
2634
2706
  params: {},
2635
- resourceConfiguration: /**
2636
- * Types the following value.
2637
- * @type {import("./configuration-types.js").FrontendModelResourceConfiguration | undefined} */ (typeof resourceClass.resourceConfig === "function" ? resourceClass.resourceConfig() : undefined)
2707
+ resourceConfiguration: resourceClass.resourceConfig()
2638
2708
  })
2639
2709
  }
2640
2710
  }
@@ -2842,15 +2912,9 @@ export default class FrontendModelController extends Controller {
2842
2912
  for (const [modelIndex, model] of models.entries()) {
2843
2913
  const serializedAttributes = await this.serializeFrontendModelAttributes(model)
2844
2914
  const preloadedRelationships = preloadedRelationshipsPerModel[modelIndex]
2845
- const associationCounts = typeof model.associationCounts === "function"
2846
- ? model.associationCounts()
2847
- : {}
2848
- const queryDataValues = typeof model.queryDataValues === "function"
2849
- ? model.queryDataValues()
2850
- : {}
2851
- const computedAbilities = typeof model.computedAbilities === "function"
2852
- ? model.computedAbilities()
2853
- : {}
2915
+ const associationCounts = model.associationCounts()
2916
+ const queryDataValues = model.queryDataValues()
2917
+ const computedAbilities = model.computedAbilities()
2854
2918
  const hasCounts = Object.keys(associationCounts).length > 0
2855
2919
  const hasQueryData = Object.keys(queryDataValues).length > 0
2856
2920
  const hasAbilities = Object.keys(computedAbilities).length > 0
@@ -2944,7 +3008,7 @@ export default class FrontendModelController extends Controller {
2944
3008
  * @param {object} args - Error context args.
2945
3009
  * @param {string} args.action - Endpoint/action label.
2946
3010
  * @param {unknown} args.error - Caught error.
2947
- * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url" | "custom-command"} [args.commandType] - Frontend-model command type.
3011
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url" | "custom-command"} [args.commandType] - Frontend-model command type.
2948
3012
  * @param {string | undefined} [args.model] - Request model name when available.
2949
3013
  * @param {string | undefined} [args.requestId] - Batch request id when available.
2950
3014
  * @returns {FrontendModelEndpointErrorContext} Frontend-model endpoint error context.
@@ -3025,7 +3089,7 @@ export default class FrontendModelController extends Controller {
3025
3089
  * @param {object} args - Error log args.
3026
3090
  * @param {string} args.action - Endpoint/action label.
3027
3091
  * @param {?} args.error - Caught error.
3028
- * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url" | "custom-command"} [args.commandType] - Frontend-model command type.
3092
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url" | "custom-command"} [args.commandType] - Frontend-model command type.
3029
3093
  * @param {string | undefined} [args.model] - Request model name when available.
3030
3094
  * @param {string | undefined} [args.requestId] - Batch request id when available.
3031
3095
  * @returns {Promise<void>} - Resolves after logging.
@@ -3066,7 +3130,7 @@ export default class FrontendModelController extends Controller {
3066
3130
 
3067
3131
  /**
3068
3132
  * Runs frontend model render command response.
3069
- * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
3133
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} action - Frontend action.
3070
3134
  * @returns {Promise<void>} - Resolves when response has been rendered.
3071
3135
  */
3072
3136
  async frontendModelRenderCommandResponse(action) {
@@ -3094,7 +3158,7 @@ export default class FrontendModelController extends Controller {
3094
3158
 
3095
3159
  /**
3096
3160
  * Runs frontend model command payload.
3097
- * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
3161
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} action - Frontend action.
3098
3162
  * @returns {Promise<Record<string, ?> | null>} - Response payload.
3099
3163
  */
3100
3164
  async frontendModelCommandPayload(action) {
@@ -3127,7 +3191,7 @@ export default class FrontendModelController extends Controller {
3127
3191
 
3128
3192
  const values = await this.frontendModelPluckValues({
3129
3193
  pluck,
3130
- query: this.frontendModelIndexQuery()
3194
+ query: resource.indexQuery()
3131
3195
  })
3132
3196
 
3133
3197
  return {
@@ -3248,6 +3312,24 @@ export default class FrontendModelController extends Controller {
3248
3312
  }
3249
3313
  }
3250
3314
 
3315
+ if (action === "attachmentList") {
3316
+ const attachmentParams = frontendModelAttachmentParams(params)
3317
+ if (typeof attachmentParams === "string") return this.frontendModelErrorPayload(attachmentParams)
3318
+
3319
+ const model = await this.frontendModelFindRecord("attachmentList", id)
3320
+
3321
+ if (!model) {
3322
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3323
+ }
3324
+
3325
+ const attachments = await model.getAttachmentByName(attachmentParams.attachmentName).listMetadata()
3326
+
3327
+ return {
3328
+ attachments,
3329
+ status: "success"
3330
+ }
3331
+ }
3332
+
3251
3333
  if (action === "find") {
3252
3334
  const model = await this.frontendModelFindRecord("find", id)
3253
3335
 
@@ -3326,7 +3408,7 @@ export default class FrontendModelController extends Controller {
3326
3408
  continue
3327
3409
  }
3328
3410
 
3329
- const isBuiltInCommand = ["index", "find", "create", "update", "destroy", "attach", "download", "url"].includes(commandType)
3411
+ const isBuiltInCommand = ["index", "find", "create", "update", "destroy", "attach", "download", "url", "attachmentList"].includes(commandType)
3330
3412
 
3331
3413
  if (!isBuiltInCommand && (typeof customPath !== "string" || !customPath.startsWith("/"))) {
3332
3414
  responses.push({
@@ -3676,10 +3758,7 @@ export default class FrontendModelController extends Controller {
3676
3758
 
3677
3759
  if (isBackendModelInstance(value)) {
3678
3760
  const richSerialized = await resource.serialize(value, action)
3679
- const modelClass = /**
3680
- * Types the following value.
3681
- * @type {{constructor: {getModelName?: () => string, name?: string}}} */ (value).constructor
3682
- const modelName = typeof modelClass.getModelName === "function" ? modelClass.getModelName() : (modelClass.name || "")
3761
+ const modelName = value.getModelClass().getModelName()
3683
3762
 
3684
3763
  // Wrap the resource-serialized payload in the frontend_model transport
3685
3764
  // marker. Marker-based decoding routes through `instantiateFromResponse`,