velocious 1.0.430 → 1.0.431

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 (811) hide show
  1. package/bin/velocious.js +48 -0
  2. package/build/bin/velocious.js +39 -34
  3. package/build/index.js +1 -2
  4. package/build/src/application.js +214 -187
  5. package/build/src/authorization/ability.d.ts +24 -23
  6. package/build/src/authorization/ability.d.ts.map +1 -1
  7. package/build/src/authorization/ability.js +300 -252
  8. package/build/src/authorization/base-resource.d.ts +20 -26
  9. package/build/src/authorization/base-resource.d.ts.map +1 -1
  10. package/build/src/authorization/base-resource.js +136 -118
  11. package/build/src/background-jobs/client.js +47 -43
  12. package/build/src/background-jobs/cron-expression.js +166 -127
  13. package/build/src/background-jobs/forked-runner-child.js +47 -37
  14. package/build/src/background-jobs/job-record.js +10 -8
  15. package/build/src/background-jobs/job-registry.js +84 -72
  16. package/build/src/background-jobs/job-runner.js +81 -74
  17. package/build/src/background-jobs/job.js +72 -62
  18. package/build/src/background-jobs/json-socket.js +70 -65
  19. package/build/src/background-jobs/main.js +900 -841
  20. package/build/src/background-jobs/normalize-error.js +11 -12
  21. package/build/src/background-jobs/scheduler.js +247 -205
  22. package/build/src/background-jobs/socket-request.js +65 -60
  23. package/build/src/background-jobs/status-reporter.js +96 -86
  24. package/build/src/background-jobs/store.js +980 -862
  25. package/build/src/background-jobs/types.js +3 -2
  26. package/build/src/background-jobs/web/authorization.js +50 -38
  27. package/build/src/background-jobs/web/controller.js +268 -232
  28. package/build/src/background-jobs/web/index.js +40 -36
  29. package/build/src/background-jobs/web/path-matcher.js +48 -45
  30. package/build/src/background-jobs/web/registry.js +14 -9
  31. package/build/src/background-jobs/worker.js +639 -585
  32. package/build/src/beacon/client.js +293 -264
  33. package/build/src/beacon/in-process-broker.js +25 -20
  34. package/build/src/beacon/in-process-client.js +116 -104
  35. package/build/src/beacon/server.js +126 -110
  36. package/build/src/beacon/types.js +8 -2
  37. package/build/src/cli/base-command.js +57 -49
  38. package/build/src/cli/browser-cli.js +42 -37
  39. package/build/src/cli/commands/background-jobs-main.js +5 -5
  40. package/build/src/cli/commands/background-jobs-runner.js +5 -5
  41. package/build/src/cli/commands/background-jobs-worker.js +5 -5
  42. package/build/src/cli/commands/beacon.js +5 -5
  43. package/build/src/cli/commands/console.js +10 -10
  44. package/build/src/cli/commands/db/base-command.js +76 -71
  45. package/build/src/cli/commands/db/create.js +61 -53
  46. package/build/src/cli/commands/db/drop.js +71 -62
  47. package/build/src/cli/commands/db/migrate.js +15 -13
  48. package/build/src/cli/commands/db/reset.js +19 -16
  49. package/build/src/cli/commands/db/rollback.js +13 -12
  50. package/build/src/cli/commands/db/schema/dump.js +9 -9
  51. package/build/src/cli/commands/db/schema/load.js +9 -9
  52. package/build/src/cli/commands/db/seed.js +9 -9
  53. package/build/src/cli/commands/db/tenants/check.js +35 -32
  54. package/build/src/cli/commands/db/tenants/create.js +29 -26
  55. package/build/src/cli/commands/db/tenants/migrate.js +44 -40
  56. package/build/src/cli/commands/destroy/migration.js +5 -5
  57. package/build/src/cli/commands/generate/base-models.js +5 -5
  58. package/build/src/cli/commands/generate/frontend-models.js +9 -9
  59. package/build/src/cli/commands/generate/migration.js +5 -5
  60. package/build/src/cli/commands/generate/model.js +5 -5
  61. package/build/src/cli/commands/init.js +9 -7
  62. package/build/src/cli/commands/routes.js +6 -6
  63. package/build/src/cli/commands/run-script.js +9 -9
  64. package/build/src/cli/commands/runner.js +9 -9
  65. package/build/src/cli/commands/server.js +6 -6
  66. package/build/src/cli/commands/test.js +7 -6
  67. package/build/src/cli/index.js +141 -127
  68. package/build/src/cli/tenant-database-command-helper.js +185 -154
  69. package/build/src/cli/use-browser-cli.js +20 -15
  70. package/build/src/configuration-resolver.js +54 -47
  71. package/build/src/configuration-types.d.ts +21 -2
  72. package/build/src/configuration-types.d.ts.map +1 -1
  73. package/build/src/configuration-types.js +60 -3
  74. package/build/src/configuration.js +2547 -2240
  75. package/build/src/controller.js +407 -363
  76. package/build/src/current-configuration.js +12 -9
  77. package/build/src/current.js +75 -70
  78. package/build/src/database/annotations-async-hooks.js +22 -16
  79. package/build/src/database/annotations.js +18 -12
  80. package/build/src/database/drivers/base-column.js +179 -155
  81. package/build/src/database/drivers/base-columns-index.js +78 -69
  82. package/build/src/database/drivers/base-foreign-key.js +101 -89
  83. package/build/src/database/drivers/base-table.js +149 -124
  84. package/build/src/database/drivers/base.js +1489 -1306
  85. package/build/src/database/drivers/mssql/column.js +50 -39
  86. package/build/src/database/drivers/mssql/columns-index.js +3 -2
  87. package/build/src/database/drivers/mssql/connect-connection.js +9 -11
  88. package/build/src/database/drivers/mssql/foreign-key.js +9 -8
  89. package/build/src/database/drivers/mssql/index.js +587 -507
  90. package/build/src/database/drivers/mssql/options.js +75 -68
  91. package/build/src/database/drivers/mssql/query-parser.js +3 -2
  92. package/build/src/database/drivers/mssql/sql/alter-table.js +2 -2
  93. package/build/src/database/drivers/mssql/sql/create-database.js +31 -24
  94. package/build/src/database/drivers/mssql/sql/create-index.js +2 -2
  95. package/build/src/database/drivers/mssql/sql/create-table.js +2 -2
  96. package/build/src/database/drivers/mssql/sql/delete.js +16 -14
  97. package/build/src/database/drivers/mssql/sql/drop-database.js +31 -24
  98. package/build/src/database/drivers/mssql/sql/drop-table.js +2 -2
  99. package/build/src/database/drivers/mssql/sql/insert.js +2 -2
  100. package/build/src/database/drivers/mssql/sql/update.js +28 -24
  101. package/build/src/database/drivers/mssql/sql/upsert.js +20 -18
  102. package/build/src/database/drivers/mssql/structure-sql.js +114 -102
  103. package/build/src/database/drivers/mssql/table.js +96 -81
  104. package/build/src/database/drivers/mysql/column.js +92 -75
  105. package/build/src/database/drivers/mysql/columns-index.js +19 -16
  106. package/build/src/database/drivers/mysql/foreign-key.js +9 -8
  107. package/build/src/database/drivers/mysql/index.js +457 -396
  108. package/build/src/database/drivers/mysql/options.js +30 -26
  109. package/build/src/database/drivers/mysql/query-parser.js +3 -2
  110. package/build/src/database/drivers/mysql/query.js +29 -26
  111. package/build/src/database/drivers/mysql/sql/alter-table.js +3 -2
  112. package/build/src/database/drivers/mysql/sql/create-database.js +28 -23
  113. package/build/src/database/drivers/mysql/sql/create-index.js +3 -2
  114. package/build/src/database/drivers/mysql/sql/create-table.js +3 -2
  115. package/build/src/database/drivers/mysql/sql/delete.js +17 -14
  116. package/build/src/database/drivers/mysql/sql/drop-database.js +3 -2
  117. package/build/src/database/drivers/mysql/sql/drop-table.js +3 -2
  118. package/build/src/database/drivers/mysql/sql/insert.js +3 -2
  119. package/build/src/database/drivers/mysql/sql/update.js +29 -24
  120. package/build/src/database/drivers/mysql/sql/upsert.js +10 -8
  121. package/build/src/database/drivers/mysql/structure-sql.js +88 -79
  122. package/build/src/database/drivers/mysql/table.js +98 -83
  123. package/build/src/database/drivers/pgsql/column.js +72 -56
  124. package/build/src/database/drivers/pgsql/columns-index.js +3 -2
  125. package/build/src/database/drivers/pgsql/foreign-key.js +9 -8
  126. package/build/src/database/drivers/pgsql/index.js +438 -377
  127. package/build/src/database/drivers/pgsql/options.js +28 -25
  128. package/build/src/database/drivers/pgsql/query-parser.js +3 -2
  129. package/build/src/database/drivers/pgsql/sql/alter-table.js +3 -2
  130. package/build/src/database/drivers/pgsql/sql/create-database.js +23 -19
  131. package/build/src/database/drivers/pgsql/sql/create-index.js +3 -2
  132. package/build/src/database/drivers/pgsql/sql/create-table.js +3 -2
  133. package/build/src/database/drivers/pgsql/sql/delete.js +17 -14
  134. package/build/src/database/drivers/pgsql/sql/drop-database.js +3 -2
  135. package/build/src/database/drivers/pgsql/sql/drop-table.js +3 -2
  136. package/build/src/database/drivers/pgsql/sql/insert.js +3 -2
  137. package/build/src/database/drivers/pgsql/sql/update.js +29 -24
  138. package/build/src/database/drivers/pgsql/sql/upsert.js +11 -9
  139. package/build/src/database/drivers/pgsql/structure-sql.js +120 -108
  140. package/build/src/database/drivers/pgsql/table.js +77 -60
  141. package/build/src/database/drivers/sqlite/base.js +478 -405
  142. package/build/src/database/drivers/sqlite/column.js +69 -54
  143. package/build/src/database/drivers/sqlite/columns-index.js +27 -22
  144. package/build/src/database/drivers/sqlite/connection-sql-js.js +42 -35
  145. package/build/src/database/drivers/sqlite/foreign-key.js +21 -18
  146. package/build/src/database/drivers/sqlite/index.js +373 -330
  147. package/build/src/database/drivers/sqlite/index.native.js +64 -55
  148. package/build/src/database/drivers/sqlite/index.web.js +87 -69
  149. package/build/src/database/drivers/sqlite/options.js +28 -25
  150. package/build/src/database/drivers/sqlite/query-parser.js +3 -2
  151. package/build/src/database/drivers/sqlite/query.js +24 -21
  152. package/build/src/database/drivers/sqlite/query.native.js +25 -20
  153. package/build/src/database/drivers/sqlite/query.web.js +37 -30
  154. package/build/src/database/drivers/sqlite/sql/alter-table.js +179 -159
  155. package/build/src/database/drivers/sqlite/sql/create-index.js +3 -2
  156. package/build/src/database/drivers/sqlite/sql/create-table.js +3 -2
  157. package/build/src/database/drivers/sqlite/sql/delete.js +22 -17
  158. package/build/src/database/drivers/sqlite/sql/drop-table.js +3 -2
  159. package/build/src/database/drivers/sqlite/sql/insert.js +3 -2
  160. package/build/src/database/drivers/sqlite/sql/update.js +29 -24
  161. package/build/src/database/drivers/sqlite/sql/upsert.js +11 -9
  162. package/build/src/database/drivers/sqlite/structure-sql.js +52 -49
  163. package/build/src/database/drivers/sqlite/table-rebuilder.js +75 -62
  164. package/build/src/database/drivers/sqlite/table.js +125 -102
  165. package/build/src/database/drivers/structure-sql/utils.js +17 -14
  166. package/build/src/database/handler.js +10 -9
  167. package/build/src/database/initializer-from-require-context.js +87 -76
  168. package/build/src/database/migration/index.js +395 -332
  169. package/build/src/database/migrator/files-finder.js +50 -40
  170. package/build/src/database/migrator/types.js +30 -2
  171. package/build/src/database/migrator.js +526 -454
  172. package/build/src/database/pool/async-tracked-multi-connection.js +1147 -997
  173. package/build/src/database/pool/base-methods-forward.js +43 -40
  174. package/build/src/database/pool/base.js +343 -298
  175. package/build/src/database/pool/single-multi-use.js +110 -93
  176. package/build/src/database/query/alter-table-base.js +99 -84
  177. package/build/src/database/query/base.js +46 -39
  178. package/build/src/database/query/create-database-base.js +30 -25
  179. package/build/src/database/query/create-index-base.js +94 -75
  180. package/build/src/database/query/create-table-base.js +193 -151
  181. package/build/src/database/query/delete-base.js +16 -14
  182. package/build/src/database/query/drop-database-base.js +28 -23
  183. package/build/src/database/query/drop-table-base.js +53 -42
  184. package/build/src/database/query/from-base.js +33 -30
  185. package/build/src/database/query/from-plain.js +13 -11
  186. package/build/src/database/query/from-table.js +15 -13
  187. package/build/src/database/query/index.js +472 -410
  188. package/build/src/database/query/insert-base.js +164 -143
  189. package/build/src/database/query/join-base.js +40 -35
  190. package/build/src/database/query/join-object.js +153 -128
  191. package/build/src/database/query/join-plain.js +15 -13
  192. package/build/src/database/query/join-tracker.js +90 -76
  193. package/build/src/database/query/model-class-query.js +1370 -1134
  194. package/build/src/database/query/order-base.js +30 -27
  195. package/build/src/database/query/order-column.js +53 -44
  196. package/build/src/database/query/order-plain.js +24 -20
  197. package/build/src/database/query/preloader/belongs-to.js +258 -210
  198. package/build/src/database/query/preloader/ensure-model-class-initialized.js +9 -8
  199. package/build/src/database/query/preloader/has-many.js +301 -240
  200. package/build/src/database/query/preloader/has-one.js +117 -91
  201. package/build/src/database/query/preloader/selection.js +129 -117
  202. package/build/src/database/query/preloader.js +185 -160
  203. package/build/src/database/query/query-data.js +201 -157
  204. package/build/src/database/query/select-base.js +27 -25
  205. package/build/src/database/query/select-plain.js +15 -13
  206. package/build/src/database/query/select-table-and-column.js +25 -21
  207. package/build/src/database/query/update-base.js +38 -35
  208. package/build/src/database/query/upsert-base.js +100 -93
  209. package/build/src/database/query/where-base.js +35 -32
  210. package/build/src/database/query/where-combinator.d.ts.map +1 -1
  211. package/build/src/database/query/where-combinator.js +28 -26
  212. package/build/src/database/query/where-hash.js +68 -61
  213. package/build/src/database/query/where-model-class-hash.js +469 -414
  214. package/build/src/database/query/where-not.js +20 -18
  215. package/build/src/database/query/where-plain.js +17 -15
  216. package/build/src/database/query/with-count.js +159 -125
  217. package/build/src/database/query-parser/base-query-parser.js +37 -32
  218. package/build/src/database/query-parser/from-parser.js +45 -36
  219. package/build/src/database/query-parser/group-parser.js +50 -42
  220. package/build/src/database/query-parser/joins-parser.js +33 -28
  221. package/build/src/database/query-parser/limit-parser.js +70 -67
  222. package/build/src/database/query-parser/options.js +82 -75
  223. package/build/src/database/query-parser/order-parser.js +40 -36
  224. package/build/src/database/query-parser/select-parser.js +60 -49
  225. package/build/src/database/query-parser/where-parser.js +41 -36
  226. package/build/src/database/record/acts-as-list.js +273 -235
  227. package/build/src/database/record/attachments/download.js +45 -44
  228. package/build/src/database/record/attachments/handle.js +161 -141
  229. package/build/src/database/record/attachments/normalize-input.js +138 -128
  230. package/build/src/database/record/attachments/storage-drivers/filesystem.js +91 -77
  231. package/build/src/database/record/attachments/storage-drivers/native.js +121 -112
  232. package/build/src/database/record/attachments/storage-drivers/s3.js +208 -177
  233. package/build/src/database/record/attachments/store.d.ts +1 -1
  234. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  235. package/build/src/database/record/attachments/store.js +540 -468
  236. package/build/src/database/record/index.d.ts +17 -15
  237. package/build/src/database/record/index.d.ts.map +1 -1
  238. package/build/src/database/record/index.js +3894 -3361
  239. package/build/src/database/record/instance-relationships/base.js +268 -234
  240. package/build/src/database/record/instance-relationships/belongs-to.js +73 -58
  241. package/build/src/database/record/instance-relationships/has-many.js +264 -225
  242. package/build/src/database/record/instance-relationships/has-one.js +105 -85
  243. package/build/src/database/record/record-not-found-error.js +2 -3
  244. package/build/src/database/record/relationships/base.d.ts +2 -2
  245. package/build/src/database/record/relationships/base.d.ts.map +1 -1
  246. package/build/src/database/record/relationships/base.js +167 -145
  247. package/build/src/database/record/relationships/belongs-to.js +51 -44
  248. package/build/src/database/record/relationships/has-many.js +40 -32
  249. package/build/src/database/record/relationships/has-one.js +40 -32
  250. package/build/src/database/record/state-machine.js +208 -156
  251. package/build/src/database/record/user-module.js +38 -32
  252. package/build/src/database/record/validators/base.js +24 -22
  253. package/build/src/database/record/validators/format.js +46 -36
  254. package/build/src/database/record/validators/presence.js +20 -18
  255. package/build/src/database/record/validators/uniqueness.js +117 -99
  256. package/build/src/database/table-data/index.js +231 -199
  257. package/build/src/database/table-data/table-column.js +382 -338
  258. package/build/src/database/table-data/table-foreign-key.js +66 -57
  259. package/build/src/database/table-data/table-index.js +36 -29
  260. package/build/src/database/table-data/table-reference.js +10 -10
  261. package/build/src/database/use-database.js +40 -32
  262. package/build/src/environment-handlers/base.js +544 -484
  263. package/build/src/environment-handlers/browser.js +294 -241
  264. package/build/src/environment-handlers/node/cli/commands/background-jobs-main.js +19 -16
  265. package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.js +21 -18
  266. package/build/src/environment-handlers/node/cli/commands/background-jobs-worker.js +29 -22
  267. package/build/src/environment-handlers/node/cli/commands/beacon.js +19 -16
  268. package/build/src/environment-handlers/node/cli/commands/cli-command-context.js +15 -14
  269. package/build/src/environment-handlers/node/cli/commands/console.js +120 -99
  270. package/build/src/environment-handlers/node/cli/commands/db/schema/dump.js +39 -34
  271. package/build/src/environment-handlers/node/cli/commands/db/schema/load.js +63 -57
  272. package/build/src/environment-handlers/node/cli/commands/db/seed.js +63 -51
  273. package/build/src/environment-handlers/node/cli/commands/destroy/migration.js +40 -32
  274. package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
  275. package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +353 -298
  276. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +844 -729
  277. package/build/src/environment-handlers/node/cli/commands/generate/migration.js +38 -34
  278. package/build/src/environment-handlers/node/cli/commands/generate/model.js +38 -34
  279. package/build/src/environment-handlers/node/cli/commands/init.js +61 -56
  280. package/build/src/environment-handlers/node/cli/commands/routes.js +59 -51
  281. package/build/src/environment-handlers/node/cli/commands/run-script.js +68 -54
  282. package/build/src/environment-handlers/node/cli/commands/runner.js +74 -56
  283. package/build/src/environment-handlers/node/cli/commands/server.js +106 -93
  284. package/build/src/environment-handlers/node/cli/commands/test.js +113 -97
  285. package/build/src/environment-handlers/node.js +874 -753
  286. package/build/src/error-logger.js +21 -22
  287. package/build/src/frontend-model-controller.d.ts +6 -6
  288. package/build/src/frontend-model-controller.d.ts.map +1 -1
  289. package/build/src/frontend-model-controller.js +3288 -2788
  290. package/build/src/frontend-model-resource/base-resource.d.ts +18 -17
  291. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  292. package/build/src/frontend-model-resource/base-resource.js +869 -759
  293. package/build/src/frontend-models/base.d.ts +19 -12
  294. package/build/src/frontend-models/base.d.ts.map +1 -1
  295. package/build/src/frontend-models/base.js +3602 -3114
  296. package/build/src/frontend-models/clear-pending-debounced-callback.js +8 -7
  297. package/build/src/frontend-models/event-hook-models.js +21 -16
  298. package/build/src/frontend-models/model-registry.js +11 -9
  299. package/build/src/frontend-models/outgoing-event-buffer.js +17 -10
  300. package/build/src/frontend-models/preloader.d.ts +6 -6
  301. package/build/src/frontend-models/preloader.d.ts.map +1 -1
  302. package/build/src/frontend-models/preloader.js +149 -131
  303. package/build/src/frontend-models/query.d.ts.map +1 -1
  304. package/build/src/frontend-models/query.js +1855 -1560
  305. package/build/src/frontend-models/resource-config-validation.js +37 -27
  306. package/build/src/frontend-models/resource-definition.js +288 -234
  307. package/build/src/frontend-models/transport-serialization.js +266 -203
  308. package/build/src/frontend-models/use-created-event.js +7 -5
  309. package/build/src/frontend-models/use-destroyed-event.js +93 -80
  310. package/build/src/frontend-models/use-model-class-event.js +91 -79
  311. package/build/src/frontend-models/use-updated-event.js +97 -84
  312. package/build/src/frontend-models/websocket-channel.js +441 -381
  313. package/build/src/frontend-models/websocket-publishers.js +173 -140
  314. package/build/src/http-client/header.js +14 -13
  315. package/build/src/http-client/index.js +132 -116
  316. package/build/src/http-client/request.js +87 -71
  317. package/build/src/http-client/response.js +140 -122
  318. package/build/src/http-client/websocket-client.js +17 -15
  319. package/build/src/http-server/client/index.js +465 -409
  320. package/build/src/http-server/client/params-to-object.js +135 -124
  321. package/build/src/http-server/client/request-buffer/form-data-part.js +132 -111
  322. package/build/src/http-server/client/request-buffer/header.js +16 -15
  323. package/build/src/http-server/client/request-buffer/index.js +506 -446
  324. package/build/src/http-server/client/request-parser.js +186 -163
  325. package/build/src/http-server/client/request-runner.js +259 -226
  326. package/build/src/http-server/client/request-timing.js +151 -132
  327. package/build/src/http-server/client/request.js +108 -96
  328. package/build/src/http-server/client/response.js +235 -213
  329. package/build/src/http-server/client/uploaded-file/memory-uploaded-file.js +29 -25
  330. package/build/src/http-server/client/uploaded-file/temporary-uploaded-file.js +29 -25
  331. package/build/src/http-server/client/uploaded-file/uploaded-file.js +33 -33
  332. package/build/src/http-server/client/websocket-request.js +137 -114
  333. package/build/src/http-server/client/websocket-session.js +1657 -1452
  334. package/build/src/http-server/cookie.js +236 -216
  335. package/build/src/http-server/development-reloader.js +221 -190
  336. package/build/src/http-server/index.js +525 -451
  337. package/build/src/http-server/remote-address.js +50 -38
  338. package/build/src/http-server/server-client.js +208 -181
  339. package/build/src/http-server/server-lock.js +167 -153
  340. package/build/src/http-server/websocket-channel-subscribers.js +93 -81
  341. package/build/src/http-server/websocket-channel.js +117 -104
  342. package/build/src/http-server/websocket-connection.js +104 -96
  343. package/build/src/http-server/websocket-event-log-store.js +404 -350
  344. package/build/src/http-server/websocket-events-host.js +164 -145
  345. package/build/src/http-server/websocket-events.js +47 -47
  346. package/build/src/http-server/worker-handler/channel-subscriber-dispatch.js +14 -13
  347. package/build/src/http-server/worker-handler/in-process.js +141 -123
  348. package/build/src/http-server/worker-handler/index.js +349 -313
  349. package/build/src/http-server/worker-handler/worker-script.js +5 -4
  350. package/build/src/http-server/worker-handler/worker-thread.js +269 -240
  351. package/build/src/initializer.js +36 -31
  352. package/build/src/jobs/mail-delivery.js +15 -13
  353. package/build/src/logger/base-logger.js +26 -24
  354. package/build/src/logger/console-logger.js +23 -21
  355. package/build/src/logger/file-logger.js +31 -29
  356. package/build/src/logger/outputs/array-output.js +42 -37
  357. package/build/src/logger/outputs/console-output.js +24 -20
  358. package/build/src/logger/outputs/file-output.js +48 -43
  359. package/build/src/logger/outputs/stdout-output.js +48 -39
  360. package/build/src/logger.js +394 -338
  361. package/build/src/mailer/backends/smtp.js +163 -134
  362. package/build/src/mailer/base.js +251 -211
  363. package/build/src/mailer/delivery.js +64 -56
  364. package/build/src/mailer/index.js +22 -4
  365. package/build/src/mailer.js +13 -4
  366. package/build/src/plugins/sqljs-wasm-route-controller.js +52 -42
  367. package/build/src/plugins/sqljs-wasm-route.js +38 -28
  368. package/build/src/record-payload-values.js +28 -25
  369. package/build/src/routes/app-routes.js +14 -12
  370. package/build/src/routes/base-route.js +130 -112
  371. package/build/src/routes/basic-route.js +102 -83
  372. package/build/src/routes/built-in/debug/controller.js +10 -10
  373. package/build/src/routes/built-in/errors/controller.js +5 -5
  374. package/build/src/routes/get-route.js +63 -50
  375. package/build/src/routes/hooks/frontend-model-command-route-hook.js +80 -66
  376. package/build/src/routes/index.js +43 -36
  377. package/build/src/routes/namespace-route.js +47 -38
  378. package/build/src/routes/plugin-routes.js +124 -107
  379. package/build/src/routes/post-route.js +62 -51
  380. package/build/src/routes/resolver.js +494 -422
  381. package/build/src/routes/resource-route.js +143 -124
  382. package/build/src/routes/root-route.js +8 -7
  383. package/build/src/testing/base-expect.js +14 -13
  384. package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +405 -329
  385. package/build/src/testing/browser-test-app.js +29 -23
  386. package/build/src/testing/expect-to-change.js +50 -41
  387. package/build/src/testing/expect-utils.js +184 -139
  388. package/build/src/testing/expect.js +731 -638
  389. package/build/src/testing/request-client.js +85 -70
  390. package/build/src/testing/test-files-finder.js +339 -285
  391. package/build/src/testing/test-filter-parser.js +155 -124
  392. package/build/src/testing/test-runner.js +1020 -883
  393. package/build/src/testing/test-suite-splitter.js +142 -114
  394. package/build/src/testing/test.js +256 -216
  395. package/build/src/utils/backtrace-cleaner-node.js +69 -62
  396. package/build/src/utils/backtrace-cleaner.js +216 -188
  397. package/build/src/utils/ensure-error.js +7 -7
  398. package/build/src/utils/event-emitter.js +6 -4
  399. package/build/src/utils/file-exists.js +10 -9
  400. package/build/src/utils/format-value.js +76 -67
  401. package/build/src/utils/model-scope.js +31 -27
  402. package/build/src/utils/nest-callbacks.js +13 -10
  403. package/build/src/utils/plain-object.js +6 -5
  404. package/build/src/utils/ransack.d.ts.map +1 -1
  405. package/build/src/utils/ransack.js +563 -449
  406. package/build/src/utils/rest-args-error.js +6 -5
  407. package/build/src/utils/singularize-model-name.js +11 -9
  408. package/build/src/utils/split-sql-statements.js +79 -68
  409. package/build/src/utils/to-import-specifier.js +30 -24
  410. package/build/src/utils/with-tracked-stack-async-hooks.js +74 -60
  411. package/build/src/utils/with-tracked-stack.js +18 -14
  412. package/build/src/velocious-error.js +30 -27
  413. package/index.js +1 -0
  414. package/package.json +10 -4
  415. package/scripts/clean-build.js +8 -0
  416. package/scripts/ensure-bin-executable.js +13 -0
  417. package/scripts/run-tests.js +37 -0
  418. package/scripts/test-browser.js +486 -0
  419. package/src/application.js +229 -0
  420. package/src/authorization/ability.js +329 -0
  421. package/src/authorization/base-resource.js +143 -0
  422. package/src/background-jobs/client.js +50 -0
  423. package/src/background-jobs/cron-expression.js +277 -0
  424. package/src/background-jobs/forked-runner-child.js +86 -0
  425. package/src/background-jobs/job-record.js +13 -0
  426. package/src/background-jobs/job-registry.js +92 -0
  427. package/src/background-jobs/job-runner.js +107 -0
  428. package/src/background-jobs/job.js +77 -0
  429. package/src/background-jobs/json-socket.js +78 -0
  430. package/src/background-jobs/main.js +926 -0
  431. package/src/background-jobs/normalize-error.js +26 -0
  432. package/src/background-jobs/scheduler.js +274 -0
  433. package/src/background-jobs/socket-request.js +68 -0
  434. package/src/background-jobs/status-reporter.js +101 -0
  435. package/src/background-jobs/store.js +994 -0
  436. package/src/background-jobs/types.js +70 -0
  437. package/src/background-jobs/web/authorization.js +89 -0
  438. package/src/background-jobs/web/controller.js +280 -0
  439. package/src/background-jobs/web/index.js +57 -0
  440. package/src/background-jobs/web/path-matcher.js +74 -0
  441. package/src/background-jobs/web/registry.js +49 -0
  442. package/src/background-jobs/worker.js +683 -0
  443. package/src/beacon/client.js +330 -0
  444. package/src/beacon/in-process-broker.js +71 -0
  445. package/src/beacon/in-process-client.js +139 -0
  446. package/src/beacon/server.js +148 -0
  447. package/src/beacon/types.js +55 -0
  448. package/src/cli/base-command.js +67 -0
  449. package/src/cli/browser-cli.js +45 -0
  450. package/src/cli/commands/background-jobs-main.js +7 -0
  451. package/src/cli/commands/background-jobs-runner.js +7 -0
  452. package/src/cli/commands/background-jobs-worker.js +7 -0
  453. package/src/cli/commands/beacon.js +7 -0
  454. package/src/cli/commands/console.js +12 -0
  455. package/src/cli/commands/db/base-command.js +82 -0
  456. package/src/cli/commands/db/create.js +64 -0
  457. package/src/cli/commands/db/drop.js +75 -0
  458. package/src/cli/commands/db/migrate.js +17 -0
  459. package/src/cli/commands/db/reset.js +22 -0
  460. package/src/cli/commands/db/rollback.js +15 -0
  461. package/src/cli/commands/db/schema/dump.js +12 -0
  462. package/src/cli/commands/db/schema/load.js +12 -0
  463. package/src/cli/commands/db/seed.js +12 -0
  464. package/src/cli/commands/db/tenants/check.js +38 -0
  465. package/src/cli/commands/db/tenants/create.js +33 -0
  466. package/src/cli/commands/db/tenants/migrate.js +49 -0
  467. package/src/cli/commands/destroy/migration.js +7 -0
  468. package/src/cli/commands/generate/base-models.js +7 -0
  469. package/src/cli/commands/generate/frontend-models.js +12 -0
  470. package/src/cli/commands/generate/migration.js +7 -0
  471. package/src/cli/commands/generate/model.js +7 -0
  472. package/src/cli/commands/init.js +11 -0
  473. package/src/cli/commands/routes.js +7 -0
  474. package/src/cli/commands/run-script.js +12 -0
  475. package/src/cli/commands/runner.js +12 -0
  476. package/src/cli/commands/server.js +7 -0
  477. package/src/cli/commands/test.js +9 -0
  478. package/src/cli/index.js +152 -0
  479. package/src/cli/tenant-database-command-helper.js +198 -0
  480. package/src/cli/use-browser-cli.js +30 -0
  481. package/src/configuration-resolver.js +65 -0
  482. package/src/configuration-types.js +429 -0
  483. package/src/configuration.js +2590 -0
  484. package/src/controller.js +421 -0
  485. package/src/current-configuration.js +31 -0
  486. package/src/current.js +80 -0
  487. package/src/database/annotations-async-hooks.js +47 -0
  488. package/src/database/annotations.js +40 -0
  489. package/src/database/drivers/base-column.js +182 -0
  490. package/src/database/drivers/base-columns-index.js +81 -0
  491. package/src/database/drivers/base-foreign-key.js +104 -0
  492. package/src/database/drivers/base-table.js +156 -0
  493. package/src/database/drivers/base.js +1609 -0
  494. package/src/database/drivers/mssql/column.js +74 -0
  495. package/src/database/drivers/mssql/columns-index.js +6 -0
  496. package/src/database/drivers/mssql/connect-connection.js +16 -0
  497. package/src/database/drivers/mssql/foreign-key.js +12 -0
  498. package/src/database/drivers/mssql/index.js +590 -0
  499. package/src/database/drivers/mssql/options.js +79 -0
  500. package/src/database/drivers/mssql/query-parser.js +6 -0
  501. package/src/database/drivers/mssql/sql/alter-table.js +4 -0
  502. package/src/database/drivers/mssql/sql/create-database.js +36 -0
  503. package/src/database/drivers/mssql/sql/create-index.js +4 -0
  504. package/src/database/drivers/mssql/sql/create-table.js +4 -0
  505. package/src/database/drivers/mssql/sql/delete.js +19 -0
  506. package/src/database/drivers/mssql/sql/drop-database.js +36 -0
  507. package/src/database/drivers/mssql/sql/drop-table.js +4 -0
  508. package/src/database/drivers/mssql/sql/insert.js +4 -0
  509. package/src/database/drivers/mssql/sql/update.js +31 -0
  510. package/src/database/drivers/mssql/sql/upsert.js +23 -0
  511. package/src/database/drivers/mssql/structure-sql.js +120 -0
  512. package/src/database/drivers/mssql/table.js +145 -0
  513. package/src/database/drivers/mysql/column.js +112 -0
  514. package/src/database/drivers/mysql/columns-index.js +22 -0
  515. package/src/database/drivers/mysql/foreign-key.js +12 -0
  516. package/src/database/drivers/mysql/index.js +473 -0
  517. package/src/database/drivers/mysql/options.js +34 -0
  518. package/src/database/drivers/mysql/query-parser.js +6 -0
  519. package/src/database/drivers/mysql/query.js +37 -0
  520. package/src/database/drivers/mysql/sql/alter-table.js +6 -0
  521. package/src/database/drivers/mysql/sql/create-database.js +39 -0
  522. package/src/database/drivers/mysql/sql/create-index.js +6 -0
  523. package/src/database/drivers/mysql/sql/create-table.js +6 -0
  524. package/src/database/drivers/mysql/sql/delete.js +21 -0
  525. package/src/database/drivers/mysql/sql/drop-database.js +6 -0
  526. package/src/database/drivers/mysql/sql/drop-table.js +6 -0
  527. package/src/database/drivers/mysql/sql/insert.js +6 -0
  528. package/src/database/drivers/mysql/sql/update.js +33 -0
  529. package/src/database/drivers/mysql/sql/upsert.js +13 -0
  530. package/src/database/drivers/mysql/structure-sql.js +93 -0
  531. package/src/database/drivers/mysql/table.js +121 -0
  532. package/src/database/drivers/pgsql/column.js +90 -0
  533. package/src/database/drivers/pgsql/columns-index.js +6 -0
  534. package/src/database/drivers/pgsql/foreign-key.js +12 -0
  535. package/src/database/drivers/pgsql/index.js +441 -0
  536. package/src/database/drivers/pgsql/options.js +32 -0
  537. package/src/database/drivers/pgsql/query-parser.js +6 -0
  538. package/src/database/drivers/pgsql/sql/alter-table.js +6 -0
  539. package/src/database/drivers/pgsql/sql/create-database.js +38 -0
  540. package/src/database/drivers/pgsql/sql/create-index.js +6 -0
  541. package/src/database/drivers/pgsql/sql/create-table.js +6 -0
  542. package/src/database/drivers/pgsql/sql/delete.js +21 -0
  543. package/src/database/drivers/pgsql/sql/drop-database.js +6 -0
  544. package/src/database/drivers/pgsql/sql/drop-table.js +6 -0
  545. package/src/database/drivers/pgsql/sql/insert.js +6 -0
  546. package/src/database/drivers/pgsql/sql/update.js +33 -0
  547. package/src/database/drivers/pgsql/sql/upsert.js +14 -0
  548. package/src/database/drivers/pgsql/structure-sql.js +126 -0
  549. package/src/database/drivers/pgsql/table.js +135 -0
  550. package/src/database/drivers/sqlite/base.js +509 -0
  551. package/src/database/drivers/sqlite/column.js +75 -0
  552. package/src/database/drivers/sqlite/columns-index.js +30 -0
  553. package/src/database/drivers/sqlite/connection-sql-js.js +46 -0
  554. package/src/database/drivers/sqlite/foreign-key.js +24 -0
  555. package/src/database/drivers/sqlite/index.js +394 -0
  556. package/src/database/drivers/sqlite/index.native.js +72 -0
  557. package/src/database/drivers/sqlite/index.web.js +99 -0
  558. package/src/database/drivers/sqlite/options.js +32 -0
  559. package/src/database/drivers/sqlite/query-parser.js +6 -0
  560. package/src/database/drivers/sqlite/query.js +35 -0
  561. package/src/database/drivers/sqlite/query.native.js +35 -0
  562. package/src/database/drivers/sqlite/query.web.js +49 -0
  563. package/src/database/drivers/sqlite/sql/alter-table.js +187 -0
  564. package/src/database/drivers/sqlite/sql/create-index.js +6 -0
  565. package/src/database/drivers/sqlite/sql/create-table.js +6 -0
  566. package/src/database/drivers/sqlite/sql/delete.js +26 -0
  567. package/src/database/drivers/sqlite/sql/drop-table.js +6 -0
  568. package/src/database/drivers/sqlite/sql/insert.js +6 -0
  569. package/src/database/drivers/sqlite/sql/update.js +33 -0
  570. package/src/database/drivers/sqlite/sql/upsert.js +14 -0
  571. package/src/database/drivers/sqlite/structure-sql.js +56 -0
  572. package/src/database/drivers/sqlite/table-rebuilder.js +96 -0
  573. package/src/database/drivers/sqlite/table.js +131 -0
  574. package/src/database/drivers/structure-sql/utils.js +35 -0
  575. package/src/database/handler.js +13 -0
  576. package/src/database/initializer-from-require-context.js +101 -0
  577. package/src/database/migration/index.js +438 -0
  578. package/src/database/migrator/files-finder.js +55 -0
  579. package/src/database/migrator/types.js +31 -0
  580. package/src/database/migrator.js +557 -0
  581. package/src/database/pool/async-tracked-multi-connection.js +1164 -0
  582. package/src/database/pool/base-methods-forward.js +52 -0
  583. package/src/database/pool/base.js +380 -0
  584. package/src/database/pool/single-multi-use.js +118 -0
  585. package/src/database/query/alter-table-base.js +104 -0
  586. package/src/database/query/base.js +49 -0
  587. package/src/database/query/create-database-base.js +42 -0
  588. package/src/database/query/create-index-base.js +117 -0
  589. package/src/database/query/create-table-base.js +205 -0
  590. package/src/database/query/delete-base.js +19 -0
  591. package/src/database/query/drop-database-base.js +38 -0
  592. package/src/database/query/drop-table-base.js +58 -0
  593. package/src/database/query/from-base.js +36 -0
  594. package/src/database/query/from-plain.js +16 -0
  595. package/src/database/query/from-table.js +18 -0
  596. package/src/database/query/index.js +533 -0
  597. package/src/database/query/insert-base.js +172 -0
  598. package/src/database/query/join-base.js +43 -0
  599. package/src/database/query/join-object.js +167 -0
  600. package/src/database/query/join-plain.js +18 -0
  601. package/src/database/query/join-tracker.js +93 -0
  602. package/src/database/query/model-class-query.js +1577 -0
  603. package/src/database/query/order-base.js +33 -0
  604. package/src/database/query/order-column.js +77 -0
  605. package/src/database/query/order-plain.js +28 -0
  606. package/src/database/query/preloader/belongs-to.js +267 -0
  607. package/src/database/query/preloader/ensure-model-class-initialized.js +18 -0
  608. package/src/database/query/preloader/has-many.js +316 -0
  609. package/src/database/query/preloader/has-one.js +123 -0
  610. package/src/database/query/preloader/selection.js +152 -0
  611. package/src/database/query/preloader.js +201 -0
  612. package/src/database/query/query-data.js +305 -0
  613. package/src/database/query/select-base.js +30 -0
  614. package/src/database/query/select-plain.js +18 -0
  615. package/src/database/query/select-table-and-column.js +28 -0
  616. package/src/database/query/update-base.js +41 -0
  617. package/src/database/query/upsert-base.js +103 -0
  618. package/src/database/query/where-base.js +38 -0
  619. package/src/database/query/where-combinator.js +31 -0
  620. package/src/database/query/where-hash.js +77 -0
  621. package/src/database/query/where-model-class-hash.js +505 -0
  622. package/src/database/query/where-not.js +23 -0
  623. package/src/database/query/where-plain.js +20 -0
  624. package/src/database/query/with-count.js +219 -0
  625. package/src/database/query-parser/base-query-parser.js +40 -0
  626. package/src/database/query-parser/from-parser.js +49 -0
  627. package/src/database/query-parser/group-parser.js +55 -0
  628. package/src/database/query-parser/joins-parser.js +37 -0
  629. package/src/database/query-parser/limit-parser.js +77 -0
  630. package/src/database/query-parser/options.js +94 -0
  631. package/src/database/query-parser/order-parser.js +45 -0
  632. package/src/database/query-parser/select-parser.js +67 -0
  633. package/src/database/query-parser/where-parser.js +46 -0
  634. package/src/database/record/acts-as-list.js +374 -0
  635. package/src/database/record/attachments/download.js +49 -0
  636. package/src/database/record/attachments/handle.js +188 -0
  637. package/src/database/record/attachments/normalize-input.js +213 -0
  638. package/src/database/record/attachments/storage-drivers/filesystem.js +114 -0
  639. package/src/database/record/attachments/storage-drivers/native.js +146 -0
  640. package/src/database/record/attachments/storage-drivers/s3.js +245 -0
  641. package/src/database/record/attachments/store.js +591 -0
  642. package/src/database/record/index.js +3970 -0
  643. package/src/database/record/instance-relationships/base.js +289 -0
  644. package/src/database/record/instance-relationships/belongs-to.js +84 -0
  645. package/src/database/record/instance-relationships/has-many.js +284 -0
  646. package/src/database/record/instance-relationships/has-one.js +117 -0
  647. package/src/database/record/record-not-found-error.js +3 -0
  648. package/src/database/record/relationships/base.js +195 -0
  649. package/src/database/record/relationships/belongs-to.js +57 -0
  650. package/src/database/record/relationships/has-many.js +46 -0
  651. package/src/database/record/relationships/has-one.js +46 -0
  652. package/src/database/record/state-machine.js +278 -0
  653. package/src/database/record/user-module.js +43 -0
  654. package/src/database/record/validators/base.js +27 -0
  655. package/src/database/record/validators/format.js +50 -0
  656. package/src/database/record/validators/presence.js +24 -0
  657. package/src/database/record/validators/uniqueness.js +124 -0
  658. package/src/database/table-data/index.js +241 -0
  659. package/src/database/table-data/table-column.js +416 -0
  660. package/src/database/table-data/table-foreign-key.js +69 -0
  661. package/src/database/table-data/table-index.js +46 -0
  662. package/src/database/table-data/table-reference.js +13 -0
  663. package/src/database/use-database.js +48 -0
  664. package/src/environment-handlers/base.js +561 -0
  665. package/src/environment-handlers/browser.js +338 -0
  666. package/src/environment-handlers/node/cli/commands/background-jobs-main.js +21 -0
  667. package/src/environment-handlers/node/cli/commands/background-jobs-runner.js +24 -0
  668. package/src/environment-handlers/node/cli/commands/background-jobs-worker.js +47 -0
  669. package/src/environment-handlers/node/cli/commands/beacon.js +21 -0
  670. package/src/environment-handlers/node/cli/commands/cli-command-context.js +31 -0
  671. package/src/environment-handlers/node/cli/commands/console.js +149 -0
  672. package/src/environment-handlers/node/cli/commands/db/schema/dump.js +43 -0
  673. package/src/environment-handlers/node/cli/commands/db/schema/load.js +69 -0
  674. package/src/environment-handlers/node/cli/commands/db/seed.js +79 -0
  675. package/src/environment-handlers/node/cli/commands/destroy/migration.js +47 -0
  676. package/src/environment-handlers/node/cli/commands/generate/base-models.js +367 -0
  677. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +872 -0
  678. package/src/environment-handlers/node/cli/commands/generate/migration.js +45 -0
  679. package/src/environment-handlers/node/cli/commands/generate/model.js +45 -0
  680. package/src/environment-handlers/node/cli/commands/init.js +68 -0
  681. package/src/environment-handlers/node/cli/commands/routes.js +63 -0
  682. package/src/environment-handlers/node/cli/commands/run-script.js +85 -0
  683. package/src/environment-handlers/node/cli/commands/runner.js +84 -0
  684. package/src/environment-handlers/node/cli/commands/server.js +151 -0
  685. package/src/environment-handlers/node/cli/commands/test.js +118 -0
  686. package/src/environment-handlers/node.js +887 -0
  687. package/src/error-logger.js +30 -0
  688. package/src/frontend-model-controller.js +3491 -0
  689. package/src/frontend-model-resource/base-resource.js +935 -0
  690. package/src/frontend-models/base.js +4004 -0
  691. package/src/frontend-models/clear-pending-debounced-callback.js +16 -0
  692. package/src/frontend-models/event-hook-models.js +49 -0
  693. package/src/frontend-models/model-registry.js +28 -0
  694. package/src/frontend-models/outgoing-event-buffer.js +51 -0
  695. package/src/frontend-models/preloader.js +169 -0
  696. package/src/frontend-models/query.js +2245 -0
  697. package/src/frontend-models/resource-config-validation.js +56 -0
  698. package/src/frontend-models/resource-definition.js +399 -0
  699. package/src/frontend-models/transport-serialization.js +369 -0
  700. package/src/frontend-models/use-created-event.js +21 -0
  701. package/src/frontend-models/use-destroyed-event.js +148 -0
  702. package/src/frontend-models/use-model-class-event.js +164 -0
  703. package/src/frontend-models/use-updated-event.js +152 -0
  704. package/src/frontend-models/websocket-channel.js +494 -0
  705. package/src/frontend-models/websocket-publishers.js +224 -0
  706. package/src/http-client/header.js +17 -0
  707. package/src/http-client/index.js +139 -0
  708. package/src/http-client/request.js +94 -0
  709. package/src/http-client/response.js +151 -0
  710. package/src/http-client/websocket-client.js +27 -0
  711. package/src/http-server/client/index.js +507 -0
  712. package/src/http-server/client/params-to-object.js +152 -0
  713. package/src/http-server/client/request-buffer/form-data-part.js +139 -0
  714. package/src/http-server/client/request-buffer/header.js +19 -0
  715. package/src/http-server/client/request-buffer/index.js +535 -0
  716. package/src/http-server/client/request-parser.js +195 -0
  717. package/src/http-server/client/request-runner.js +321 -0
  718. package/src/http-server/client/request-timing.js +171 -0
  719. package/src/http-server/client/request.js +114 -0
  720. package/src/http-server/client/response.js +251 -0
  721. package/src/http-server/client/uploaded-file/memory-uploaded-file.js +32 -0
  722. package/src/http-server/client/uploaded-file/temporary-uploaded-file.js +32 -0
  723. package/src/http-server/client/uploaded-file/uploaded-file.js +36 -0
  724. package/src/http-server/client/websocket-request.js +147 -0
  725. package/src/http-server/client/websocket-session.js +1755 -0
  726. package/src/http-server/cookie.js +245 -0
  727. package/src/http-server/development-reloader.js +240 -0
  728. package/src/http-server/index.js +561 -0
  729. package/src/http-server/remote-address.js +77 -0
  730. package/src/http-server/server-client.js +222 -0
  731. package/src/http-server/server-lock.js +178 -0
  732. package/src/http-server/websocket-channel-subscribers.js +110 -0
  733. package/src/http-server/websocket-channel.js +137 -0
  734. package/src/http-server/websocket-connection.js +118 -0
  735. package/src/http-server/websocket-event-log-store.js +433 -0
  736. package/src/http-server/websocket-events-host.js +170 -0
  737. package/src/http-server/websocket-events.js +50 -0
  738. package/src/http-server/worker-handler/channel-subscriber-dispatch.js +28 -0
  739. package/src/http-server/worker-handler/in-process.js +155 -0
  740. package/src/http-server/worker-handler/index.js +370 -0
  741. package/src/http-server/worker-handler/worker-script.js +6 -0
  742. package/src/http-server/worker-handler/worker-thread.js +286 -0
  743. package/src/initializer.js +39 -0
  744. package/src/jobs/.gitkeep +1 -0
  745. package/src/jobs/mail-delivery.js +22 -0
  746. package/src/logger/base-logger.js +34 -0
  747. package/src/logger/console-logger.js +28 -0
  748. package/src/logger/file-logger.js +36 -0
  749. package/src/logger/outputs/array-output.js +50 -0
  750. package/src/logger/outputs/console-output.js +32 -0
  751. package/src/logger/outputs/file-output.js +55 -0
  752. package/src/logger/outputs/stdout-output.js +64 -0
  753. package/src/logger.js +507 -0
  754. package/src/mailer/backends/smtp.js +197 -0
  755. package/src/mailer/base.js +337 -0
  756. package/src/mailer/delivery.js +70 -0
  757. package/src/mailer/index.js +24 -0
  758. package/src/mailer.js +15 -0
  759. package/src/plugins/sqljs-wasm-route-controller.js +70 -0
  760. package/src/plugins/sqljs-wasm-route.js +71 -0
  761. package/src/record-payload-values.js +83 -0
  762. package/src/routes/app-routes.js +17 -0
  763. package/src/routes/base-route.js +133 -0
  764. package/src/routes/basic-route.js +109 -0
  765. package/src/routes/built-in/debug/controller.js +12 -0
  766. package/src/routes/built-in/errors/controller.js +7 -0
  767. package/src/routes/built-in/errors/not-found.ejs +1 -0
  768. package/src/routes/get-route.js +75 -0
  769. package/src/routes/hooks/frontend-model-command-route-hook.js +100 -0
  770. package/src/routes/index.js +50 -0
  771. package/src/routes/namespace-route.js +51 -0
  772. package/src/routes/plugin-routes.js +141 -0
  773. package/src/routes/post-route.js +74 -0
  774. package/src/routes/resolver.js +535 -0
  775. package/src/routes/resource-route.js +154 -0
  776. package/src/routes/root-route.js +11 -0
  777. package/src/templates/configuration.js +61 -0
  778. package/src/templates/generate-migration.js +11 -0
  779. package/src/templates/generate-model.js +6 -0
  780. package/src/templates/routes.js +11 -0
  781. package/src/testing/base-expect.js +17 -0
  782. package/src/testing/browser-frontend-model-event-hook-scenarios.js +520 -0
  783. package/src/testing/browser-test-app.js +32 -0
  784. package/src/testing/expect-to-change.js +55 -0
  785. package/src/testing/expect-utils.js +269 -0
  786. package/src/testing/expect.js +763 -0
  787. package/src/testing/request-client.js +90 -0
  788. package/src/testing/test-files-finder.js +364 -0
  789. package/src/testing/test-filter-parser.js +198 -0
  790. package/src/testing/test-runner.js +1168 -0
  791. package/src/testing/test-suite-splitter.js +177 -0
  792. package/src/testing/test.js +370 -0
  793. package/src/types/external-modules.d.ts +57 -0
  794. package/src/utils/backtrace-cleaner-node.js +87 -0
  795. package/src/utils/backtrace-cleaner.js +266 -0
  796. package/src/utils/ensure-error.js +15 -0
  797. package/src/utils/event-emitter.js +8 -0
  798. package/src/utils/file-exists.js +18 -0
  799. package/src/utils/format-value.js +101 -0
  800. package/src/utils/model-scope.js +56 -0
  801. package/src/utils/nest-callbacks.js +22 -0
  802. package/src/utils/plain-object.js +14 -0
  803. package/src/utils/ransack.js +859 -0
  804. package/src/utils/rest-args-error.js +14 -0
  805. package/src/utils/singularize-model-name.js +18 -0
  806. package/src/utils/split-sql-statements.js +88 -0
  807. package/src/utils/to-import-specifier.js +53 -0
  808. package/src/utils/with-tracked-stack-async-hooks.js +103 -0
  809. package/src/utils/with-tracked-stack.js +38 -0
  810. package/src/velocious-error.js +34 -0
  811. package/tsconfig.json +16 -0
@@ -0,0 +1,3491 @@
1
+ // @ts-check
2
+
3
+ import * as inflection from "inflection"
4
+ import Controller from "./controller.js"
5
+ import Response from "./http-server/client/response.js"
6
+ import FrontendModelBaseResource from "./frontend-model-resource/base-resource.js"
7
+ import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcePath, frontendModelResourcesForBackendProject} from "./frontend-models/resource-definition.js"
8
+ import {normalizeGroup as normalizeQueryGroup, normalizeJoins as normalizeQueryJoins, normalizePluck as normalizeQueryPluck, normalizePreload as normalizeQueryPreload, normalizeSearchOperator as normalizeQuerySearchOperator, normalizeSort as normalizeQuerySort} from "./frontend-models/query.js"
9
+ import {assignSafeProperty, deserializeFrontendModelTransportValue, isBackendModelInstance, serializeFrontendModelTransportValue} from "./frontend-models/transport-serialization.js"
10
+ import RoutesResolver from "./routes/resolver.js"
11
+ import {ValidationError} from "./database/record/index.js"
12
+ import VelociousError from "./velocious-error.js"
13
+ import isPlainObject from "./utils/plain-object.js"
14
+
15
+ /**
16
+ * Runs normalize frontend model preload.
17
+ * @param {import("./database/query/index.js").NestedPreloadRecord | string | string[] | boolean | undefined | null} preload - Preload shorthand.
18
+ * @returns {import("./database/query/index.js").NestedPreloadRecord | null} - Normalized preload.
19
+ */
20
+ function normalizeFrontendModelPreload(preload) {
21
+ if (!preload) return null
22
+
23
+ return normalizeQueryPreload(preload)
24
+ }
25
+
26
+ /**
27
+ * Runs normalize frontend model joins.
28
+ * @param {?} joins - Joins payload.
29
+ * @returns {Record<string, ?> | null} - Normalized relationship-object joins.
30
+ */
31
+ function normalizeFrontendModelJoins(joins) {
32
+ if (!joins) return null
33
+
34
+ try {
35
+ return normalizeQueryJoins(joins)
36
+ } catch (error) {
37
+ throw frontendModelValidationErrorForError(error)
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Runs normalize frontend model select.
43
+ * @param {?} select - Select payload.
44
+ * @param {string | null} [rootModelName] - Optional root model name for shorthand payloads.
45
+ * @returns {Record<string, string[]> | null} - Normalized model-name keyed select record.
46
+ */
47
+ function normalizeFrontendModelSelect(select, rootModelName = null) {
48
+ if (!select) return null
49
+
50
+ if (typeof select === "string") {
51
+ if (!rootModelName) {
52
+ throw frontendModelValidationError("Invalid select shorthand without root model name")
53
+ }
54
+
55
+ return {[rootModelName]: [select]}
56
+ }
57
+
58
+ if (Array.isArray(select)) {
59
+ if (!rootModelName) {
60
+ throw frontendModelValidationError("Invalid select shorthand without root model name")
61
+ }
62
+
63
+ for (const attributeName of select) {
64
+ if (typeof attributeName !== "string") {
65
+ throw frontendModelValidationError(`Invalid select attribute for ${rootModelName}: ${typeof attributeName}`)
66
+ }
67
+ }
68
+
69
+ return {[rootModelName]: Array.from(new Set(select))}
70
+ }
71
+
72
+ if (!isPlainObject(select)) {
73
+ throw frontendModelValidationError(`Invalid select type: ${typeof select}`)
74
+ }
75
+
76
+ /**
77
+ * Normalized.
78
+ @type {Record<string, string[]>} */
79
+ const normalized = {}
80
+
81
+ for (const [modelName, selectValue] of Object.entries(select)) {
82
+ if (typeof selectValue === "string") {
83
+ normalized[modelName] = [selectValue]
84
+ continue
85
+ }
86
+
87
+ if (!Array.isArray(selectValue)) {
88
+ throw frontendModelValidationError(`Invalid select value for ${modelName}: ${typeof selectValue}`)
89
+ }
90
+
91
+ for (const attributeName of selectValue) {
92
+ if (typeof attributeName !== "string") {
93
+ throw frontendModelValidationError(`Invalid select attribute for ${modelName}: ${typeof attributeName}`)
94
+ }
95
+ }
96
+
97
+ normalized[modelName] = Array.from(new Set(selectValue))
98
+ }
99
+
100
+ return normalized
101
+ }
102
+
103
+ /**
104
+ * FrontendModelSearch type.
105
+ * @typedef {object} FrontendModelSearch
106
+ * @property {string[]} path - Relationship path.
107
+ * @property {string} column - Column or attribute name.
108
+ * @property {"eq" | "like" | "notEq" | "gt" | "gteq" | "lt" | "lteq"} operator - Search operator.
109
+ * @property {?} value - Search value.
110
+ */
111
+
112
+ /**
113
+ * FrontendModelSort type.
114
+ * @typedef {object} FrontendModelSort
115
+ * @property {string} column - Attribute name to sort by.
116
+ * @property {"asc" | "desc"} direction - Sort direction.
117
+ * @property {string[]} path - Relationship path from root model.
118
+ */
119
+
120
+ /**
121
+ * FrontendModelGroup type.
122
+ * @typedef {object} FrontendModelGroup
123
+ * @property {string} column - Attribute name to group by.
124
+ * @property {string[]} path - Relationship path from root model.
125
+ */
126
+
127
+ /**
128
+ * FrontendModelPluck type.
129
+ * @typedef {object} FrontendModelPluck
130
+ * @property {string} column - Attribute name to pluck.
131
+ * @property {string[]} path - Relationship path from root model.
132
+ */
133
+
134
+ /**
135
+ * FrontendModelPagination type.
136
+ * @typedef {object} FrontendModelPagination
137
+ * @property {number | null} limit - Maximum number of records.
138
+ * @property {number | null} offset - Number of records to skip.
139
+ * @property {number | null} page - 1-based page number.
140
+ * @property {number | null} perPage - Page size.
141
+ */
142
+
143
+ const frontendModelJoinedPathsSymbol = Symbol("frontendModelJoinedPaths")
144
+ const frontendModelGroupedColumnsSymbol = Symbol("frontendModelGroupedColumns")
145
+ const frontendModelWhereNoMatchSymbol = Symbol("frontendModelWhereNoMatch")
146
+ const frontendModelClientSafeErrorMessage = "Request failed."
147
+ const frontendModelDebugErrorEnvironments = new Set(["development", "test"])
148
+
149
+ /**
150
+ * Runs frontend model query metadata.
151
+ * @param {import("./database/query/model-class-query.js").default} query - Query instance.
152
+ * @returns {import("./database/query/model-class-query.js").default & {[frontendModelJoinedPathsSymbol]?: Set<string>, [frontendModelGroupedColumnsSymbol]?: Set<string>}} - Query metadata access helper.
153
+ */
154
+ function frontendModelQueryMetadata(query) {
155
+ return /** Narrows the runtime value to the documented type. @type {import("./database/query/model-class-query.js").default & {[frontendModelJoinedPathsSymbol]?: Set<string>, [frontendModelGroupedColumnsSymbol]?: Set<string>}} */ (query)
156
+ }
157
+
158
+ /**
159
+ * Runs frontend model validation error.
160
+ * @param {string} message - Validation error message.
161
+ * @returns {VelociousError} - Client-safe validation error.
162
+ */
163
+ function frontendModelValidationError(message) {
164
+ return VelociousError.safe(message, {code: "frontend-model-validation"})
165
+ }
166
+
167
+ /**
168
+ * Runs frontend model validation error for error.
169
+ * @param {?} error - Error raised while normalizing client query params.
170
+ * @returns {VelociousError} - Client-safe validation error preserving the normalizer message.
171
+ */
172
+ function frontendModelValidationErrorForError(error) {
173
+ if (error instanceof VelociousError && error.safeToExpose) return error
174
+
175
+ const message = error instanceof Error
176
+ ? error.message
177
+ : String(error)
178
+
179
+ return frontendModelValidationError(message)
180
+ }
181
+
182
+ /**
183
+ * Whether the error carries an `error.velocious` metadata bag. The
184
+ * presence of any such bag marks the error as "annotated by the
185
+ * developer for the frontend" — the framework treats it as
186
+ * user-facing: surface the message, forward the metadata, and skip
187
+ * the noisy endpoint-error log.
188
+ * @param {?} error - Caught error.
189
+ * @returns {boolean}
190
+ */
191
+ function frontendModelErrorHasVelociousMetadata(error) {
192
+ return Boolean(error && typeof error === "object" && /**
193
+ * Types the following value.
194
+ @type {?} */ (error).velocious && typeof /**
195
+ * Types the following value.
196
+ @type {?} */ (error).velocious === "object")
197
+ }
198
+
199
+ /**
200
+ * Runs frontend model velocious metadata for error.
201
+ * @param {?} error - Caught error.
202
+ * @returns {Record<string, ?> | null}
203
+ */
204
+ function frontendModelVelociousMetadataForError(error) {
205
+ if (!frontendModelErrorHasVelociousMetadata(error)) return null
206
+ return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (/**
207
+ * Types the following value.
208
+ @type {?} */ (error).velocious)
209
+ }
210
+
211
+ /**
212
+ * Runs frontend model client message for error.
213
+ * @param {?} error - Caught error.
214
+ * @returns {string} - Message safe to return to API clients.
215
+ */
216
+ function frontendModelClientMessageForError(error) {
217
+ if (error instanceof VelociousError && error.safeToExpose) {
218
+ return error.message
219
+ }
220
+
221
+ if (frontendModelErrorHasVelociousMetadata(error) && error instanceof Error) {
222
+ return error.message
223
+ }
224
+
225
+ return frontendModelClientSafeErrorMessage
226
+ }
227
+
228
+ /**
229
+ * Runs frontend model debug payload for error.
230
+ * @param {object} args - Arguments.
231
+ * @param {import("./configuration.js").default} args.configuration - Current configuration.
232
+ * @param {string} args.environment - Current environment.
233
+ * @param {?} args.error - Caught error.
234
+ * @returns {Record<string, ?>} - Optional debug payload for non-production environments.
235
+ */
236
+ function frontendModelDebugPayloadForError({configuration, environment, error}) {
237
+ const debugAllowed = frontendModelDebugErrorEnvironments.has(environment) || environment !== "production" && configuration.getExposeInternalErrorsToClients()
238
+
239
+ if (!debugAllowed) {
240
+ return {}
241
+ }
242
+
243
+ if (error instanceof VelociousError && error.safeToExpose) {
244
+ return {}
245
+ }
246
+
247
+ if (frontendModelErrorHasVelociousMetadata(error)) {
248
+ return {}
249
+ }
250
+
251
+ const debugErrorClass = error instanceof Error && error.name
252
+ ? error.name
253
+ : typeof error
254
+ const debugErrorMessage = error instanceof Error
255
+ ? error.message
256
+ : String(error)
257
+ const debugBacktrace = error instanceof Error && typeof error.stack === "string" && error.stack.length > 0
258
+ ? error.stack.split("\n")
259
+ : undefined
260
+
261
+ return {
262
+ debugErrorClass,
263
+ debugErrorMessage,
264
+ ...(debugBacktrace ? {debugBacktrace} : {})
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Runs normalize frontend model searches.
270
+ * @param {?} searches - Search payload.
271
+ * @returns {FrontendModelSearch[]} - Normalized searches.
272
+ */
273
+ function normalizeFrontendModelSearches(searches) {
274
+ if (!searches) return []
275
+
276
+ if (!Array.isArray(searches)) {
277
+ throw frontendModelValidationError(`Invalid searches type: ${typeof searches}`)
278
+ }
279
+
280
+ /**
281
+ * Normalized.
282
+ @type {FrontendModelSearch[]} */
283
+ const normalized = []
284
+
285
+ for (const search of searches) {
286
+ if (!isPlainObject(search)) {
287
+ throw frontendModelValidationError(`Invalid search entry type: ${typeof search}`)
288
+ }
289
+
290
+ const path = search.path
291
+ const column = search.column
292
+ const operator = search.operator
293
+
294
+ if (!Array.isArray(path)) {
295
+ throw frontendModelValidationError("Invalid search path: expected an array")
296
+ }
297
+
298
+ for (const pathEntry of path) {
299
+ if (typeof pathEntry !== "string" || pathEntry.length < 1) {
300
+ throw frontendModelValidationError("Invalid search path entry: expected non-empty string")
301
+ }
302
+ }
303
+
304
+ if (typeof column !== "string" || column.length < 1) {
305
+ throw frontendModelValidationError("Invalid search column: expected non-empty string")
306
+ }
307
+
308
+ if (typeof operator !== "string") {
309
+ throw frontendModelValidationError(`Invalid search operator: ${operator}`)
310
+ }
311
+
312
+ normalized.push({
313
+ column,
314
+ operator: normalizeQuerySearchOperator(operator),
315
+ path: [...path],
316
+ value: search.value
317
+ })
318
+ }
319
+
320
+ return normalized
321
+ }
322
+
323
+ /**
324
+ * Runs normalize frontend model where.
325
+ * @param {?} where - Where payload.
326
+ * @returns {Record<string, ?> | null} - Normalized where hash.
327
+ */
328
+ function normalizeFrontendModelWhere(where) {
329
+ if (!where) return null
330
+
331
+ if (!isPlainObject(where)) {
332
+ throw frontendModelValidationError(`Invalid where type: ${typeof where}`)
333
+ }
334
+
335
+ return where
336
+ }
337
+
338
+ /**
339
+ * Runs normalize frontend model ransack.
340
+ * @param {?} ransack - Ransack payload.
341
+ * @returns {Record<string, ?> | null} - Normalized Ransack hash.
342
+ */
343
+ function normalizeFrontendModelRansack(ransack) {
344
+ if (!ransack) return null
345
+
346
+ if (!isPlainObject(ransack)) {
347
+ throw frontendModelValidationError(`Invalid ransack type: ${typeof ransack}`)
348
+ }
349
+
350
+ return ransack
351
+ }
352
+
353
+ /**
354
+ * Runs normalize frontend model integer param.
355
+ * @param {?} value - Candidate integer.
356
+ * @param {string} name - Param name for errors.
357
+ * @param {number} min - Minimum allowed value.
358
+ * @returns {number | null} - Normalized integer.
359
+ */
360
+ function normalizeFrontendModelIntegerParam(value, name, min) {
361
+ if (value == null) return null
362
+
363
+ if (typeof value !== "number" || !Number.isInteger(value)) {
364
+ throw frontendModelValidationError(`Invalid ${name}: expected integer number`)
365
+ }
366
+
367
+ if (value < min) {
368
+ throw frontendModelValidationError(`Invalid ${name}: expected value >= ${min}`)
369
+ }
370
+
371
+ return value
372
+ }
373
+
374
+ /**
375
+ * Runs normalize frontend model pagination.
376
+ * @param {object} args - Pagination args.
377
+ * @param {?} args.limit - Limit payload.
378
+ * @param {?} args.offset - Offset payload.
379
+ * @param {?} args.page - Page payload.
380
+ * @param {?} args.perPage - Per-page payload.
381
+ * @returns {FrontendModelPagination} - Normalized pagination data.
382
+ */
383
+ function normalizeFrontendModelPagination({limit, offset, page, perPage}) {
384
+ return {
385
+ limit: normalizeFrontendModelIntegerParam(limit, "limit", 0),
386
+ offset: normalizeFrontendModelIntegerParam(offset, "offset", 0),
387
+ page: normalizeFrontendModelIntegerParam(page, "page", 1),
388
+ perPage: normalizeFrontendModelIntegerParam(perPage, "perPage", 1)
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Runs normalize frontend model distinct.
394
+ * @param {?} distinct - Distinct payload.
395
+ * @returns {boolean | null} - Normalized distinct flag when provided.
396
+ */
397
+ function normalizeFrontendModelDistinct(distinct) {
398
+ if (distinct == null) return null
399
+
400
+ if (typeof distinct !== "boolean") {
401
+ throw frontendModelValidationError(`Invalid distinct: expected boolean`)
402
+ }
403
+
404
+ return distinct
405
+ }
406
+
407
+ /**
408
+ * Runs build frontend model join object from path.
409
+ * @param {string[]} path - Relationship path.
410
+ * @returns {Record<string, ?>} - Join object.
411
+ */
412
+ function buildFrontendModelJoinObjectFromPath(path) {
413
+ /**
414
+ * Join object.
415
+ @type {Record<string, ?>} */
416
+ const joinObject = {}
417
+ /**
418
+ * Current node.
419
+ @type {Record<string, ?>} */
420
+ let currentNode = joinObject
421
+
422
+ for (const relationshipName of path) {
423
+ currentNode[relationshipName] = {}
424
+ currentNode = currentNode[relationshipName]
425
+ }
426
+
427
+ return joinObject
428
+ }
429
+
430
+ /**
431
+ * Build a successful single-model frontend-model response payload.
432
+ * @param {Record<string, ?>} model - Serialized model payload.
433
+ * @returns {{model: Record<string, ?>, status: "success"}} - Success response payload.
434
+ */
435
+ function frontendModelSerializedModelSuccess(model) {
436
+ return {model, status: "success"}
437
+ }
438
+
439
+ /**
440
+ * Resolve and validate attachment params shared by attachment commands.
441
+ * @param {Record<string, ?>} params - Frontend-model request params.
442
+ * @returns {{attachmentId: string | undefined, attachmentName: string} | string} - Attachment params or validation error message.
443
+ */
444
+ function frontendModelAttachmentParams(params) {
445
+ const attachmentName = params.attachmentName
446
+
447
+ if (typeof attachmentName !== "string" || attachmentName.length < 1) {
448
+ return "Expected attachmentName."
449
+ }
450
+
451
+ return {
452
+ attachmentId: typeof params.attachmentId === "string" ? params.attachmentId : undefined,
453
+ attachmentName
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Extract mutation attributes shared by create and update commands.
459
+ * @param {Record<string, ?>} params - Frontend-model request params.
460
+ * @returns {{attributes: Record<string, ?>, nestedAttributes: Record<string, ?> | null} | string} - Mutation attributes or validation error message.
461
+ */
462
+ function frontendModelMutationAttributes(params) {
463
+ const attributes = params.attributes
464
+
465
+ if (!attributes || typeof attributes !== "object") {
466
+ return "Expected model attributes."
467
+ }
468
+
469
+ return {
470
+ attributes,
471
+ nestedAttributes: params.nestedAttributes && typeof params.nestedAttributes === "object"
472
+ ? /**
473
+ * Types the following value.
474
+ @type {Record<string, ?>} */ (params.nestedAttributes)
475
+ : null
476
+ }
477
+ }
478
+
479
+ /** Controller with built-in frontend model resource actions. */
480
+ export default class FrontendModelController extends Controller {
481
+ /**
482
+ * Frontend model params.
483
+ @type {Record<string, ?> | undefined} */
484
+ _frontendModelParams = undefined
485
+ /**
486
+ * Frontend model params override.
487
+ @type {Record<string, ?> | undefined} */
488
+ _frontendModelParamsOverride = undefined
489
+ /**
490
+ * Frontend model ability override.
491
+ @type {import("./authorization/ability.js").default | undefined} */
492
+ _frontendModelAbilityOverride = undefined
493
+
494
+ /**
495
+ * Runs frontend model params.
496
+ * @returns {Record<string, ?>} - Decoded request params.
497
+ */
498
+ frontendModelParams() {
499
+ if (this._frontendModelParamsOverride) {
500
+ return this._frontendModelParamsOverride
501
+ }
502
+
503
+ this._frontendModelParams ||= /**
504
+ * Types the following value.
505
+ @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(this.params()))
506
+
507
+ return this._frontendModelParams
508
+ }
509
+
510
+ /**
511
+ * Runs with frontend model params.
512
+ * @template T
513
+ * @param {Record<string, ?>} params - Temporary frontend model params.
514
+ * @param {() => Promise<T>} callback - Callback executed with temporary params.
515
+ * @returns {Promise<T>} - Callback return value.
516
+ */
517
+ async withFrontendModelParams(params, callback) {
518
+ const previousOverride = this._frontendModelParamsOverride
519
+ const previousParams = this._frontendModelParams
520
+
521
+ this._frontendModelParamsOverride = params
522
+ this._frontendModelParams = undefined
523
+
524
+ try {
525
+ return await callback()
526
+ } finally {
527
+ this._frontendModelParamsOverride = previousOverride
528
+ this._frontendModelParams = previousParams
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Runs with frontend model request context.
534
+ * @template T
535
+ * @param {Record<string, ?>} params - Request-scoped params.
536
+ * @param {import("./http-server/client/response.js").default} response - Response instance.
537
+ * @param {() => Promise<T>} callback - Callback executed inside resolved tenant and ability context.
538
+ * @returns {Promise<T>} - Callback return value.
539
+ */
540
+ async withFrontendModelRequestContext(params, response, callback) {
541
+ const configuration = this.getConfiguration()
542
+ const tenant = await configuration.resolveTenant({
543
+ params,
544
+ request: this.request(),
545
+ response
546
+ })
547
+
548
+ return await configuration.runWithTenant(tenant, async () => {
549
+ return await configuration.ensureConnections({name: "Frontend model request"}, async () => {
550
+ const ability = await configuration.resolveAbility({
551
+ params,
552
+ request: this.request(),
553
+ response
554
+ })
555
+ /**
556
+ * Previous ability override.
557
+ @type {import("./authorization/ability.js").default | undefined} */
558
+ const previousAbilityOverride = this._frontendModelAbilityOverride
559
+
560
+ this._frontendModelAbilityOverride = ability
561
+
562
+ try {
563
+ return await configuration.runWithAbility(ability, async () => {
564
+ return await callback()
565
+ })
566
+ } finally {
567
+ this._frontendModelAbilityOverride = previousAbilityOverride
568
+ }
569
+ })
570
+ })
571
+ }
572
+
573
+ /**
574
+ * Runs current ability.
575
+ * @returns {import("./authorization/ability.js").default | undefined} - Current ability for frontend-model request scope.
576
+ */
577
+ currentAbility() {
578
+ return this._frontendModelAbilityOverride || super.currentAbility()
579
+ }
580
+
581
+ /**
582
+ * Runs frontend model class.
583
+ * @returns {typeof import("./database/record/index.js").default} - Frontend model class for controller resource actions.
584
+ */
585
+ frontendModelClass() {
586
+ const frontendModelClass = this.frontendModelClassFromConfiguration()
587
+ const params = this.frontendModelParams()
588
+ const modelName = typeof params.model === "string" ? params.model : undefined
589
+ const controllerName = typeof params.controller === "string" ? params.controller : undefined
590
+
591
+ if (frontendModelClass) return frontendModelClass
592
+
593
+ throw new Error(`No frontend model configured for model '${modelName || "unknown"}' and controller '${controllerName || "unknown"}'. Ensure a FrontendModelBaseResource subclass exists in src/resources/ or is listed in the ability resolver.`)
594
+ }
595
+
596
+ /**
597
+ * Runs frontend model resource configuration.
598
+ * @returns {{backendProject: import("./configuration-types.js").BackendProjectConfiguration, modelName: string, resourceClass: import("./configuration-types.js").FrontendModelResourceClassType, resourceConfiguration: import("./configuration-types.js").NormalizedFrontendModelResourceConfiguration} | null} - Frontend model resource configuration for current controller.
599
+ */
600
+ frontendModelResourceConfiguration() {
601
+ const params = this.frontendModelParams()
602
+ const modelName = typeof params.model === "string" ? params.model : undefined
603
+ const controllerName = typeof params.controller === "string" ? params.controller : undefined
604
+ const backendProjects = this.getConfiguration().getBackendProjects()
605
+
606
+ for (const backendProject of backendProjects) {
607
+ const resources = frontendModelResourcesForBackendProject(backendProject)
608
+
609
+ if (modelName && modelName.length > 0 && resources[modelName]) {
610
+ const resourceDefinition = resources[modelName]
611
+ const resourceConfiguration = frontendModelResourceConfigurationFromDefinition(resourceDefinition)
612
+ const resourceClass = frontendModelResourceClassFromDefinition(resourceDefinition)
613
+
614
+ if (!resourceConfiguration || !resourceClass) {
615
+ throw new Error(`Frontend model resource '${modelName}' must be a FrontendModelBaseResource subclass`)
616
+ }
617
+
618
+ return {
619
+ backendProject,
620
+ modelName,
621
+ resourceClass,
622
+ resourceConfiguration
623
+ }
624
+ }
625
+
626
+ if (!controllerName || controllerName.length < 1) continue
627
+
628
+ for (const resourceModelName in resources) {
629
+ const resourceDefinition = resources[resourceModelName]
630
+ const resourceConfiguration = frontendModelResourceConfigurationFromDefinition(resourceDefinition)
631
+ const resourceClass = frontendModelResourceClassFromDefinition(resourceDefinition)
632
+
633
+ if (!resourceConfiguration || !resourceClass) {
634
+ throw new Error(`Frontend model resource '${resourceModelName}' must be a FrontendModelBaseResource subclass`)
635
+ }
636
+
637
+ const resourcePath = this.frontendModelResourcePath(resourceModelName, resourceDefinition)
638
+
639
+ if (this.frontendModelResourceMatchesController({controllerName, resourcePath})) {
640
+ return {
641
+ backendProject,
642
+ modelName: resourceModelName,
643
+ resourceClass,
644
+ resourceConfiguration
645
+ }
646
+ }
647
+ }
648
+ }
649
+
650
+ return null
651
+ }
652
+
653
+ /**
654
+ * Runs frontend model resource configuration for backend project model name.
655
+ * @param {object} args - Arguments.
656
+ * @param {import("./configuration-types.js").BackendProjectConfiguration} args.backendProject - Backend project configuration.
657
+ * @param {string} args.modelName - Model name.
658
+ * @returns {{backendProject: import("./configuration-types.js").BackendProjectConfiguration, modelName: string, resourceClass: import("./configuration-types.js").FrontendModelResourceClassType, resourceConfiguration: import("./configuration-types.js").NormalizedFrontendModelResourceConfiguration} | null} - Frontend model resource configuration for model name.
659
+ */
660
+ frontendModelResourceConfigurationForBackendProjectModelName({backendProject, modelName}) {
661
+ const resources = frontendModelResourcesForBackendProject(backendProject)
662
+ const resourceDefinition = resources[modelName]
663
+
664
+ if (!resourceDefinition) return null
665
+
666
+ const resourceConfiguration = frontendModelResourceConfigurationFromDefinition(resourceDefinition)
667
+ const resourceClass = frontendModelResourceClassFromDefinition(resourceDefinition)
668
+
669
+ if (!resourceConfiguration || !resourceClass) return null
670
+
671
+ return {
672
+ backendProject,
673
+ modelName,
674
+ resourceClass,
675
+ resourceConfiguration
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Runs frontend model resource configuration for model class.
681
+ * @param {typeof import("./database/record/index.js").default} modelClass - Model class.
682
+ * @returns {{backendProject: import("./configuration-types.js").BackendProjectConfiguration, modelName: string, resourceClass: import("./configuration-types.js").FrontendModelResourceClassType, resourceConfiguration: import("./configuration-types.js").NormalizedFrontendModelResourceConfiguration} | null} - Frontend model resource configuration for model class.
683
+ */
684
+ frontendModelResourceConfigurationForModelClass(modelClass) {
685
+ const frontendModelResource = this.frontendModelResourceConfiguration()
686
+
687
+ if (!frontendModelResource) return null
688
+
689
+ return this.frontendModelResourceConfigurationForBackendProjectModelName({
690
+ backendProject: frontendModelResource.backendProject,
691
+ modelName: modelClass.getModelName()
692
+ })
693
+ }
694
+
695
+ /**
696
+ * Runs frontend model resource model class.
697
+ * @param {{modelName: string, resourceClass: import("./configuration-types.js").FrontendModelResourceClassType}} frontendModelResource - Frontend model resource configuration.
698
+ * @returns {typeof import("./database/record/index.js").default | null} - Backing record class, when available.
699
+ */
700
+ frontendModelResourceModelClass(frontendModelResource) {
701
+ const resourceModelClass = frontendModelResource.resourceClass.ModelClass
702
+
703
+ if (resourceModelClass) return resourceModelClass
704
+
705
+ return this.getConfiguration().getModelClasses()[frontendModelResource.modelName] || null
706
+ }
707
+
708
+ /**
709
+ * Runs frontend model class from configuration.
710
+ * @returns {typeof import("./database/record/index.js").default | null} - Frontend model class resolved from backend project configuration.
711
+ */
712
+ frontendModelClassFromConfiguration() {
713
+ const frontendModelResource = this.frontendModelResourceConfiguration()
714
+
715
+ if (!frontendModelResource) return null
716
+
717
+ const resourceModelClass = this.frontendModelResourceModelClass(frontendModelResource)
718
+
719
+ if (resourceModelClass) return resourceModelClass
720
+
721
+ const modelClasses = this.getConfiguration().getModelClasses()
722
+ const modelClass = modelClasses[frontendModelResource.modelName]
723
+
724
+ if (!modelClass) {
725
+ 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(", ")}`)
726
+ }
727
+
728
+ return modelClass
729
+ }
730
+
731
+ /**
732
+ * Ensures the frontend model class and requested preload target classes are initialized.
733
+ * This handles the case where model initialization was skipped at startup (e.g., browser tests).
734
+ * @returns {Promise<void>} - Resolves when the model class is ready.
735
+ */
736
+ async ensureFrontendModelClassInitialized() {
737
+ const frontendModelResource = this.frontendModelResourceConfiguration()
738
+ const modelClass = this.frontendModelClassFromConfiguration()
739
+
740
+ if (!modelClass) return
741
+
742
+ await this.ensureFrontendModelRecordClassInitialized(modelClass)
743
+
744
+ if (!frontendModelResource) return
745
+
746
+ await this.ensureFrontendModelPreloadClassesInitialized({
747
+ backendProject: frontendModelResource.backendProject,
748
+ modelClass,
749
+ preload: this.frontendModelPreload()
750
+ })
751
+ }
752
+
753
+ /**
754
+ * Runs ensure frontend model record class initialized.
755
+ * @param {typeof import("./database/record/index.js").default} modelClass - Model class to initialize.
756
+ * @returns {Promise<void>} - Resolves when the model class is ready.
757
+ */
758
+ async ensureFrontendModelRecordClassInitialized(modelClass) {
759
+ if (!modelClass || modelClass.isInitialized()) return
760
+
761
+ const configuration = this.getConfiguration()
762
+
763
+ if (typeof modelClass.ensureInitialized === "function") {
764
+ await modelClass.ensureInitialized({configuration})
765
+ } else if (typeof modelClass.initializeRecord === "function") {
766
+ await modelClass.initializeRecord({configuration})
767
+ }
768
+ }
769
+
770
+ /**
771
+ * Runs ensure frontend model preload classes initialized.
772
+ * @param {object} args - Arguments.
773
+ * @param {import("./configuration-types.js").BackendProjectConfiguration} args.backendProject - Backend project configuration.
774
+ * @param {typeof import("./database/record/index.js").default} args.modelClass - Model class whose preload tree is being resolved.
775
+ * @param {import("./database/query/index.js").NestedPreloadRecord | null} args.preload - Normalized preload tree.
776
+ * @returns {Promise<void>} - Resolves when preload target classes are initialized.
777
+ */
778
+ async ensureFrontendModelPreloadClassesInitialized({backendProject, modelClass, preload}) {
779
+ if (!preload) return
780
+
781
+ for (const [relationshipName, relationshipPreload] of Object.entries(preload)) {
782
+ if (relationshipPreload === false) continue
783
+
784
+ const relationship = modelClass.getRelationshipByName(relationshipName)
785
+ const targetModelClass = await this.ensureFrontendModelRelationshipTargetClassInitialized({
786
+ backendProject,
787
+ relationship
788
+ })
789
+
790
+ if (!targetModelClass || !isPlainObject(relationshipPreload)) continue
791
+
792
+ await this.ensureFrontendModelPreloadClassesInitialized({
793
+ backendProject,
794
+ modelClass: targetModelClass,
795
+ preload: /**
796
+ * Types the following value.
797
+ @type {import("./database/query/index.js").NestedPreloadRecord} */ (relationshipPreload)
798
+ })
799
+ }
800
+ }
801
+
802
+ /**
803
+ * Runs ensure frontend model relationship target class initialized.
804
+ * @param {object} args - Arguments.
805
+ * @param {import("./configuration-types.js").BackendProjectConfiguration} args.backendProject - Backend project configuration.
806
+ * @param {import("./database/record/relationships/base.js").default} args.relationship - Relationship definition.
807
+ * @returns {Promise<typeof import("./database/record/index.js").default | null>} - Target model class, when available.
808
+ */
809
+ async ensureFrontendModelRelationshipTargetClassInitialized({backendProject, relationship}) {
810
+ if (relationship.through) {
811
+ const throughRelationship = relationship.getModelClass().getRelationshipByName(relationship.through)
812
+ await this.ensureFrontendModelRelationshipTargetClassInitialized({
813
+ backendProject,
814
+ relationship: throughRelationship
815
+ })
816
+ }
817
+
818
+ const targetModelClass = this.frontendModelRelationshipTargetModelClass({
819
+ backendProject,
820
+ relationship
821
+ })
822
+
823
+ if (!targetModelClass) return null
824
+
825
+ await this.ensureFrontendModelRecordClassInitialized(targetModelClass)
826
+
827
+ return targetModelClass
828
+ }
829
+
830
+ /**
831
+ * Runs frontend model relationship target model class.
832
+ * @param {object} args - Arguments.
833
+ * @param {import("./configuration-types.js").BackendProjectConfiguration} args.backendProject - Backend project configuration.
834
+ * @param {import("./database/record/relationships/base.js").default} args.relationship - Relationship definition.
835
+ * @returns {typeof import("./database/record/index.js").default | null} - Target model class, when available.
836
+ */
837
+ frontendModelRelationshipTargetModelClass({backendProject, relationship}) {
838
+ if (relationship.getPolymorphic() && relationship.getType() === "belongsTo") return null
839
+
840
+ if (relationship.klass) return relationship.klass
841
+
842
+ if (relationship.className) {
843
+ const frontendModelResource = this.frontendModelResourceConfigurationForBackendProjectModelName({
844
+ backendProject,
845
+ modelName: relationship.className
846
+ })
847
+ const resourceModelClass = frontendModelResource ? this.frontendModelResourceModelClass(frontendModelResource) : null
848
+
849
+ if (resourceModelClass) return resourceModelClass
850
+
851
+ const registeredModelClass = this.getConfiguration().getModelClasses()[relationship.className]
852
+
853
+ if (registeredModelClass) return registeredModelClass
854
+ }
855
+
856
+ const targetModelClass = relationship.getTargetModelClass()
857
+
858
+ return targetModelClass || null
859
+ }
860
+
861
+ /**
862
+ * Runs frontend model resource path.
863
+ * @param {string} modelName - Model class name.
864
+ * @param {?} resourceDefinition - Resource definition.
865
+ * @returns {string} - Normalized resource path.
866
+ */
867
+ frontendModelResourcePath(modelName, resourceDefinition) {
868
+ return frontendModelResourcePath(modelName, resourceDefinition)
869
+ }
870
+
871
+ /**
872
+ * Runs frontend model resource matches controller.
873
+ * @param {object} args - Arguments.
874
+ * @param {string} args.controllerName - Controller name from params.
875
+ * @param {string} args.resourcePath - Resource path from configuration.
876
+ * @returns {boolean} - Whether resource path matches current controller.
877
+ */
878
+ frontendModelResourceMatchesController({controllerName, resourcePath}) {
879
+ const normalizedController = controllerName.replace(/^\/+|\/+$/g, "")
880
+ const normalizedResourcePath = resourcePath.replace(/^\/+|\/+$/g, "")
881
+
882
+ if (normalizedResourcePath === normalizedController) return true
883
+
884
+ return normalizedResourcePath.endsWith(`/${normalizedController}`)
885
+ }
886
+
887
+ /**
888
+ * Runs frontend model resource instance.
889
+ * @returns {FrontendModelBaseResource} - Backend resource instance for current frontend-model action.
890
+ */
891
+ frontendModelResourceInstance() {
892
+ const frontendModelResource = this.frontendModelResourceConfiguration()
893
+
894
+ if (!frontendModelResource) {
895
+ throw new Error(`No frontend model resource configuration for controller '${this.frontendModelParams().controller}'`)
896
+ }
897
+
898
+ const resourceArgs = {
899
+ ability: this.currentAbility(),
900
+ controller: this,
901
+ context: {
902
+ ...(this.currentAbility()?.getContext() || {}),
903
+ params: this.frontendModelParams(),
904
+ request: this.request()
905
+ },
906
+ locals: this.currentAbility()?.getLocals() || {},
907
+ modelClass: this.frontendModelClass(),
908
+ modelName: frontendModelResource.modelName,
909
+ params: this.frontendModelParams(),
910
+ resourceConfiguration: frontendModelResource.resourceConfiguration
911
+ }
912
+
913
+ return new frontendModelResource.resourceClass(resourceArgs)
914
+ }
915
+
916
+ /**
917
+ * Runs frontend model primary key.
918
+ * @returns {string} - Frontend model primary key.
919
+ */
920
+ frontendModelPrimaryKey() {
921
+ return this.frontendModelClass().primaryKey()
922
+ }
923
+
924
+ /**
925
+ * Runs frontend model ability action.
926
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
927
+ * @returns {string} - Ability action configured for the frontend action.
928
+ */
929
+ frontendModelAbilityAction(action) {
930
+ const frontendModelResource = this.frontendModelResourceConfiguration()
931
+
932
+ if (!frontendModelResource) {
933
+ throw new Error(`No frontend model resource configuration for controller '${this.frontendModelParams().controller}'`)
934
+ }
935
+
936
+ const abilities = frontendModelResource.resourceConfiguration.abilities
937
+
938
+ if (!abilities || typeof abilities !== "object") {
939
+ throw new Error(`Resource '${frontendModelResource.modelName}' must define an 'abilities' object`)
940
+ }
941
+
942
+ const abilityKey = action === "attach"
943
+ ? "update"
944
+ : ((action === "download" || action === "url") ? "find" : action)
945
+ const abilityAction = abilities[abilityKey]
946
+
947
+ if (typeof abilityAction !== "string" || abilityAction.length < 1) {
948
+ throw new Error(`Resource '${frontendModelResource.modelName}' must define abilities.${abilityKey}`)
949
+ }
950
+
951
+ return abilityAction
952
+ }
953
+
954
+ /**
955
+ * Runs frontend model authorized query.
956
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
957
+ * @returns {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} - Authorized query for the action.
958
+ */
959
+ frontendModelAuthorizedQuery(action) {
960
+ const abilityAction = this.frontendModelAbilityAction(action)
961
+
962
+ return this.frontendModelClass().accessibleFor(abilityAction, this.currentAbility())
963
+ }
964
+
965
+ /**
966
+ * Runs frontend model primary key value.
967
+ * @param {import("./database/record/index.js").default} model - Model instance.
968
+ * @returns {string} - Primary key value as string.
969
+ */
970
+ frontendModelPrimaryKeyValue(model) {
971
+ const columnName = this.frontendModelPrimaryKey()
972
+ const attributeNameMap = model.getModelClass().getColumnNameToAttributeNameMap()
973
+ const attributeName = attributeNameMap[columnName] || columnName
974
+ const value = model.readAttribute(attributeName)
975
+
976
+ return String(value)
977
+ }
978
+
979
+ /**
980
+ * Runs frontend model filter authorized models.
981
+ * @param {object} args - Arguments.
982
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} args.action - Frontend action.
983
+ * @param {import("./database/record/index.js").default[]} args.models - Candidate models.
984
+ * @returns {Promise<import("./database/record/index.js").default[]>} - Authorized models.
985
+ */
986
+ async frontendModelFilterAuthorizedModels({action, models}) {
987
+ if (models.length === 0) return models
988
+
989
+ const primaryKey = this.frontendModelPrimaryKey()
990
+ const ids = models.map((model) => this.frontendModelPrimaryKeyValue(model))
991
+ const authorizedQuery = this.frontendModelAuthorizedQuery(action).where({[primaryKey]: ids})
992
+
993
+ const authorizedIdsRaw = await authorizedQuery.pluck(primaryKey)
994
+
995
+ const authorizedIds = new Set(authorizedIdsRaw.map((id) => String(id)))
996
+
997
+ return models.filter((model) => authorizedIds.has(this.frontendModelPrimaryKeyValue(model)))
998
+ }
999
+
1000
+ /**
1001
+ * Runs run frontend model before action.
1002
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
1003
+ * @returns {Promise<boolean>} - Whether action should continue.
1004
+ */
1005
+ async runFrontendModelBeforeAction(action) {
1006
+ const result = await this.frontendModelResourceInstance().beforeAction(action)
1007
+
1008
+ return result !== false
1009
+ }
1010
+
1011
+ /**
1012
+ * Runs frontend model find record.
1013
+ * @param {"find" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
1014
+ * @param {string | number} id - Record id.
1015
+ * @returns {Promise<import("./database/record/index.js").default | null>} - Located model record.
1016
+ */
1017
+ async frontendModelFindRecord(action, id) {
1018
+ const model = await this.frontendModelResourceInstance().find(action, id)
1019
+
1020
+ if (!model) return null
1021
+
1022
+ const authorizedModels = await this.frontendModelFilterAuthorizedModels({action, models: [model]})
1023
+
1024
+ return authorizedModels[0] || null
1025
+ }
1026
+
1027
+ /**
1028
+ * Runs frontend model create record.
1029
+ * @param {Record<string, ?>} attributes - Create attributes.
1030
+ * @param {Record<string, ?> | null} [nestedAttributes] - Optional nested-attribute payload for cascading writes.
1031
+ * @returns {Promise<import("./database/record/index.js").default | null>} - Created model when authorized.
1032
+ */
1033
+ async frontendModelCreateRecord(attributes, nestedAttributes = null) {
1034
+ const resource = this.frontendModelResourceInstance()
1035
+ const model = await resource.create(attributes, {nestedAttributes, controller: this})
1036
+
1037
+ const authorizedModels = await this.frontendModelFilterAuthorizedModels({action: "create", models: [model]})
1038
+
1039
+ if (authorizedModels.length > 0) {
1040
+ return authorizedModels[0]
1041
+ }
1042
+
1043
+ await resource.handleUnauthorizedCreatedModel(model)
1044
+
1045
+ return null
1046
+ }
1047
+
1048
+ /**
1049
+ * Runs frontend model records.
1050
+ * @returns {Promise<import("./database/record/index.js").default[]>} - Frontend model records.
1051
+ */
1052
+ async frontendModelRecords() {
1053
+ const models = await this.frontendModelResourceInstance().records()
1054
+
1055
+ return await this.frontendModelFilterAuthorizedModels({action: "index", models})
1056
+ }
1057
+
1058
+ /**
1059
+ * Runs frontend model preload.
1060
+ * @returns {import("./database/query/index.js").NestedPreloadRecord | null} - Frontend preload data.
1061
+ */
1062
+ frontendModelPreload() {
1063
+ return normalizeFrontendModelPreload(this.frontendModelParams().preload)
1064
+ }
1065
+
1066
+ /**
1067
+ * Runs frontend model select.
1068
+ * @returns {Record<string, string[]> | null} - Frontend select data.
1069
+ */
1070
+ frontendModelSelect() {
1071
+ return normalizeFrontendModelSelect(this.frontendModelParams().select, this.frontendModelClass().getModelName())
1072
+ }
1073
+
1074
+ /**
1075
+ * Runs frontend model selects extra.
1076
+ * @returns {Record<string, string[]> | null} - Frontend extra-select data (defaults plus these), keyed by model name.
1077
+ */
1078
+ frontendModelSelectsExtra() {
1079
+ return normalizeFrontendModelSelect(this.frontendModelParams().selectsExtra, this.frontendModelClass().getModelName())
1080
+ }
1081
+
1082
+ /**
1083
+ * Runs frontend model searches.
1084
+ * @returns {FrontendModelSearch[]} - Frontend search filters.
1085
+ */
1086
+ frontendModelSearches() {
1087
+ return normalizeFrontendModelSearches(this.frontendModelParams().searches)
1088
+ }
1089
+
1090
+ /**
1091
+ * Runs frontend model where.
1092
+ * @returns {Record<string, ?> | null} - Frontend where filters.
1093
+ */
1094
+ frontendModelWhere() {
1095
+ return normalizeFrontendModelWhere(this.frontendModelParams().where)
1096
+ }
1097
+
1098
+ /**
1099
+ * Runs frontend model ransack.
1100
+ * @returns {Record<string, ?> | null} - Frontend Ransack filters.
1101
+ */
1102
+ frontendModelRansack() {
1103
+ return normalizeFrontendModelRansack(this.frontendModelParams().ransack)
1104
+ }
1105
+
1106
+ /**
1107
+ * Runs frontend model joins.
1108
+ * @returns {Record<string, ?> | null} - Frontend joins descriptors.
1109
+ */
1110
+ frontendModelJoins() {
1111
+ return normalizeFrontendModelJoins(this.frontendModelParams().joins)
1112
+ }
1113
+
1114
+ /**
1115
+ * Runs frontend model sort.
1116
+ * @returns {FrontendModelSort[]} - Frontend sort definitions.
1117
+ */
1118
+ frontendModelSort() {
1119
+ return normalizeQuerySort(this.frontendModelParams().sort)
1120
+ }
1121
+
1122
+ /**
1123
+ * Runs frontend model group.
1124
+ * @returns {FrontendModelGroup[]} - Frontend group definitions.
1125
+ */
1126
+ frontendModelGroup() {
1127
+ try {
1128
+ return normalizeQueryGroup(this.frontendModelParams().group)
1129
+ } catch (error) {
1130
+ throw frontendModelValidationErrorForError(error)
1131
+ }
1132
+ }
1133
+
1134
+ /**
1135
+ * Runs frontend model pagination.
1136
+ * @returns {FrontendModelPagination} - Frontend pagination params.
1137
+ */
1138
+ frontendModelPagination() {
1139
+ const params = this.frontendModelParams()
1140
+
1141
+ return normalizeFrontendModelPagination({
1142
+ limit: params.limit,
1143
+ offset: params.offset,
1144
+ page: params.page,
1145
+ perPage: params.perPage
1146
+ })
1147
+ }
1148
+
1149
+ /**
1150
+ * Runs frontend model distinct.
1151
+ * @returns {boolean | null} - Frontend distinct flag when provided.
1152
+ */
1153
+ frontendModelDistinct() {
1154
+ return normalizeFrontendModelDistinct(this.frontendModelParams().distinct)
1155
+ }
1156
+
1157
+ /**
1158
+ * Runs frontend model pluck.
1159
+ * @returns {FrontendModelPluck[]} - Frontend pluck definitions.
1160
+ */
1161
+ frontendModelPluck() {
1162
+ try {
1163
+ return normalizeQueryPluck(this.frontendModelParams().pluck)
1164
+ } catch (error) {
1165
+ throw frontendModelValidationErrorForError(error)
1166
+ }
1167
+ }
1168
+
1169
+ /**
1170
+ * Runs frontend model count requested.
1171
+ * @returns {boolean} - Whether the request asks for an aggregate count.
1172
+ */
1173
+ frontendModelCountRequested() {
1174
+ return this.frontendModelParams().count === true
1175
+ }
1176
+
1177
+ /**
1178
+ * Runs frontend model with count.
1179
+ * @returns {Array<{attributeName: string, relationshipName: string, where?: Record<string, ?>}>}
1180
+ * Frontend withCount entries. Empty array when not requested.
1181
+ */
1182
+ frontendModelWithCount() {
1183
+ const raw = this.frontendModelParams().withCount
1184
+
1185
+ if (!Array.isArray(raw)) return []
1186
+
1187
+ /**
1188
+ * Entries.
1189
+ @type {Array<{attributeName: string, relationshipName: string, where?: Record<string, ?>}>} */
1190
+ const entries = []
1191
+
1192
+ for (const entry of raw) {
1193
+ if (!entry || typeof entry !== "object") continue
1194
+ if (typeof entry.attributeName !== "string" || entry.attributeName.length === 0) continue
1195
+ if (typeof entry.relationshipName !== "string" || entry.relationshipName.length === 0) continue
1196
+
1197
+ entries.push({
1198
+ attributeName: entry.attributeName,
1199
+ relationshipName: entry.relationshipName,
1200
+ where: entry.where && typeof entry.where === "object" ? entry.where : undefined
1201
+ })
1202
+ }
1203
+
1204
+ return entries
1205
+ }
1206
+
1207
+ /**
1208
+ * Resolve an entry from the frontend-model `abilities` payload to
1209
+ * its backend model class by looking up the resource by modelName
1210
+ * across all configured backend projects. Returns null when no
1211
+ * resource matches — the spec entry is then silently ignored so a
1212
+ * caller requesting abilities for a model they cannot resolve does
1213
+ * not crash the request.
1214
+ * @param {string} modelName
1215
+ * @returns {typeof import("./database/record/index.js").default | null}
1216
+ */
1217
+ _frontendModelClassForAbilities(modelName) {
1218
+ if (typeof modelName !== "string" || modelName.length === 0) return null
1219
+
1220
+ const configuration = this.getConfiguration()
1221
+ const backendProjects = configuration?.getBackendProjects?.() ?? []
1222
+
1223
+ for (const backendProject of backendProjects) {
1224
+ const frontendModels = backendProject?.frontendModels
1225
+ if (!frontendModels || typeof frontendModels !== "object") continue
1226
+
1227
+ const resourceDefinition = frontendModels[modelName]
1228
+ if (!resourceDefinition) continue
1229
+
1230
+ const resourceClass = frontendModelResourceClassFromDefinition(resourceDefinition)
1231
+ if (!resourceClass) continue
1232
+
1233
+ const modelClass = typeof resourceClass.modelClass === "function"
1234
+ ? resourceClass.modelClass()
1235
+ : resourceClass.ModelClass
1236
+
1237
+ if (typeof modelClass === "function") return modelClass
1238
+ }
1239
+
1240
+ return null
1241
+ }
1242
+
1243
+ /**
1244
+ * Collect every loaded record whose `getModelName()` matches the
1245
+ * requested name, walking across the root-level slice plus any
1246
+ * preloaded relationships at any depth. Used to evaluate per-record
1247
+ * abilities against nested preloaded children with a single batched
1248
+ * query per (modelClass, action) pair.
1249
+ * @param {import("./database/record/index.js").default[]} rootModels
1250
+ * @param {string} modelName
1251
+ * @returns {import("./database/record/index.js").default[]}
1252
+ */
1253
+ _frontendModelCollectRecordsForName(rootModels, modelName) {
1254
+ /**
1255
+ * Out.
1256
+ @type {import("./database/record/index.js").default[]} */
1257
+ const out = []
1258
+ /**
1259
+ * Seen.
1260
+ @type {Set<import("./database/record/index.js").default>} */
1261
+ const seen = new Set()
1262
+
1263
+ /**
1264
+ * Walk.
1265
+ @param {import("./database/record/index.js").default | null | undefined} record */
1266
+ const walk = (record) => {
1267
+ if (!record || typeof record !== "object") return
1268
+ if (seen.has(record)) return
1269
+ seen.add(record)
1270
+
1271
+ const ModelClass = typeof record.getModelClass === "function"
1272
+ ? record.getModelClass()
1273
+ : null
1274
+ if (ModelClass && typeof ModelClass.getModelName === "function" && ModelClass.getModelName() === modelName) {
1275
+ out.push(record)
1276
+ }
1277
+
1278
+ const relationshipsMap = typeof ModelClass?.getRelationshipsMap === "function"
1279
+ ? ModelClass.getRelationshipsMap()
1280
+ : null
1281
+ if (!relationshipsMap) return
1282
+
1283
+ for (const relationshipName of Object.keys(relationshipsMap)) {
1284
+ const relationship = typeof record.getRelationshipByName === "function"
1285
+ ? record.getRelationshipByName(relationshipName)
1286
+ : null
1287
+ if (!relationship || typeof relationship.getLoadedOrUndefined !== "function") continue
1288
+
1289
+ const loaded = relationship.getLoadedOrUndefined()
1290
+ if (loaded === undefined) continue
1291
+
1292
+ if (Array.isArray(loaded)) {
1293
+ for (const child of loaded) walk(child)
1294
+ } else {
1295
+ walk(loaded)
1296
+ }
1297
+ }
1298
+ }
1299
+
1300
+ for (const root of rootModels) walk(root)
1301
+
1302
+ return out
1303
+ }
1304
+
1305
+ /**
1306
+ * Evaluate every ability requested via the frontend `abilities`
1307
+ * param against the loaded model cohort (plus any preloaded
1308
+ * children), attaching the results to each record via
1309
+ * `_setComputedAbility`. Runs one batched `authorized query + pluck`
1310
+ * per (modelClass, action) pair, regardless of how many records
1311
+ * were loaded.
1312
+ * @param {import("./database/record/index.js").default[]} rootModels
1313
+ * @returns {Promise<void>}
1314
+ */
1315
+ async frontendModelComputeAbilities(rootModels) {
1316
+ const entries = this.frontendModelAbilities()
1317
+ if (entries.length === 0) return
1318
+ if (!Array.isArray(rootModels) || rootModels.length === 0) return
1319
+
1320
+ const ability = this.currentAbility()
1321
+ if (!ability) return
1322
+
1323
+ for (const entry of entries) {
1324
+ const modelClass = this._frontendModelClassForAbilities(entry.modelName)
1325
+ if (!modelClass) continue
1326
+
1327
+ const candidates = this._frontendModelCollectRecordsForName(rootModels, entry.modelName)
1328
+ if (candidates.length === 0) continue
1329
+
1330
+ const primaryKey = modelClass.primaryKey()
1331
+ const ids = candidates
1332
+ .map((record) => record.readAttribute(primaryKey))
1333
+ .filter((value) => value !== null && value !== undefined)
1334
+ if (ids.length === 0) continue
1335
+
1336
+ for (const action of entry.actions) {
1337
+ let allowedIds
1338
+ try {
1339
+ const authorizedQuery = modelClass.accessibleFor(action, ability).where({[primaryKey]: ids})
1340
+ const plucked = await authorizedQuery.pluck(primaryKey)
1341
+ allowedIds = new Set(plucked.map((value) => String(value)))
1342
+ } catch (error) {
1343
+ // An ability with no allow rules for the action throws via
1344
+ // `accessibleFor`; treat as a universal deny so the frontend
1345
+ // gets `can(action) === false` for every candidate, instead
1346
+ // of surfacing an error that the UI can't act on.
1347
+ void error
1348
+ allowedIds = new Set()
1349
+ }
1350
+
1351
+ for (const record of candidates) {
1352
+ const idValue = record.readAttribute(primaryKey)
1353
+ const allowed = idValue !== null && idValue !== undefined && allowedIds.has(String(idValue))
1354
+ record._setComputedAbility(action, allowed)
1355
+ }
1356
+ }
1357
+ }
1358
+ }
1359
+
1360
+ /**
1361
+ * Parse the frontend-model `abilities` param into a list of
1362
+ * `{modelName, actions}` entries to evaluate against loaded records.
1363
+ * Unknown entries are silently skipped — downstream code resolves
1364
+ * model names to classes when applying the check, so unresolved
1365
+ * names naturally become no-ops.
1366
+ * @returns {Array<{modelName: string, actions: string[]}>}
1367
+ */
1368
+ frontendModelAbilities() {
1369
+ const raw = this.frontendModelParams().abilities
1370
+
1371
+ if (!Array.isArray(raw)) return []
1372
+
1373
+ /**
1374
+ * Entries.
1375
+ @type {Array<{modelName: string, actions: string[]}>} */
1376
+ const entries = []
1377
+
1378
+ for (const entry of raw) {
1379
+ if (!entry || typeof entry !== "object") continue
1380
+ if (typeof entry.modelName !== "string" || entry.modelName.length === 0) continue
1381
+ if (!Array.isArray(entry.actions)) continue
1382
+
1383
+ const actions = entry.actions.filter(
1384
+ (/**
1385
+ * Types the following value.
1386
+ @type {?} */ action) => typeof action === "string" && action.length > 0
1387
+ )
1388
+
1389
+ if (actions.length === 0) continue
1390
+
1391
+ entries.push({actions, modelName: entry.modelName})
1392
+ }
1393
+
1394
+ return entries
1395
+ }
1396
+
1397
+ /**
1398
+ * Read the frontend-model `queryData` param. The wire format carries
1399
+ * only **names** (the keys the frontend wants attached) plus the
1400
+ * optional nested-relationship chain leading to them — the actual SQL
1401
+ * fragments live on the backend model as `Model.queryData(name, fn)`
1402
+ * registrations. Callers cannot push SQL through this endpoint.
1403
+ *
1404
+ * Returns the raw nested-record spec (shape validated by the
1405
+ * normalizer inside `Query.queryData`) or `null` when not requested.
1406
+ * @returns {import("./database/query/query-data.js").QueryDataSpec | null}
1407
+ */
1408
+ frontendModelQueryData() {
1409
+ const raw = this.frontendModelParams().queryData
1410
+
1411
+ if (raw == null) return null
1412
+
1413
+ if (typeof raw === "string") return raw
1414
+ if (Array.isArray(raw)) return raw
1415
+ if (typeof raw === "object") return raw
1416
+
1417
+ return null
1418
+ }
1419
+
1420
+ /**
1421
+ * Runs frontend model index query.
1422
+ * @returns {import("./database/query/model-class-query.js").default} - Frontend index query with normalized params applied.
1423
+ */
1424
+ frontendModelIndexQuery() {
1425
+ let query = this.frontendModelAuthorizedQuery("index")
1426
+ const preload = this.frontendModelPreload()
1427
+
1428
+ if (preload) {
1429
+ query = query.preload(preload)
1430
+ }
1431
+
1432
+ const joins = this.frontendModelJoins()
1433
+ const where = this.frontendModelWhere()
1434
+ const pagination = this.frontendModelPagination()
1435
+ const distinct = this.frontendModelDistinct()
1436
+
1437
+ this.applyFrontendModelPagination({pagination, query})
1438
+
1439
+ if (distinct !== null) {
1440
+ query.distinct(distinct)
1441
+ }
1442
+
1443
+ if (where) {
1444
+ this.applyFrontendModelWhere({query, where})
1445
+ }
1446
+
1447
+ const ransack = this.frontendModelRansack()
1448
+
1449
+ if (ransack) {
1450
+ query.ransack(ransack)
1451
+ }
1452
+
1453
+ if (joins) {
1454
+ this.applyFrontendModelJoins({joins, query})
1455
+ }
1456
+
1457
+ const searches = this.frontendModelSearches()
1458
+
1459
+ for (const search of searches) {
1460
+ this.applyFrontendModelSearch({query, search})
1461
+ }
1462
+
1463
+ const groups = this.frontendModelGroup()
1464
+
1465
+ if (groups.length > 0) {
1466
+ this.applyFrontendModelRootGroupColumns({query})
1467
+ }
1468
+
1469
+ for (const group of groups) {
1470
+ this.applyFrontendModelGroup({group, query})
1471
+ }
1472
+
1473
+ const sorts = this.frontendModelSort()
1474
+
1475
+ if (sorts.length > 0) {
1476
+ for (const sort of sorts) {
1477
+ this.applyFrontendModelSort({query, sort})
1478
+ }
1479
+ }
1480
+
1481
+ const withCount = this.frontendModelWithCount()
1482
+
1483
+ for (const entry of withCount) {
1484
+ /**
1485
+ * Spec.
1486
+ @type {Record<string, boolean | {relationship?: string, where?: Record<string, ?>}>} */
1487
+ const spec = {}
1488
+ spec[entry.attributeName] = {relationship: entry.relationshipName, where: entry.where}
1489
+ query.withCount(spec)
1490
+ }
1491
+
1492
+ const queryData = this.frontendModelQueryData()
1493
+
1494
+ if (queryData != null) {
1495
+ query.queryData(queryData)
1496
+ }
1497
+
1498
+ query = this.applyFrontendModelTranslatedAttributePreloads({query})
1499
+
1500
+ if (query._distinct && query.driver.getType() === "mssql") {
1501
+ return this.frontendModelMssqlDistinctByPrimaryKeyQuery({query})
1502
+ }
1503
+
1504
+ return query
1505
+ }
1506
+
1507
+ /**
1508
+ * MSSQL cannot apply DISTINCT over non-comparable text columns in table.* selects.
1509
+ * This rewrites distinct frontend-model queries to select root records by distinct PK subquery.
1510
+ * @param {object} args - Args.
1511
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query with distinct and filters.
1512
+ * @returns {import("./database/query/model-class-query.js").default} - MSSQL-safe distinct query.
1513
+ */
1514
+ frontendModelMssqlDistinctByPrimaryKeyQuery({query}) {
1515
+ const modelClass = this.frontendModelClass()
1516
+ const primaryKey = modelClass.primaryKey()
1517
+ const rootTableSql = query.driver.quoteTable(modelClass.tableName())
1518
+ const primaryKeySql = `${rootTableSql}.${query.driver.quoteColumn(primaryKey)}`
1519
+ const distinctIdsQuery = query.clone()
1520
+
1521
+ distinctIdsQuery._preload = {}
1522
+ distinctIdsQuery._selects = []
1523
+ distinctIdsQuery.select(primaryKeySql)
1524
+ distinctIdsQuery.distinct(true)
1525
+
1526
+ const distinctRootQuery = modelClass._newQuery()
1527
+
1528
+ distinctRootQuery.where(`${primaryKeySql} IN (${distinctIdsQuery.toSql()})`)
1529
+ distinctRootQuery._preload = {...query._preload}
1530
+
1531
+ return distinctRootQuery
1532
+ }
1533
+
1534
+ /**
1535
+ * Runs frontend model pluck values.
1536
+ * @param {object} args - Pluck args.
1537
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
1538
+ * @param {FrontendModelPluck[]} args.pluck - Pluck descriptors.
1539
+ * @returns {Promise<Array<?>>} - Plucked values.
1540
+ */
1541
+ async frontendModelPluckValues({query, pluck}) {
1542
+ if (pluck.length < 1) {
1543
+ throw new Error("No columns given to pluck")
1544
+ }
1545
+
1546
+ const modelClass = this.frontendModelClass()
1547
+ const pluckQuery = query.clone()
1548
+ /**
1549
+ * Aliases.
1550
+ @type {string[]} */
1551
+ const aliases = []
1552
+ const queryMetadata = frontendModelQueryMetadata(query)
1553
+ const pluckQueryMetadata = frontendModelQueryMetadata(pluckQuery)
1554
+ const joinedPaths = queryMetadata[frontendModelJoinedPathsSymbol]
1555
+
1556
+ pluckQuery._preload = {}
1557
+ pluckQuery._selects = []
1558
+ pluckQueryMetadata[frontendModelJoinedPathsSymbol] = joinedPaths ? new Set(joinedPaths) : new Set()
1559
+
1560
+ for (const [pluckIndex, pluckEntry] of pluck.entries()) {
1561
+ const targetModelClass = this.frontendModelSearchTargetModelClass({
1562
+ modelClass,
1563
+ path: pluckEntry.path
1564
+ })
1565
+ const columnName = this.resolveFrontendModelColumnName(targetModelClass, pluckEntry.column)
1566
+
1567
+ if (!columnName) {
1568
+ throw new Error(`Unknown pluck column "${pluckEntry.column}" for ${targetModelClass.name}`)
1569
+ }
1570
+
1571
+ if (pluckEntry.path.length > 0) {
1572
+ this.ensureFrontendModelJoinPath({path: pluckEntry.path, query: pluckQuery})
1573
+ }
1574
+
1575
+ const tableReference = pluckQuery.getTableReferenceForJoin(...pluckEntry.path)
1576
+ const columnSql = `${pluckQuery.driver.quoteTable(tableReference)}.${pluckQuery.driver.quoteColumn(columnName)}`
1577
+ const alias = `frontend_model_pluck_${pluckIndex}`
1578
+
1579
+ pluckQuery.select(`${columnSql} AS ${pluckQuery.driver.quoteColumn(alias)}`)
1580
+ aliases.push(alias)
1581
+ }
1582
+
1583
+ const rows = await pluckQuery.results()
1584
+
1585
+ if (aliases.length === 1) {
1586
+ const [alias] = aliases
1587
+
1588
+ return rows.map((row) => /**
1589
+ * Types the following value.
1590
+ @type {Record<string, ?>} */ (row)[alias])
1591
+ }
1592
+
1593
+ return rows.map((row) => {
1594
+ const rowHash = /**
1595
+ * Types the following value.
1596
+ @type {Record<string, ?>} */ (row)
1597
+
1598
+ return aliases.map((alias) => rowHash[alias])
1599
+ })
1600
+ }
1601
+
1602
+ /**
1603
+ * Runs frontend model search target model class.
1604
+ * @param {object} args - Search args.
1605
+ * @param {typeof import("./database/record/index.js").default} args.modelClass - Root model class.
1606
+ * @param {string[]} args.path - Relationship path.
1607
+ * @returns {typeof import("./database/record/index.js").default} - Target model class.
1608
+ */
1609
+ frontendModelSearchTargetModelClass({modelClass, path}) {
1610
+ let targetModelClass = modelClass
1611
+
1612
+ for (const relationshipName of path) {
1613
+ const relationship = targetModelClass.getRelationshipsMap()[relationshipName]
1614
+
1615
+ if (!relationship) {
1616
+ throw new Error(`Unknown search relationship "${relationshipName}" for ${targetModelClass.name}`)
1617
+ }
1618
+
1619
+ const relationshipTargetModelClass = relationship.getTargetModelClass()
1620
+
1621
+ if (!relationshipTargetModelClass) {
1622
+ throw new Error(`No target model class for ${targetModelClass.name}#${relationshipName}`)
1623
+ }
1624
+
1625
+ targetModelClass = relationshipTargetModelClass
1626
+ }
1627
+
1628
+ return targetModelClass
1629
+ }
1630
+
1631
+ /**
1632
+ * Runs apply frontend model search.
1633
+ * @param {object} args - Search args.
1634
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
1635
+ * @param {FrontendModelSearch} args.search - Search filter.
1636
+ * @returns {void}
1637
+ */
1638
+ applyFrontendModelSearch({query, search}) {
1639
+ const modelClass = this.frontendModelClass()
1640
+ const targetModelClass = this.frontendModelSearchTargetModelClass({
1641
+ modelClass,
1642
+ path: search.path
1643
+ })
1644
+ const columnName = this.resolveFrontendModelColumnName(targetModelClass, search.column)
1645
+
1646
+ if (!columnName) {
1647
+ throw new Error(`Unknown search column "${search.column}" for ${targetModelClass.name}`)
1648
+ }
1649
+
1650
+ if (search.path.length > 0) {
1651
+ this.ensureFrontendModelJoinPath({path: search.path, query})
1652
+ }
1653
+
1654
+ const tableReference = query.getTableReferenceForJoin(...search.path)
1655
+ const columnSql = `${query.driver.quoteTable(tableReference)}.${query.driver.quoteColumn(columnName)}`
1656
+ const operatorMap = {
1657
+ eq: "=",
1658
+ gt: ">",
1659
+ gteq: ">=",
1660
+ like: "LIKE",
1661
+ lt: "<",
1662
+ lteq: "<=",
1663
+ notEq: "!="
1664
+ }
1665
+ const sqlOperator = operatorMap[search.operator]
1666
+
1667
+ if (search.operator === "eq") {
1668
+ if (this.applyFrontendModelArraySearch({emptySql: "1=0", operatorSql: "IN", query, search, columnSql})) return
1669
+
1670
+ if (search.value === null) {
1671
+ query.where(`${columnSql} IS NULL`)
1672
+ return
1673
+ }
1674
+ }
1675
+
1676
+ if (search.operator === "notEq") {
1677
+ if (this.applyFrontendModelArraySearch({emptySql: "1=1", operatorSql: "NOT IN", query, search, columnSql})) return
1678
+
1679
+ if (search.value === null) {
1680
+ query.where(`${columnSql} IS NOT NULL`)
1681
+ return
1682
+ }
1683
+ }
1684
+
1685
+ query.where(`${columnSql} ${sqlOperator} ${query.driver.quote(search.value)}`)
1686
+ }
1687
+
1688
+ /**
1689
+ * Apply array-valued equality search filters.
1690
+ * @param {object} args - Search arguments.
1691
+ * @param {string} args.columnSql - SQL for the searched column.
1692
+ * @param {string} args.emptySql - SQL predicate used when the array is empty.
1693
+ * @param {"IN" | "NOT IN"} args.operatorSql - SQL array operator.
1694
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
1695
+ * @param {FrontendModelSearch} args.search - Search descriptor.
1696
+ * @returns {boolean} - Whether an array predicate was applied.
1697
+ */
1698
+ applyFrontendModelArraySearch({columnSql, emptySql, operatorSql, query, search}) {
1699
+ if (!Array.isArray(search.value)) return false
1700
+
1701
+ if (search.value.length === 0) {
1702
+ query.where(emptySql)
1703
+ } else {
1704
+ query.where(`${columnSql} ${operatorSql} (${search.value.map((entry) => query.driver.quote(entry)).join(", ")})`)
1705
+ }
1706
+
1707
+ return true
1708
+ }
1709
+
1710
+ /**
1711
+ * Runs apply frontend model pagination.
1712
+ * @param {object} args - Pagination args.
1713
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
1714
+ * @param {FrontendModelPagination} args.pagination - Pagination values.
1715
+ * @returns {void}
1716
+ */
1717
+ applyFrontendModelPagination({query, pagination}) {
1718
+ if (pagination.limit !== null) {
1719
+ query.limit(pagination.limit)
1720
+ }
1721
+
1722
+ if (pagination.offset !== null) {
1723
+ query.offset(pagination.offset)
1724
+ }
1725
+
1726
+ if (pagination.perPage !== null) {
1727
+ query.perPage(pagination.perPage)
1728
+ }
1729
+
1730
+ if (pagination.page !== null) {
1731
+ query.page(pagination.page)
1732
+ }
1733
+ }
1734
+
1735
+ /**
1736
+ * Runs apply frontend model where.
1737
+ * @param {object} args - Where args.
1738
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
1739
+ * @param {Record<string, ?>} args.where - Root-model where conditions.
1740
+ * @returns {void}
1741
+ */
1742
+ applyFrontendModelWhere({query, where}) {
1743
+ this.applyFrontendModelWhereForPath({
1744
+ modelClass: this.frontendModelClass(),
1745
+ path: [],
1746
+ query,
1747
+ where
1748
+ })
1749
+ }
1750
+
1751
+ /**
1752
+ * Runs apply frontend model joins.
1753
+ * @param {object} args - Joins args.
1754
+ * @param {Record<string, ?>} args.joins - Relationship-object joins.
1755
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
1756
+ * @returns {void}
1757
+ */
1758
+ applyFrontendModelJoins({joins, query}) {
1759
+ const joinPathKeys = new Set()
1760
+
1761
+ this.applyFrontendModelJoinsForPath({
1762
+ joins,
1763
+ joinPathKeys,
1764
+ modelClass: this.frontendModelClass(),
1765
+ path: [],
1766
+ query
1767
+ })
1768
+
1769
+ query.joins(joins)
1770
+
1771
+ const queryMetadata = frontendModelQueryMetadata(query)
1772
+ const joinedPaths = queryMetadata[frontendModelJoinedPathsSymbol] || new Set()
1773
+
1774
+ for (const joinPathKey of joinPathKeys) {
1775
+ joinedPaths.add(joinPathKey)
1776
+ }
1777
+
1778
+ queryMetadata[frontendModelJoinedPathsSymbol] = joinedPaths
1779
+ }
1780
+
1781
+ /**
1782
+ * Runs apply frontend model joins for path.
1783
+ * @param {object} args - Joins args.
1784
+ * @param {Record<string, ?>} args.joins - Joins for current path.
1785
+ * @param {Set<string>} args.joinPathKeys - Joined path keys.
1786
+ * @param {typeof import("./database/record/index.js").default} args.modelClass - Model class for current path.
1787
+ * @param {string[]} args.path - Relationship path.
1788
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
1789
+ * @returns {void}
1790
+ */
1791
+ applyFrontendModelJoinsForPath({joins, joinPathKeys, modelClass, path, query}) {
1792
+ void query
1793
+
1794
+ for (const [relationshipName, relationshipJoin] of Object.entries(joins)) {
1795
+ const relationship = modelClass.getRelationshipsMap()[relationshipName]
1796
+
1797
+ if (!relationship) {
1798
+ throw new Error(`Unknown join relationship "${relationshipName}" for ${modelClass.name}`)
1799
+ }
1800
+
1801
+ const targetModelClass = relationship.getTargetModelClass()
1802
+
1803
+ if (!targetModelClass) {
1804
+ throw new Error(`No target model class for join relationship "${relationshipName}" on ${modelClass.name}`)
1805
+ }
1806
+
1807
+ const relationshipPath = [...path, relationshipName]
1808
+ joinPathKeys.add(relationshipPath.join("."))
1809
+
1810
+ if (relationshipJoin === true) continue
1811
+
1812
+ this.applyFrontendModelJoinsForPath({
1813
+ joins: relationshipJoin,
1814
+ joinPathKeys,
1815
+ modelClass: targetModelClass,
1816
+ path: relationshipPath,
1817
+ query
1818
+ })
1819
+ }
1820
+ }
1821
+
1822
+ /**
1823
+ * Resolves a key that may be either a camelCase attribute name or a raw DB
1824
+ * column name to its canonical column name. Returns `undefined` when the
1825
+ * key matches neither map.
1826
+ * @param {typeof import("./database/record/index.js").default} modelClass - Model class.
1827
+ * @param {string} key - Attribute name or column name to resolve.
1828
+ * @returns {string | undefined} - Resolved DB column name, or `undefined`.
1829
+ */
1830
+ resolveFrontendModelColumnName(modelClass, key) {
1831
+ const attributeNameToColumnNameMap = modelClass.getAttributeNameToColumnNameMap()
1832
+ const columnName = attributeNameToColumnNameMap[key]
1833
+
1834
+ if (columnName) return columnName
1835
+
1836
+ // Fall back: check whether the key is already a raw DB column name.
1837
+ const columnNameToAttributeNameMap = modelClass.getColumnNameToAttributeNameMap()
1838
+
1839
+ if (columnNameToAttributeNameMap[key]) return key
1840
+
1841
+ return undefined
1842
+ }
1843
+
1844
+ /**
1845
+ * Runs apply frontend model where for path.
1846
+ * @param {object} args - Where args.
1847
+ * @param {typeof import("./database/record/index.js").default} args.modelClass - Model class for current where scope.
1848
+ * @param {string[]} args.path - Relationship path from root.
1849
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
1850
+ * @param {Record<string, ?>} args.where - Where conditions for current scope.
1851
+ * @returns {void}
1852
+ */
1853
+ applyFrontendModelWhereForPath({modelClass, path, query, where}) {
1854
+ for (const [attributeName, value] of Object.entries(where)) {
1855
+ const columnName = this.resolveFrontendModelColumnName(modelClass, attributeName)
1856
+
1857
+ if (columnName) {
1858
+ this.ensureFrontendModelJoinPath({path, query})
1859
+
1860
+ const tableReference = query.getTableReferenceForJoin(...path)
1861
+ const columnSql = `${query.driver.quoteTable(tableReference)}.${query.driver.quoteColumn(columnName)}`
1862
+
1863
+ if (Array.isArray(value)) {
1864
+ if (value.length === 0) {
1865
+ query.where("1=0")
1866
+ } else {
1867
+ const normalizedValues = value.map((entry) => this.normalizeFrontendModelWhereColumnValue({columnName, modelClass, value: entry}))
1868
+
1869
+ if (normalizedValues.includes(frontendModelWhereNoMatchSymbol)) {
1870
+ query.where("1=0")
1871
+ } else {
1872
+ query.where(`${columnSql} IN (${normalizedValues.map((entry) => query.driver.quote(entry)).join(", ")})`)
1873
+ }
1874
+ }
1875
+
1876
+ continue
1877
+ }
1878
+
1879
+ if (value == null) {
1880
+ query.where(`${columnSql} IS NULL`)
1881
+ } else {
1882
+ const normalizedValue = this.normalizeFrontendModelWhereColumnValue({columnName, modelClass, value})
1883
+
1884
+ if (normalizedValue === frontendModelWhereNoMatchSymbol) {
1885
+ query.where("1=0")
1886
+ } else {
1887
+ query.where(`${columnSql} = ${query.driver.quote(normalizedValue)}`)
1888
+ }
1889
+ }
1890
+
1891
+ continue
1892
+ }
1893
+
1894
+ if (isPlainObject(value)) {
1895
+ const relationship = modelClass.getRelationshipsMap()[attributeName]
1896
+
1897
+ if (!relationship) {
1898
+ throw new Error(`Unknown where relationship "${attributeName}" for ${modelClass.name}`)
1899
+ }
1900
+
1901
+ const targetModelClass = relationship.getTargetModelClass()
1902
+
1903
+ if (!targetModelClass) {
1904
+ throw new Error(`No target model class for where relationship "${attributeName}" on ${modelClass.name}`)
1905
+ }
1906
+
1907
+ const relationshipPath = [...path, attributeName]
1908
+
1909
+ this.applyFrontendModelWhereForPath({
1910
+ modelClass: targetModelClass,
1911
+ path: relationshipPath,
1912
+ query,
1913
+ where: value
1914
+ })
1915
+
1916
+ continue
1917
+ }
1918
+
1919
+ throw new Error(`Unknown where column "${attributeName}" for ${modelClass.name}`)
1920
+ }
1921
+ }
1922
+
1923
+ /**
1924
+ * Runs normalize frontend model where column value.
1925
+ * @param {object} args - Args.
1926
+ * @param {typeof import("./database/record/index.js").default} args.modelClass - Model class.
1927
+ * @param {string} args.columnName - Column name.
1928
+ * @param {?} args.value - Where value.
1929
+ * @returns {? | symbol} - SQL-safe where value.
1930
+ */
1931
+ normalizeFrontendModelWhereColumnValue({columnName, modelClass, value}) {
1932
+ if (typeof value === "string") {
1933
+ const columnType = modelClass.getColumnTypeByName(columnName)?.toLowerCase()
1934
+ const isDateTimeColumn = typeof columnType === "string" && ["date", "datetime", "timestamp"].some((type) => columnType.includes(type))
1935
+
1936
+ if (isDateTimeColumn) {
1937
+ const parsedDate = new Date(value)
1938
+
1939
+ if (!Number.isNaN(parsedDate.getTime())) {
1940
+ return parsedDate
1941
+ }
1942
+ }
1943
+ }
1944
+
1945
+ if (isPlainObject(value)) {
1946
+ const columnType = modelClass.getColumnTypeByName(columnName)
1947
+
1948
+ if (typeof columnType !== "string") {
1949
+ return frontendModelWhereNoMatchSymbol
1950
+ }
1951
+
1952
+ const normalizedType = columnType.toLowerCase()
1953
+ const objectValueTypes = new Set(["char", "varchar", "nvarchar", "string", "enum", "json", "jsonb", "citext", "binary", "varbinary"])
1954
+ const supportsObjectValues = normalizedType.includes("text") || objectValueTypes.has(normalizedType)
1955
+
1956
+ if (!supportsObjectValues) {
1957
+ return frontendModelWhereNoMatchSymbol
1958
+ }
1959
+
1960
+ return JSON.stringify(value)
1961
+ }
1962
+
1963
+ return value
1964
+ }
1965
+
1966
+ /**
1967
+ * Runs apply frontend model group.
1968
+ * @param {object} args - Group args.
1969
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
1970
+ * @param {FrontendModelGroup} args.group - Group definition.
1971
+ * @returns {void}
1972
+ */
1973
+ applyFrontendModelGroup({query, group}) {
1974
+ const modelClass = this.frontendModelClass()
1975
+ const targetModelClass = this.frontendModelSearchTargetModelClass({
1976
+ modelClass,
1977
+ path: group.path
1978
+ })
1979
+ const columnName = this.resolveFrontendModelColumnName(targetModelClass, group.column)
1980
+
1981
+ if (!columnName) {
1982
+ throw new Error(`Unknown group column "${group.column}" for ${targetModelClass.name}`)
1983
+ }
1984
+
1985
+ this.ensureFrontendModelJoinPath({path: group.path, query})
1986
+
1987
+ const tableReference = query.getTableReferenceForJoin(...group.path)
1988
+ const columnSql = `${query.driver.quoteTable(tableReference)}.${query.driver.quoteColumn(columnName)}`
1989
+
1990
+ this.ensureFrontendModelGroupColumn({columnSql, query})
1991
+ }
1992
+
1993
+ /**
1994
+ * Adds root-model columns to GROUP BY so strict SQL engines accept default root-table selects.
1995
+ * @param {object} args - Args.
1996
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
1997
+ * @returns {void}
1998
+ */
1999
+ applyFrontendModelRootGroupColumns({query}) {
2000
+ const modelClass = this.frontendModelClass()
2001
+ const attributeNameToColumnNameMap = modelClass.getAttributeNameToColumnNameMap()
2002
+ const rootTableReference = query.getTableReferenceForJoin()
2003
+
2004
+ for (const columnName of Object.values(attributeNameToColumnNameMap)) {
2005
+ const columnSql = `${query.driver.quoteTable(rootTableReference)}.${query.driver.quoteColumn(columnName)}`
2006
+
2007
+ this.ensureFrontendModelGroupColumn({columnSql, query})
2008
+ }
2009
+ }
2010
+
2011
+ /**
2012
+ * Ensures a group-by SQL column is only appended once.
2013
+ * @param {object} args - Args.
2014
+ * @param {string} args.columnSql - Fully-qualified column SQL.
2015
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
2016
+ * @returns {void}
2017
+ */
2018
+ ensureFrontendModelGroupColumn({columnSql, query}) {
2019
+ const queryMetadata = frontendModelQueryMetadata(query)
2020
+ const groupedColumns = queryMetadata[frontendModelGroupedColumnsSymbol] || new Set()
2021
+
2022
+ if (groupedColumns.has(columnSql)) return
2023
+
2024
+ query.group(columnSql)
2025
+ groupedColumns.add(columnSql)
2026
+ queryMetadata[frontendModelGroupedColumnsSymbol] = groupedColumns
2027
+ }
2028
+
2029
+ /**
2030
+ * Runs apply frontend model translated attribute preloads.
2031
+ * @param {object} args - Args.
2032
+ * @param {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} args.query - Query instance.
2033
+ * @returns {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} - Query with translations preloaded if needed.
2034
+ */
2035
+ applyFrontendModelTranslatedAttributePreloads({query}) {
2036
+ const modelClass = this.frontendModelClass()
2037
+ const selectedAttributes = this.frontendModelEffectiveSelectedAttributesForModelClass(modelClass, this.frontendModelDefaultAttributesForModelClass(modelClass) || [])
2038
+ || this.frontendModelDefaultAttributesForModelClass(modelClass)
2039
+
2040
+ if (!selectedAttributes) return query
2041
+
2042
+ const resource = this.frontendModelResourceInstance()
2043
+ const resourceClass = /**
2044
+ * Types the following value.
2045
+ @type {typeof import("./frontend-model-resource/base-resource.js").default} */ (resource.constructor)
2046
+ const translatedSet = new Set(resourceClass.translatedAttributes || [])
2047
+ let needsTranslations = false
2048
+
2049
+ for (const attributeName of selectedAttributes) {
2050
+ const hookName = `${attributeName}AttributeSelected`
2051
+ const dynamicResource = /**
2052
+ * Types the following value.
2053
+ @type {Record<string, ?>} */ (/**
2054
+ * Types the following value.
2055
+ @type {?} */ (resource))
2056
+
2057
+ if (typeof dynamicResource[hookName] === "function") {
2058
+ const result = dynamicResource[hookName]({query})
2059
+
2060
+ if (result) {
2061
+ query = result
2062
+ }
2063
+ } else if (translatedSet.has(attributeName)) {
2064
+ needsTranslations = true
2065
+ }
2066
+ }
2067
+
2068
+ if (needsTranslations) {
2069
+ query = query.preload({translations: {}})
2070
+ }
2071
+
2072
+ return query
2073
+ }
2074
+
2075
+ /**
2076
+ * Runs apply frontend model sort.
2077
+ * @param {object} args - Sort args.
2078
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
2079
+ * @param {FrontendModelSort} args.sort - Sort definition.
2080
+ * @returns {void}
2081
+ */
2082
+ applyFrontendModelSort({query, sort}) {
2083
+ const modelClass = this.frontendModelClass()
2084
+ const targetModelClass = this.frontendModelSearchTargetModelClass({
2085
+ modelClass,
2086
+ path: sort.path
2087
+ })
2088
+ const translatedAttributesMap = targetModelClass.getTranslationsMap()
2089
+ const translatedAttributeNames = Object.keys(translatedAttributesMap)
2090
+ const isTranslatedSortAttribute = translatedAttributeNames.includes(sort.column)
2091
+
2092
+ const columnName = this.resolveFrontendModelColumnName(targetModelClass, sort.column)
2093
+ const direction = sort.direction.toUpperCase()
2094
+
2095
+ if (isTranslatedSortAttribute) {
2096
+ const translationModelClass = targetModelClass.getTranslationClass()
2097
+ const translationAttributeNameToColumnNameMap = translationModelClass.getAttributeNameToColumnNameMap()
2098
+ const translationColumnName = translationAttributeNameToColumnNameMap[sort.column]
2099
+ const translationPath = sort.path.concat(["currentTranslation"])
2100
+
2101
+ if (!translationColumnName) {
2102
+ throw new Error(`Unknown translated sort column "${sort.column}" for ${targetModelClass.name}`)
2103
+ }
2104
+
2105
+ this.ensureFrontendModelSortJoinPath({path: translationPath, query})
2106
+
2107
+ const translationTableReference = query.getTableReferenceForJoin(...translationPath)
2108
+ const translationColumnSql = `${query.driver.quoteTable(translationTableReference)}.${query.driver.quoteColumn(translationColumnName)}`
2109
+
2110
+ query.order(`${translationColumnSql} ${direction}`)
2111
+
2112
+ return
2113
+ }
2114
+
2115
+ if (!columnName) {
2116
+ throw new Error(`Unknown sort column "${sort.column}" for ${targetModelClass.name}`)
2117
+ }
2118
+
2119
+ this.ensureFrontendModelSortJoinPath({path: sort.path, query})
2120
+
2121
+ const tableReference = query.getTableReferenceForJoin(...sort.path)
2122
+ const columnSql = `${query.driver.quoteTable(tableReference)}.${query.driver.quoteColumn(columnName)}`
2123
+
2124
+ query.order(`${columnSql} ${direction}`)
2125
+ }
2126
+
2127
+ /**
2128
+ * Ensures a sort join path has been joined on query.
2129
+ * @param {object} args - Join args.
2130
+ * @param {string[]} args.path - Relationship join path.
2131
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
2132
+ * @returns {void}
2133
+ */
2134
+ ensureFrontendModelSortJoinPath({path, query}) {
2135
+ this.ensureFrontendModelJoinPath({path, query})
2136
+ }
2137
+
2138
+ /**
2139
+ * Ensures a relationship path has exactly one SQL join.
2140
+ * @param {object} args - Join args.
2141
+ * @param {string[]} args.path - Relationship join path.
2142
+ * @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
2143
+ * @returns {void}
2144
+ */
2145
+ ensureFrontendModelJoinPath({path, query}) {
2146
+ if (path.length < 1) return
2147
+
2148
+ const queryMetadata = frontendModelQueryMetadata(query)
2149
+ const joinedPaths = queryMetadata[frontendModelJoinedPathsSymbol] || new Set()
2150
+ const pathKey = path.join(".")
2151
+
2152
+ if (joinedPaths.has(pathKey)) return
2153
+
2154
+ query.joins(buildFrontendModelJoinObjectFromPath(path))
2155
+ joinedPaths.add(pathKey)
2156
+ queryMetadata[frontendModelJoinedPathsSymbol] = joinedPaths
2157
+ }
2158
+
2159
+ /**
2160
+ * Runs frontend model selected attributes for model class.
2161
+ * @param {typeof import("./database/record/index.js").default} modelClass - Model class.
2162
+ * @returns {string[] | null} - Selected attributes for model class.
2163
+ */
2164
+ frontendModelSelectedAttributesForModelClass(modelClass) {
2165
+ const select = this.frontendModelSelect()
2166
+
2167
+ if (!select) return null
2168
+
2169
+ return select[modelClass.getModelName()] || null
2170
+ }
2171
+
2172
+ /**
2173
+ * Runs frontend model selects extra for model class.
2174
+ * @param {typeof import("./database/record/index.js").default} modelClass - Model class.
2175
+ * @returns {string[] | null} - Extra attributes (loaded in addition to the defaults) for the model class.
2176
+ */
2177
+ frontendModelSelectsExtraForModelClass(modelClass) {
2178
+ const selectsExtra = this.frontendModelSelectsExtra()
2179
+
2180
+ if (!selectsExtra) return null
2181
+
2182
+ return selectsExtra[modelClass.getModelName()] || null
2183
+ }
2184
+
2185
+ /**
2186
+ * Resolves the final set of attribute names to serialize for a model class:
2187
+ * an explicit narrowing `select` wins; otherwise, when `selectsExtra` is given,
2188
+ * the default attributes plus the extras; otherwise null (default behavior).
2189
+ * @param {typeof import("./database/record/index.js").default} modelClass - Model class.
2190
+ * @param {string[]} fallbackAttributeNames - Attribute names to treat as the defaults when the resource declares none.
2191
+ * @returns {string[] | null} - Effective selected attribute names, or null for default serialization.
2192
+ */
2193
+ frontendModelEffectiveSelectedAttributesForModelClass(modelClass, fallbackAttributeNames) {
2194
+ const selectedAttributes = this.frontendModelSelectedAttributesForModelClass(modelClass)
2195
+
2196
+ if (selectedAttributes) return selectedAttributes
2197
+
2198
+ const extraAttributes = this.frontendModelSelectsExtraForModelClass(modelClass)
2199
+
2200
+ if (!extraAttributes) return null
2201
+
2202
+ const defaultAttributes = this.frontendModelDefaultAttributesForModelClass(modelClass) || fallbackAttributeNames
2203
+
2204
+ return Array.from(new Set([...defaultAttributes, ...extraAttributes]))
2205
+ }
2206
+
2207
+ /**
2208
+ * Runs frontend model default attributes for model class.
2209
+ * @param {typeof import("./database/record/index.js").default} modelClass - Model class.
2210
+ * @returns {string[] | null} - Default frontend-model attributes declared on the resource.
2211
+ */
2212
+ frontendModelDefaultAttributesForModelClass(modelClass) {
2213
+ const frontendModelResource = this.frontendModelResourceConfigurationForModelClass(modelClass)
2214
+ const attributes = frontendModelResource?.resourceConfiguration.attributes
2215
+
2216
+ if (!attributes) return null
2217
+
2218
+ if (Array.isArray(attributes)) {
2219
+ return attributes
2220
+ .filter((entry) => {
2221
+ if (typeof entry === "string") return true
2222
+
2223
+ const config = /**
2224
+ * Types the following value.
2225
+ @type {Record<string, ?>} */ (entry)
2226
+
2227
+ if (config && config.selectedByDefault === false) return false
2228
+
2229
+ return true
2230
+ })
2231
+ .map((entry) => typeof entry === "string" ? entry : /**
2232
+ * Types the following value.
2233
+ @type {Record<string, ?>} */ (entry).name)
2234
+ }
2235
+
2236
+ if (typeof attributes === "object") {
2237
+ return Object.entries(attributes)
2238
+ .filter(([, config]) => {
2239
+ if (!config || typeof config !== "object") return true
2240
+
2241
+ return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (config).selectedByDefault !== false
2242
+ })
2243
+ .map(([name]) => name)
2244
+ }
2245
+
2246
+ return null
2247
+ }
2248
+
2249
+ /**
2250
+ * Runs frontend model non default attributes for model class.
2251
+ * @param {typeof import("./database/record/index.js").default} modelClass - Model class.
2252
+ * @returns {string[]} - Attribute names explicitly marked selectedByDefault: false.
2253
+ */
2254
+ frontendModelNonDefaultAttributesForModelClass(modelClass) {
2255
+ const frontendModelResource = this.frontendModelResourceConfigurationForModelClass(modelClass)
2256
+ const attributes = frontendModelResource?.resourceConfiguration.attributes
2257
+
2258
+ if (!attributes) return []
2259
+
2260
+ if (Array.isArray(attributes)) {
2261
+ return attributes
2262
+ .filter((entry) => {
2263
+ if (typeof entry !== "object" || !entry) return false
2264
+
2265
+ return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (entry).selectedByDefault === false
2266
+ })
2267
+ .map((entry) => /**
2268
+ * Types the following value.
2269
+ @type {Record<string, ?>} */ (/**
2270
+ * Types the following value.
2271
+ @type {?} */ (entry)).name)
2272
+ }
2273
+
2274
+ if (typeof attributes === "object") {
2275
+ return Object.entries(attributes)
2276
+ .filter(([, config]) => typeof config === "object" && config && /**
2277
+ * Types the following value.
2278
+ @type {Record<string, ?>} */ (config).selectedByDefault === false)
2279
+ .map(([name]) => name)
2280
+ }
2281
+
2282
+ return []
2283
+ }
2284
+
2285
+ /**
2286
+ * Runs serialize frontend model attributes.
2287
+ * @param {import("./database/record/index.js").default} model - Model instance.
2288
+ * @returns {Promise<Record<string, ?>>} - Serialized attributes filtered by select map.
2289
+ */
2290
+ async serializeFrontendModelAttributes(model) {
2291
+ const modelClass = /**
2292
+ * Types the following value.
2293
+ @type {typeof import("./database/record/index.js").default} */ (model.constructor)
2294
+ const modelAttributes = model.attributes()
2295
+ const selectedAttributes = this.frontendModelEffectiveSelectedAttributesForModelClass(modelClass, Object.keys(modelAttributes))
2296
+ const defaultAttributes = this.frontendModelDefaultAttributesForModelClass(modelClass)
2297
+ const resourceInstance = this._serializationResourceInstanceForModel(model)
2298
+
2299
+ /**
2300
+ * Resource attribute method name.
2301
+ * @param {string} attributeName - Attribute name.
2302
+ */
2303
+ const resourceAttributeMethodName = (attributeName) => `${attributeName}Attribute`
2304
+
2305
+ /**
2306
+ * Resource has attribute.
2307
+ * @param {string} attributeName - Attribute name.
2308
+ */
2309
+ const resourceHasAttribute = (attributeName) => {
2310
+ const methodName = resourceAttributeMethodName(attributeName)
2311
+
2312
+ return resourceInstance && typeof /**
2313
+ * Types the following value.
2314
+ @type {Record<string, ?>} */ (/**
2315
+ * Types the following value.
2316
+ @type {?} */ (resourceInstance))[methodName] === "function"
2317
+ }
2318
+
2319
+ /**
2320
+ * Prototype attribute method.
2321
+ * @param {string} attributeName - Attribute name.
2322
+ */
2323
+ const prototypeAttributeMethod = (attributeName) => {
2324
+ let currentPrototype = Object.getPrototypeOf(model)
2325
+
2326
+ while (currentPrototype && currentPrototype !== Object.prototype) {
2327
+ const candidate = Object.getOwnPropertyDescriptor(currentPrototype, attributeName)?.value
2328
+
2329
+ if (typeof candidate === "function") {
2330
+ return {
2331
+ method: candidate,
2332
+ ownerName: currentPrototype.constructor?.name
2333
+ }
2334
+ }
2335
+
2336
+ currentPrototype = Object.getPrototypeOf(currentPrototype)
2337
+ }
2338
+ }
2339
+
2340
+ /**
2341
+ * Serialized attribute value.
2342
+ * @param {string} attributeName - Attribute name.
2343
+ */
2344
+ const serializedAttributeValue = async (attributeName) => {
2345
+ // Check resource instance first (virtual/computed attributes via ${name}Attribute convention)
2346
+ if (resourceHasAttribute(attributeName)) {
2347
+ const methodName = resourceAttributeMethodName(attributeName)
2348
+
2349
+ return await /**
2350
+ * Types the following value.
2351
+ @type {Record<string, Function>} */ (/**
2352
+ * Types the following value.
2353
+ @type {?} */ (resourceInstance))[methodName](model)
2354
+ }
2355
+
2356
+ // Fall back to model method
2357
+ const attributeMethodLookup = prototypeAttributeMethod(attributeName)
2358
+ const attributeMethod = attributeMethodLookup?.method
2359
+
2360
+ if (typeof attributeMethod === "function") {
2361
+ return await attributeMethod.call(model)
2362
+ }
2363
+
2364
+ return modelAttributes[attributeName]
2365
+ }
2366
+
2367
+ /**
2368
+ * Attribute exists.
2369
+ * @param {string} attributeName - Attribute name.
2370
+ */
2371
+ const attributeExists = (attributeName) => {
2372
+ return (attributeName in modelAttributes) || (attributeName in /**
2373
+ * Types the following value.
2374
+ @type {Record<string, ?>} */ (model)) || resourceHasAttribute(attributeName)
2375
+ }
2376
+
2377
+ if (!selectedAttributes) {
2378
+ if (!defaultAttributes || defaultAttributes.length < 1) {
2379
+ return modelAttributes
2380
+ }
2381
+
2382
+ const excludedAttributes = this.frontendModelNonDefaultAttributesForModelClass(modelClass)
2383
+ const serializedAttributes = {...modelAttributes}
2384
+
2385
+ for (const excludedName of excludedAttributes) {
2386
+ delete serializedAttributes[excludedName]
2387
+ }
2388
+
2389
+ for (const attributeName of defaultAttributes) {
2390
+ if (!attributeExists(attributeName)) continue
2391
+ serializedAttributes[attributeName] = await serializedAttributeValue(attributeName)
2392
+ }
2393
+
2394
+ return serializedAttributes
2395
+ }
2396
+
2397
+ /**
2398
+ * Serialized attributes.
2399
+ @type {Record<string, ?>} */
2400
+ const serializedAttributes = {}
2401
+
2402
+ for (const attributeName of selectedAttributes) {
2403
+ if (!attributeExists(attributeName)) continue
2404
+ serializedAttributes[attributeName] = await serializedAttributeValue(attributeName)
2405
+ }
2406
+
2407
+ return serializedAttributes
2408
+ }
2409
+
2410
+ /**
2411
+ * Runs serialization resource instance for model.
2412
+ * @param {import("./database/record/index.js").default} model - Model instance.
2413
+ * @returns {import("./frontend-model-resource/base-resource.js").default | null} - Resource instance or null.
2414
+ */
2415
+ _serializationResourceInstanceForModel(model) {
2416
+ const resource = this.frontendModelResourceInstance()
2417
+
2418
+ if (resource.modelClass() === model.constructor) {
2419
+ return resource
2420
+ }
2421
+
2422
+ const configuration = this.getConfiguration()
2423
+ const backendProjects = configuration.getBackendProjects?.() || []
2424
+ const modelClassName = /**
2425
+ * Types the following value.
2426
+ @type {typeof import("./database/record/index.js").default} */ (model.constructor).getModelName()
2427
+
2428
+ for (const backendProject of backendProjects) {
2429
+ const resources = frontendModelResourcesForBackendProject(backendProject)
2430
+ const resourceDefinition = resources[modelClassName]
2431
+ const resourceClass = resourceDefinition ? frontendModelResourceClassFromDefinition(resourceDefinition) : null
2432
+
2433
+ if (resourceClass) {
2434
+ return new resourceClass({
2435
+ ability: this.currentAbility(),
2436
+ context: this.currentAbility()?.getContext() || {},
2437
+ locals: this.currentAbility()?.getLocals() || {},
2438
+ modelClass: /**
2439
+ * Types the following value.
2440
+ @type {typeof import("./database/record/index.js").default} */ (model.constructor),
2441
+ modelName: modelClassName,
2442
+ params: {},
2443
+ resourceConfiguration: /**
2444
+ * Types the following value.
2445
+ @type {import("./configuration-types.js").FrontendModelResourceConfiguration | undefined} */ (typeof resourceClass.resourceConfig === "function" ? resourceClass.resourceConfig() : undefined)
2446
+ })
2447
+ }
2448
+ }
2449
+
2450
+ return null
2451
+ }
2452
+
2453
+ /**
2454
+ * Runs frontend model filter serializable related models.
2455
+ * @param {object} args - Arguments.
2456
+ * @param {import("./database/record/index.js").default[]} args.models - Frontend model records.
2457
+ * @param {boolean} args.relationshipIsCollection - Whether relation is has-many.
2458
+ * @returns {Promise<import("./database/record/index.js").default[]>} - Serializable related models.
2459
+ */
2460
+ async frontendModelFilterSerializableRelatedModels({models, relationshipIsCollection}) {
2461
+ if (!this.currentAbility()) return models
2462
+ if (models.length === 0) return models
2463
+
2464
+ /**
2465
+ * Models by class.
2466
+ @type {Map<typeof import("./database/record/index.js").default, import("./database/record/index.js").default[]>} */
2467
+ const modelsByClass = new Map()
2468
+
2469
+ for (const model of models) {
2470
+ const relatedModelClass = /**
2471
+ * Types the following value.
2472
+ @type {typeof import("./database/record/index.js").default} */ (model.constructor)
2473
+ const existingModelsForClass = modelsByClass.get(relatedModelClass) || []
2474
+
2475
+ existingModelsForClass.push(model)
2476
+ modelsByClass.set(relatedModelClass, existingModelsForClass)
2477
+ }
2478
+
2479
+ /**
2480
+ * Authorized ids by class.
2481
+ @type {Map<typeof import("./database/record/index.js").default, Set<string>>} */
2482
+ const authorizedIdsByClass = new Map()
2483
+ /**
2484
+ * Primary keys by class.
2485
+ @type {Map<typeof import("./database/record/index.js").default, string>} */
2486
+ const primaryKeysByClass = new Map()
2487
+
2488
+ for (const [relatedModelClass, relatedModels] of modelsByClass.entries()) {
2489
+ const relatedResource = this.frontendModelResourceConfigurationForModelClass(relatedModelClass)
2490
+
2491
+ if (!relatedResource) {
2492
+ authorizedIdsByClass.set(relatedModelClass, new Set())
2493
+ continue
2494
+ }
2495
+
2496
+ const abilityAction = relationshipIsCollection
2497
+ ? relatedResource.resourceConfiguration.abilities?.index
2498
+ : relatedResource.resourceConfiguration.abilities?.find
2499
+
2500
+ if (typeof abilityAction !== "string" || abilityAction.length < 1) {
2501
+ authorizedIdsByClass.set(relatedModelClass, new Set())
2502
+ continue
2503
+ }
2504
+
2505
+ const primaryKey = relatedModelClass.primaryKey()
2506
+ const ids = relatedModels
2507
+ .map((model) => model.attributes()[primaryKey])
2508
+ .filter((id) => id !== undefined && id !== null)
2509
+
2510
+ if (ids.length < 1) {
2511
+ authorizedIdsByClass.set(relatedModelClass, new Set())
2512
+ continue
2513
+ }
2514
+
2515
+ const authorizedIdsRaw = await relatedModelClass
2516
+ .accessibleFor(abilityAction)
2517
+ .where({[primaryKey]: ids})
2518
+ .pluck(primaryKey)
2519
+
2520
+ primaryKeysByClass.set(relatedModelClass, primaryKey)
2521
+ authorizedIdsByClass.set(relatedModelClass, new Set(authorizedIdsRaw.map((id) => String(id))))
2522
+ }
2523
+
2524
+ return models.filter((model) => {
2525
+ const relatedModelClass = /**
2526
+ * Types the following value.
2527
+ @type {typeof import("./database/record/index.js").default} */ (model.constructor)
2528
+ const authorizedIds = authorizedIdsByClass.get(relatedModelClass)
2529
+ const primaryKey = primaryKeysByClass.get(relatedModelClass)
2530
+
2531
+ if (!authorizedIds || !primaryKey) return false
2532
+
2533
+ const primaryKeyValue = model.attributes()[primaryKey]
2534
+
2535
+ if (primaryKeyValue === undefined || primaryKeyValue === null) return false
2536
+
2537
+ return authorizedIds.has(String(primaryKeyValue))
2538
+ })
2539
+ }
2540
+
2541
+ /**
2542
+ * Runs is serializable frontend model.
2543
+ * @param {?} value - Candidate preloaded value.
2544
+ * @returns {value is import("./database/record/index.js").default} - Whether value behaves like a model.
2545
+ */
2546
+ isSerializableFrontendModel(value) {
2547
+ return Boolean(value && typeof value === "object" && typeof /**
2548
+ * Types the following value.
2549
+ @type {?} */ (value).attributes === "function")
2550
+ }
2551
+
2552
+ /**
2553
+ * Runs serialize frontend models.
2554
+ * @param {import("./database/record/index.js").default[]} models - Models to serialize.
2555
+ * @returns {Promise<Record<string, ?>[]>} - Serialized model payloads.
2556
+ */
2557
+ async serializeFrontendModels(models) {
2558
+ if (models.length < 1) return []
2559
+
2560
+ /**
2561
+ * Preloaded relationships per model.
2562
+ @type {Array<Record<string, ?>>} */
2563
+ const preloadedRelationshipsPerModel = Array.from({length: models.length}, () => ({}))
2564
+
2565
+ /**
2566
+ * Collection relationship entries.
2567
+ @type {Array<{loadedModels: import("./database/record/index.js").default[], modelIndex: number, relationshipName: string}>} */
2568
+ const collectionRelationshipEntries = []
2569
+ /**
2570
+ * Singular relationship entries.
2571
+ @type {Array<{loadedModel: import("./database/record/index.js").default, modelIndex: number, relationshipName: string}>} */
2572
+ const singularRelationshipEntries = []
2573
+
2574
+ models.forEach((model, modelIndex) => {
2575
+ const modelClass = /**
2576
+ * Types the following value.
2577
+ @type {typeof import("./database/record/index.js").default} */ (model.constructor)
2578
+ const relationshipsMap = modelClass.getRelationshipsMap()
2579
+
2580
+ for (const relationshipName in relationshipsMap) {
2581
+ const relationship = model.getRelationshipByName(relationshipName)
2582
+
2583
+ if (!relationship.getPreloaded()) continue
2584
+
2585
+ const loadedRelationship = relationship.loaded()
2586
+
2587
+ if (Array.isArray(loadedRelationship)) {
2588
+ collectionRelationshipEntries.push({loadedModels: loadedRelationship, modelIndex, relationshipName})
2589
+ continue
2590
+ }
2591
+
2592
+ if (this.isSerializableFrontendModel(loadedRelationship)) {
2593
+ singularRelationshipEntries.push({loadedModel: loadedRelationship, modelIndex, relationshipName})
2594
+ continue
2595
+ }
2596
+
2597
+ preloadedRelationshipsPerModel[modelIndex][relationshipName] = loadedRelationship == undefined ? null : loadedRelationship
2598
+ }
2599
+ })
2600
+
2601
+ if (collectionRelationshipEntries.length > 0) {
2602
+ const allCollectionModels = collectionRelationshipEntries.flatMap((entry) => entry.loadedModels)
2603
+ const serializableCollectionModels = await this.frontendModelFilterSerializableRelatedModels({
2604
+ models: allCollectionModels,
2605
+ relationshipIsCollection: true
2606
+ })
2607
+ const serializableCollectionModelsSet = new Set(serializableCollectionModels)
2608
+
2609
+ for (const relationshipEntry of collectionRelationshipEntries) {
2610
+ const allowedModels = relationshipEntry.loadedModels.filter((relatedModel) => serializableCollectionModelsSet.has(relatedModel))
2611
+ const serializedRelatedModels = await this.serializeFrontendModels(allowedModels)
2612
+
2613
+ preloadedRelationshipsPerModel[relationshipEntry.modelIndex][relationshipEntry.relationshipName] = serializedRelatedModels
2614
+ }
2615
+ }
2616
+
2617
+ if (singularRelationshipEntries.length > 0) {
2618
+ const allSingularModels = singularRelationshipEntries.map((entry) => entry.loadedModel)
2619
+ const serializableSingularModels = await this.frontendModelFilterSerializableRelatedModels({
2620
+ models: allSingularModels,
2621
+ relationshipIsCollection: false
2622
+ })
2623
+ const serializableSingularModelsSet = new Set(serializableSingularModels)
2624
+
2625
+ for (const relationshipEntry of singularRelationshipEntries) {
2626
+ if (!serializableSingularModelsSet.has(relationshipEntry.loadedModel)) {
2627
+ preloadedRelationshipsPerModel[relationshipEntry.modelIndex][relationshipEntry.relationshipName] = null
2628
+ continue
2629
+ }
2630
+
2631
+ const serializedModel = (await this.serializeFrontendModels([relationshipEntry.loadedModel]))[0]
2632
+ preloadedRelationshipsPerModel[relationshipEntry.modelIndex][relationshipEntry.relationshipName] = serializedModel
2633
+ }
2634
+ }
2635
+
2636
+ /**
2637
+ * Serialized models.
2638
+ @type {Record<string, ?>[]} */
2639
+ const serializedModels = []
2640
+
2641
+ for (const [modelIndex, model] of models.entries()) {
2642
+ const serializedAttributes = await this.serializeFrontendModelAttributes(model)
2643
+ const preloadedRelationships = preloadedRelationshipsPerModel[modelIndex]
2644
+ const associationCounts = typeof model.associationCounts === "function"
2645
+ ? model.associationCounts()
2646
+ : {}
2647
+ const queryDataValues = typeof model.queryDataValues === "function"
2648
+ ? model.queryDataValues()
2649
+ : {}
2650
+ const computedAbilities = typeof model.computedAbilities === "function"
2651
+ ? model.computedAbilities()
2652
+ : {}
2653
+ const hasCounts = Object.keys(associationCounts).length > 0
2654
+ const hasQueryData = Object.keys(queryDataValues).length > 0
2655
+ const hasAbilities = Object.keys(computedAbilities).length > 0
2656
+ const hasPreloaded = Object.keys(preloadedRelationships).length > 0
2657
+
2658
+ if (!hasPreloaded && !hasCounts && !hasQueryData && !hasAbilities) {
2659
+ serializedModels.push(serializedAttributes)
2660
+ continue
2661
+ }
2662
+
2663
+ /**
2664
+ * Serialized.
2665
+ @type {Record<string, ?>} */
2666
+ const serialized = {...serializedAttributes}
2667
+
2668
+ if (hasPreloaded) serialized.__preloadedRelationships = preloadedRelationships
2669
+ if (hasCounts) serialized.__associationCounts = associationCounts
2670
+ if (hasQueryData) serialized.__queryData = queryDataValues
2671
+ if (hasAbilities) serialized.__abilities = computedAbilities
2672
+
2673
+ serializedModels.push(serialized)
2674
+ }
2675
+
2676
+ return serializedModels
2677
+ }
2678
+
2679
+ /**
2680
+ * Runs serialize frontend model.
2681
+ * @param {import("./database/record/index.js").default} model - Frontend model record.
2682
+ * @returns {Promise<Record<string, ?>>} - Serialized frontend model payload.
2683
+ */
2684
+ async serializeFrontendModel(model) {
2685
+ const serializedModels = await this.serializeFrontendModels([model])
2686
+
2687
+ return serializedModels[0]
2688
+ }
2689
+
2690
+ /**
2691
+ * Runs frontend model render error.
2692
+ * @param {string} errorMessage - Error message.
2693
+ * @returns {Promise<void>} - Resolves when error has been rendered.
2694
+ */
2695
+ async frontendModelRenderError(errorMessage) {
2696
+ await this.logger.error(`Frontend model request failed: ${errorMessage}`)
2697
+
2698
+ const renderError = /**
2699
+ * Types the following value.
2700
+ @type {((errorMessage: string) => Promise<void>) | undefined} */ (
2701
+ /**
2702
+ * Types the following value.
2703
+ @type {?} */ (this).renderError
2704
+ )
2705
+
2706
+ if (typeof renderError === "function") {
2707
+ await renderError.call(this, frontendModelClientSafeErrorMessage)
2708
+ return
2709
+ }
2710
+
2711
+ await this.render({
2712
+ json: /**
2713
+ * Types the following value.
2714
+ @type {Record<string, ?>} */ (serializeFrontendModelTransportValue({
2715
+ errorMessage: frontendModelClientSafeErrorMessage,
2716
+ status: "error"
2717
+ }))
2718
+ })
2719
+ }
2720
+
2721
+ /**
2722
+ * Runs frontend model error payload.
2723
+ * @param {string} errorMessage - Error message.
2724
+ * @returns {Record<string, ?>} - Error payload.
2725
+ */
2726
+ frontendModelErrorPayload(errorMessage) {
2727
+ return {
2728
+ errorMessage,
2729
+ status: "error"
2730
+ }
2731
+ }
2732
+
2733
+ /**
2734
+ * Runs frontend model client safe error payload.
2735
+ * @returns {Record<string, ?>} - Client-safe error payload.
2736
+ */
2737
+ frontendModelClientSafeErrorPayload() {
2738
+ return this.frontendModelErrorPayload(frontendModelClientSafeErrorMessage)
2739
+ }
2740
+
2741
+ /**
2742
+ * Runs frontend model client error payload for error.
2743
+ * @param {?} error - Caught error.
2744
+ * @returns {Record<string, ?>} - Client payload for the current environment.
2745
+ */
2746
+ frontendModelClientErrorPayloadForError(error) {
2747
+ const velociousMetadata = frontendModelVelociousMetadataForError(error)
2748
+
2749
+ let validationErrorsPayload = {}
2750
+
2751
+ if (error instanceof ValidationError) {
2752
+ const validationErrors = error.getValidationErrors()
2753
+ const model = error.getModel()
2754
+ /**
2755
+ * Structured errors.
2756
+ @type {Record<string, {type: string, message: string, fullMessage: string}[]>} */
2757
+ const structuredErrors = {}
2758
+
2759
+ for (const attributeName in validationErrors) {
2760
+ structuredErrors[attributeName] = validationErrors[attributeName].map(err => ({
2761
+ type: err.type,
2762
+ message: err.message,
2763
+ fullMessage: `${model.getModelClass().humanAttributeName(attributeName)} ${err.message}`
2764
+ }))
2765
+ }
2766
+
2767
+ validationErrorsPayload = {
2768
+ errorType: "validation_error",
2769
+ validationErrors: structuredErrors
2770
+ }
2771
+ }
2772
+
2773
+ return {
2774
+ ...this.frontendModelErrorPayload(frontendModelClientMessageForError(error)),
2775
+ ...frontendModelDebugPayloadForError({
2776
+ configuration: this.getConfiguration(),
2777
+ environment: this.getConfiguration().getEnvironment(),
2778
+ error
2779
+ }),
2780
+ ...(velociousMetadata ? {velocious: velociousMetadata} : {}),
2781
+ ...validationErrorsPayload
2782
+ }
2783
+ }
2784
+
2785
+ /**
2786
+ * Runs frontend model log endpoint error.
2787
+ * @param {object} args - Error log args.
2788
+ * @param {string} args.action - Endpoint/action label.
2789
+ * @param {?} args.error - Caught error.
2790
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url" | "custom-command"} [args.commandType] - Frontend-model command type.
2791
+ * @param {string | undefined} [args.model] - Request model name when available.
2792
+ * @param {string | undefined} [args.requestId] - Batch request id when available.
2793
+ * @returns {Promise<void>} - Resolves after logging.
2794
+ */
2795
+ async frontendModelLogEndpointError({action, error, commandType, model, requestId}) {
2796
+ // Errors annotated with `error.velocious = {...}` are user-flow
2797
+ // failures the developer has marked as expected (bad password,
2798
+ // validation message, etc.). Surface the message + metadata to
2799
+ // the client (handled by frontendModelClientErrorPayloadForError),
2800
+ // but skip the error log so monitoring stays focused on real
2801
+ // backend failures.
2802
+ if (frontendModelErrorHasVelociousMetadata(error)) return
2803
+
2804
+ let resolvedModel = model
2805
+
2806
+ if (!resolvedModel) {
2807
+ try {
2808
+ resolvedModel = this.frontendModelParams().model
2809
+ } catch {
2810
+ resolvedModel = undefined
2811
+ }
2812
+ }
2813
+
2814
+ const errorMessage = error instanceof Error
2815
+ ? `${error.message}\n${error.stack || ""}`
2816
+ : String(error)
2817
+
2818
+ await this.logger.error(() => ["Frontend model endpoint request failed", {
2819
+ action,
2820
+ commandType,
2821
+ error: errorMessage,
2822
+ model: resolvedModel,
2823
+ requestId
2824
+ }])
2825
+ }
2826
+
2827
+ /**
2828
+ * Runs frontend model render command response.
2829
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
2830
+ * @returns {Promise<void>} - Resolves when response has been rendered.
2831
+ */
2832
+ async frontendModelRenderCommandResponse(action) {
2833
+ try {
2834
+ const responsePayload = await this.frontendModelCommandPayload(action)
2835
+ if (!responsePayload) return
2836
+
2837
+ await this.render({
2838
+ json: /**
2839
+ * Types the following value.
2840
+ @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(responsePayload))
2841
+ })
2842
+ } catch (error) {
2843
+ await this.frontendModelLogEndpointError({action, commandType: action, error})
2844
+
2845
+ await this.render({
2846
+ json: /**
2847
+ * Types the following value.
2848
+ @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(this.frontendModelClientErrorPayloadForError(error)))
2849
+ })
2850
+ }
2851
+ }
2852
+
2853
+ /**
2854
+ * Runs frontend model command payload.
2855
+ * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
2856
+ * @returns {Promise<Record<string, ?> | null>} - Response payload.
2857
+ */
2858
+ async frontendModelCommandPayload(action) {
2859
+ await this.ensureFrontendModelClassInitialized()
2860
+
2861
+ if (!(await this.runFrontendModelBeforeAction(action))) {
2862
+ return null
2863
+ }
2864
+
2865
+ const resource = this.frontendModelResourceInstance()
2866
+
2867
+ if (action === "index") {
2868
+ if (this.frontendModelCountRequested()) {
2869
+ if (!(await resource.supportsCount("index"))) {
2870
+ throw new Error("count is not supported when resource records are customized")
2871
+ }
2872
+
2873
+ return {
2874
+ count: await resource.count(),
2875
+ status: "success"
2876
+ }
2877
+ }
2878
+
2879
+ const pluck = this.frontendModelPluck()
2880
+
2881
+ if (pluck.length > 0) {
2882
+ if (!(await resource.supportsPluck("index"))) {
2883
+ throw new Error("pluck is not supported when resource records are customized")
2884
+ }
2885
+
2886
+ const values = await this.frontendModelPluckValues({
2887
+ pluck,
2888
+ query: this.frontendModelIndexQuery()
2889
+ })
2890
+
2891
+ return {
2892
+ status: "success",
2893
+ values
2894
+ }
2895
+ }
2896
+
2897
+ const models = await this.frontendModelRecords()
2898
+ await this.frontendModelComputeAbilities(models)
2899
+ const serializedModels = await Promise.all(models.map(async (model) => await resource.serialize(model, "index")))
2900
+
2901
+ return {
2902
+ models: serializedModels,
2903
+ status: "success"
2904
+ }
2905
+ }
2906
+
2907
+ const params = this.frontendModelParams()
2908
+ const modelClass = this.frontendModelClass()
2909
+ const id = params.id
2910
+
2911
+ if (action === "create") {
2912
+ const mutationAttributes = frontendModelMutationAttributes(params)
2913
+ if (typeof mutationAttributes === "string") return this.frontendModelErrorPayload(mutationAttributes)
2914
+
2915
+ const model = await this.frontendModelCreateRecord(mutationAttributes.attributes, mutationAttributes.nestedAttributes)
2916
+
2917
+ if (!model) {
2918
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
2919
+ }
2920
+
2921
+ const serializedModel = await resource.serialize(model, "create")
2922
+
2923
+ return frontendModelSerializedModelSuccess(serializedModel)
2924
+ }
2925
+
2926
+ if ((typeof id !== "string" && typeof id !== "number") || `${id}`.length < 1) {
2927
+ return this.frontendModelErrorPayload("Expected model id.")
2928
+ }
2929
+
2930
+ if (action === "attach") {
2931
+ const attachmentName = params.attachmentName
2932
+ const attachmentInput = params.attachment
2933
+
2934
+ if (typeof attachmentName !== "string" || attachmentName.length < 1) {
2935
+ return this.frontendModelErrorPayload("Expected attachmentName.")
2936
+ }
2937
+
2938
+ if (typeof attachmentInput === "undefined") {
2939
+ return this.frontendModelErrorPayload("Expected attachment input.")
2940
+ }
2941
+
2942
+ const model = await this.frontendModelFindRecord("attach", id)
2943
+
2944
+ if (!model) {
2945
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
2946
+ }
2947
+
2948
+ await model.getAttachmentByName(attachmentName).attach(attachmentInput)
2949
+ const serializedModel = await this.serializeFrontendModel(model)
2950
+
2951
+ return frontendModelSerializedModelSuccess(serializedModel)
2952
+ }
2953
+
2954
+ if (action === "download") {
2955
+ const attachmentParams = frontendModelAttachmentParams(params)
2956
+ if (typeof attachmentParams === "string") return this.frontendModelErrorPayload(attachmentParams)
2957
+
2958
+ const model = await this.frontendModelFindRecord("download", id)
2959
+
2960
+ if (!model) {
2961
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
2962
+ }
2963
+
2964
+ const downloadedAttachment = await model.getAttachmentByName(attachmentParams.attachmentName).download(attachmentParams.attachmentId)
2965
+
2966
+ if (!downloadedAttachment) {
2967
+ return this.frontendModelErrorPayload("Attachment not found.")
2968
+ }
2969
+
2970
+ return {
2971
+ attachment: {
2972
+ byteSize: downloadedAttachment.byteSize(),
2973
+ contentBase64: downloadedAttachment.content().toString("base64"),
2974
+ contentType: downloadedAttachment.contentType(),
2975
+ filename: downloadedAttachment.filename(),
2976
+ id: downloadedAttachment.id(),
2977
+ url: downloadedAttachment.url()
2978
+ },
2979
+ status: "success"
2980
+ }
2981
+ }
2982
+
2983
+ if (action === "url") {
2984
+ const attachmentParams = frontendModelAttachmentParams(params)
2985
+ if (typeof attachmentParams === "string") return this.frontendModelErrorPayload(attachmentParams)
2986
+
2987
+ const model = await this.frontendModelFindRecord("url", id)
2988
+
2989
+ if (!model) {
2990
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
2991
+ }
2992
+
2993
+ const url = await model.getAttachmentByName(attachmentParams.attachmentName).url(attachmentParams.attachmentId)
2994
+
2995
+ if (!url) {
2996
+ return this.frontendModelErrorPayload("Attachment URL not available.")
2997
+ }
2998
+
2999
+ return {
3000
+ status: "success",
3001
+ url
3002
+ }
3003
+ }
3004
+
3005
+ if (action === "find") {
3006
+ const model = await this.frontendModelFindRecord("find", id)
3007
+
3008
+ if (!model) {
3009
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3010
+ }
3011
+
3012
+ await this.frontendModelComputeAbilities([model])
3013
+ const serializedModel = await resource.serialize(model, "find")
3014
+
3015
+ return frontendModelSerializedModelSuccess(serializedModel)
3016
+ }
3017
+
3018
+ if (action === "update") {
3019
+ const mutationAttributes = frontendModelMutationAttributes(params)
3020
+ if (typeof mutationAttributes === "string") return this.frontendModelErrorPayload(mutationAttributes)
3021
+
3022
+ const model = await this.frontendModelFindRecord("update", id)
3023
+
3024
+ if (!model) {
3025
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3026
+ }
3027
+
3028
+ const updatedModel = await resource.update(model, mutationAttributes.attributes, {nestedAttributes: mutationAttributes.nestedAttributes, controller: this})
3029
+ const serializedModel = await resource.serialize(updatedModel, "update")
3030
+
3031
+ return frontendModelSerializedModelSuccess(serializedModel)
3032
+ }
3033
+
3034
+ const model = await this.frontendModelFindRecord("destroy", id)
3035
+
3036
+ if (!model) {
3037
+ return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
3038
+ }
3039
+
3040
+ await resource.destroy(model)
3041
+
3042
+ return {status: "success"}
3043
+ }
3044
+
3045
+ /**
3046
+ * Runs frontend api.
3047
+ * @returns {Promise<void>} - Shared frontend model API action with batch support.
3048
+ */
3049
+ async frontendApi() {
3050
+ if (this.request().httpMethod() === "OPTIONS") {
3051
+ await this.render({status: 204, json: {}})
3052
+ return
3053
+ }
3054
+
3055
+ const params = /**
3056
+ * Types the following value.
3057
+ @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(this.params()))
3058
+ const requests = Array.isArray(params.requests) ? params.requests : [params]
3059
+ /**
3060
+ * Responses.
3061
+ @type {Array<Record<string, ?>>} */
3062
+ const responses = []
3063
+
3064
+ for (const requestEntry of requests) {
3065
+ const commandType = requestEntry?.commandType
3066
+ const customPath = requestEntry?.customPath
3067
+ const model = requestEntry?.model
3068
+ const payload = requestEntry?.payload
3069
+ const requestId = requestEntry?.requestId
3070
+
3071
+ if (typeof model !== "string" || model.length < 1) {
3072
+ responses.push({
3073
+ requestId,
3074
+ response: this.frontendModelErrorPayload("Expected request model.")
3075
+ })
3076
+ continue
3077
+ }
3078
+
3079
+ const isBuiltInCommand = ["index", "find", "create", "update", "destroy", "attach", "download", "url"].includes(commandType)
3080
+
3081
+ if (!isBuiltInCommand && (typeof customPath !== "string" || !customPath.startsWith("/"))) {
3082
+ responses.push({
3083
+ requestId,
3084
+ response: this.frontendModelErrorPayload("Expected request customPath.")
3085
+ })
3086
+ continue
3087
+ }
3088
+
3089
+ try {
3090
+ let responsePayload
3091
+
3092
+ if (isBuiltInCommand) {
3093
+ const commandParams = {
3094
+ ...(payload && typeof payload === "object" ? payload : {}),
3095
+ model
3096
+ }
3097
+
3098
+ responsePayload = await this.withFrontendModelParams(commandParams, async () => {
3099
+ return await this.withFrontendModelRequestContext(commandParams, this.response(), async () => {
3100
+ return await this.frontendModelCommandPayload(commandType)
3101
+ })
3102
+ })
3103
+ } else {
3104
+ responsePayload = await this.frontendApiCustomCommandPayload({
3105
+ customPath,
3106
+ payload
3107
+ })
3108
+ }
3109
+
3110
+ responses.push({
3111
+ requestId,
3112
+ response: responsePayload || this.frontendModelErrorPayload("Action halted by beforeAction.")
3113
+ })
3114
+ } catch (error) {
3115
+ await this.frontendModelLogEndpointError({
3116
+ action: "frontendApi",
3117
+ commandType,
3118
+ error,
3119
+ model,
3120
+ requestId
3121
+ })
3122
+
3123
+ responses.push({
3124
+ requestId,
3125
+ response: this.frontendModelClientErrorPayloadForError(error)
3126
+ })
3127
+ }
3128
+ }
3129
+
3130
+ await this.render({
3131
+ json: /**
3132
+ * Types the following value.
3133
+ @type {Record<string, ?>} */ (serializeFrontendModelTransportValue({
3134
+ responses,
3135
+ status: "success"
3136
+ }))
3137
+ })
3138
+ }
3139
+
3140
+ /**
3141
+ * Dispatches a custom frontend-model command through the shared frontend-model API endpoint.
3142
+ * @param {object} args - Arguments.
3143
+ * @param {string} args.customPath - Custom backend route path.
3144
+ * @param {?} args.payload - Request payload.
3145
+ * @returns {Promise<Record<string, ?>>} - Parsed JSON response payload.
3146
+ */
3147
+ async frontendApiCustomCommandPayload({customPath, payload}) {
3148
+ const configuration = this.getConfiguration()
3149
+ const response = new Response({configuration})
3150
+ const resolver = new RoutesResolver({
3151
+ configuration,
3152
+ request: this.getRequest(),
3153
+ response
3154
+ })
3155
+ resolver.params = {}
3156
+ const routeHookMatch = await resolver.resolveRouteResolverHooks(customPath)
3157
+ const configurationRoutes = configuration.getRoutes()
3158
+ const routeMatch = routeHookMatch || !configurationRoutes?.rootRoute ? undefined : resolver.matchPathWithRoutes(configurationRoutes.rootRoute, customPath)
3159
+
3160
+ if (!routeHookMatch && !routeMatch) {
3161
+ throw new Error(`No custom frontend model route matched '${customPath}'`)
3162
+ }
3163
+
3164
+ const actionParam = routeHookMatch?.action || resolver.params.action
3165
+ const controllerParam = routeHookMatch?.controller || resolver.params.controller
3166
+ const actionValue = typeof actionParam === "string" ? actionParam : (Array.isArray(actionParam) ? actionParam[0] : undefined)
3167
+ const controllerValue = typeof controllerParam === "string" ? controllerParam : (Array.isArray(controllerParam) ? controllerParam[0] : undefined)
3168
+
3169
+ if (typeof actionValue !== "string" || actionValue.length < 1 || typeof controllerValue !== "string" || controllerValue.length < 1) {
3170
+ throw new Error(`Custom frontend model route matched '${customPath}' without controller/action params`)
3171
+ }
3172
+
3173
+ const action = inflection.camelize(actionValue.replaceAll("-", "_").replaceAll("/", "_"), true)
3174
+ const controller = controllerValue
3175
+ const controllerPath = routeHookMatch?.controllerPath || `${configuration.getDirectory()}/src/routes/${controller}/controller.js`
3176
+ const viewPath = routeHookMatch?.viewPath || `${configuration.getDirectory()}/src/routes/${controller}`
3177
+ resolver.routeHookControllerClass = routeHookMatch?.controllerClass
3178
+ const controllerClass = await resolver.resolveControllerClass({controllerPath})
3179
+ const controllerParams = {
3180
+ ...((payload && typeof payload === "object") ? payload : {}),
3181
+ ...resolver.params
3182
+ }
3183
+ const controllerInstance = new controllerClass({
3184
+ action,
3185
+ configuration,
3186
+ controller,
3187
+ params: controllerParams,
3188
+ request: /**
3189
+ * Types the following value.
3190
+ @type {import("./http-server/client/request.js").default} */ (this.getRequest()),
3191
+ response,
3192
+ viewPath
3193
+ })
3194
+
3195
+ await this.withFrontendModelRequestContext(controllerParams, response, async () => {
3196
+ await controllerInstance._runBeforeCallbacks()
3197
+ const controllerMethods = /**
3198
+ * Types the following value.
3199
+ @type {Record<string, () => Promise<void> | void>} */ (/**
3200
+ * Types the following value.
3201
+ @type {?} */ (controllerInstance))
3202
+
3203
+ await controllerMethods[action]()
3204
+ })
3205
+
3206
+ const setCookieHeaders = response.headers["Set-Cookie"] || []
3207
+
3208
+ for (const setCookieHeader of setCookieHeaders) {
3209
+ this.response().addHeader("Set-Cookie", setCookieHeader)
3210
+ }
3211
+
3212
+ const responseBody = response.getBody()
3213
+
3214
+ if (typeof responseBody !== "string" || responseBody.length < 1) {
3215
+ return {}
3216
+ }
3217
+
3218
+ // Preserve nested transport markers so the outer shared frontend-model API
3219
+ // can return them unchanged and let the client hydrate once at the edge.
3220
+ return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (JSON.parse(responseBody))
3221
+ }
3222
+
3223
+ /**
3224
+ * Runs frontend index.
3225
+ * @returns {Promise<void>} - Collection action for frontend model resources.
3226
+ */
3227
+ async frontendIndex() {
3228
+ if (this.request().httpMethod() === "OPTIONS") {
3229
+ await this.render({status: 204, json: {}})
3230
+ return
3231
+ }
3232
+
3233
+ await this.frontendModelRenderCommandResponse("index")
3234
+ }
3235
+
3236
+ /**
3237
+ * Runs frontend find.
3238
+ * @returns {Promise<void>} - Member find action for frontend model resources.
3239
+ */
3240
+ async frontendFind() {
3241
+ if (this.request().httpMethod() === "OPTIONS") {
3242
+ await this.render({status: 204, json: {}})
3243
+ return
3244
+ }
3245
+
3246
+ await this.frontendModelRenderCommandResponse("find")
3247
+ }
3248
+
3249
+ /**
3250
+ * Runs frontend update.
3251
+ * @returns {Promise<void>} - Member update action for frontend model resources.
3252
+ */
3253
+ async frontendUpdate() {
3254
+ if (this.request().httpMethod() === "OPTIONS") {
3255
+ await this.render({status: 204, json: {}})
3256
+ return
3257
+ }
3258
+
3259
+ await this.frontendModelRenderCommandResponse("update")
3260
+ }
3261
+
3262
+ /**
3263
+ * Runs frontend attach.
3264
+ * @returns {Promise<void>} - Member attach action for frontend model resources.
3265
+ */
3266
+ async frontendAttach() {
3267
+ if (this.request().httpMethod() === "OPTIONS") {
3268
+ await this.render({status: 204, json: {}})
3269
+ return
3270
+ }
3271
+
3272
+ await this.frontendModelRenderCommandResponse("attach")
3273
+ }
3274
+
3275
+ /**
3276
+ * Runs frontend download.
3277
+ * @returns {Promise<void>} - Member download action for frontend model resources.
3278
+ */
3279
+ async frontendDownload() {
3280
+ if (this.request().httpMethod() === "OPTIONS") {
3281
+ await this.render({status: 204, json: {}})
3282
+ return
3283
+ }
3284
+
3285
+ await this.frontendModelRenderCommandResponse("download")
3286
+ }
3287
+
3288
+ /**
3289
+ * Runs frontend url.
3290
+ * @returns {Promise<void>} - Member URL action for frontend model resources.
3291
+ */
3292
+ async frontendUrl() {
3293
+ if (this.request().httpMethod() === "OPTIONS") {
3294
+ await this.render({status: 204, json: {}})
3295
+ return
3296
+ }
3297
+
3298
+ await this.frontendModelRenderCommandResponse("url")
3299
+ }
3300
+
3301
+ /**
3302
+ * Runs frontend create.
3303
+ * @returns {Promise<void>} - Member create action for frontend model resources.
3304
+ */
3305
+ async frontendCreate() {
3306
+ if (this.request().httpMethod() === "OPTIONS") {
3307
+ await this.render({status: 204, json: {}})
3308
+ return
3309
+ }
3310
+
3311
+ await this.frontendModelRenderCommandResponse("create")
3312
+ }
3313
+
3314
+ /**
3315
+ * Runs frontend destroy.
3316
+ * @returns {Promise<void>} - Member destroy action for frontend model resources.
3317
+ */
3318
+ async frontendDestroy() {
3319
+ if (this.request().httpMethod() === "OPTIONS") {
3320
+ await this.render({status: 204, json: {}})
3321
+ return
3322
+ }
3323
+
3324
+ await this.frontendModelRenderCommandResponse("destroy")
3325
+ }
3326
+
3327
+ /**
3328
+ * Runs frontend custom command.
3329
+ * @returns {Promise<void>} - Custom collection/member command action for frontend-model resources.
3330
+ */
3331
+ async frontendCustomCommand() {
3332
+ if (this.request().httpMethod() === "OPTIONS") {
3333
+ await this.render({status: 204, json: {}})
3334
+ return
3335
+ }
3336
+
3337
+ try {
3338
+ const responsePayload = await this.frontendModelCustomCommandPayload()
3339
+
3340
+ await this.render({
3341
+ json: /**
3342
+ * Types the following value.
3343
+ @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(responsePayload))
3344
+ })
3345
+ } catch (error) {
3346
+ await this.frontendModelLogEndpointError({action: "frontendCustomCommand", commandType: "custom-command", error})
3347
+
3348
+ await this.render({
3349
+ json: /**
3350
+ * Types the following value.
3351
+ @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(this.frontendModelClientErrorPayloadForError(error)))
3352
+ })
3353
+ }
3354
+ }
3355
+
3356
+ /**
3357
+ * Runs frontend model custom command payload.
3358
+ * @returns {Promise<Record<string, ?>>} - Response payload.
3359
+ */
3360
+ async frontendModelCustomCommandPayload() {
3361
+ const params = this.frontendModelParams()
3362
+ const methodName = params.frontendModelCustomCommandMethodName
3363
+ const scope = params.frontendModelCustomCommandScope
3364
+
3365
+ if (typeof methodName !== "string" || methodName.length < 1) {
3366
+ return this.frontendModelErrorPayload("Expected frontend-model custom command method name.")
3367
+ }
3368
+
3369
+ if (scope !== "collection" && scope !== "member") {
3370
+ return this.frontendModelErrorPayload("Expected frontend-model custom command scope.")
3371
+ }
3372
+
3373
+ const resource = /**
3374
+ * Types the following value.
3375
+ @type {Record<string, ?>} */ (this.frontendModelResourceInstance())
3376
+ const commandMethod = resource[methodName]
3377
+
3378
+ if (typeof commandMethod !== "function") {
3379
+ return this.frontendModelErrorPayload(`Missing frontend-model custom command '${methodName}'.`)
3380
+ }
3381
+
3382
+ const responsePayload = await commandMethod.call(resource)
3383
+
3384
+ if (!responsePayload || typeof responsePayload !== "object") {
3385
+ return {status: "success"}
3386
+ }
3387
+
3388
+ return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (
3389
+ await this.autoSerializeFrontendModelsInPayload(
3390
+ responsePayload,
3391
+ /**
3392
+ * Types the following value.
3393
+ @type {{serialize: (model: ?, action: string) => Promise<Record<string, ?>>}} */ (resource),
3394
+ methodName
3395
+ )
3396
+ )
3397
+ }
3398
+
3399
+ /**
3400
+ * Walks a custom-command response payload and replaces any backend `Record`
3401
+ * instance with the resource's per-action serialized form so handlers can
3402
+ * return `{record, status: "ok"}` instead of explicitly calling
3403
+ * `await this.serialize(record, action)`. Plain objects, arrays, and
3404
+ * primitive values pass through and are later encoded by
3405
+ * `serializeFrontendModelTransportValue`.
3406
+ * @param {?} value - Payload value.
3407
+ * @param {{serialize: (model: ?, action: string) => Promise<Record<string, ?>>}} resource - Resource instance providing `serialize`.
3408
+ * @param {string} action - Custom command method name passed to `resource.serialize` for per-action authorization filtering.
3409
+ * @param {WeakSet<object>} [seen] - Recursion stack of plain-object containers currently being walked. Membership is added on entry and removed on exit so a container shared between siblings (i.e. referenced twice but not cyclically) is walked on each reference instead of being short-circuited the second time, which would let backend `Record` instances inside it bypass `resource.serialize`.
3410
+ * @returns {Promise<?>} - Payload with backend `Record` instances replaced by serialized markers.
3411
+ */
3412
+ async autoSerializeFrontendModelsInPayload(value, resource, action, seen = new WeakSet()) {
3413
+ if (value === null || value === undefined) {
3414
+ return value
3415
+ }
3416
+
3417
+ if (isBackendModelInstance(value)) {
3418
+ const richSerialized = await resource.serialize(value, action)
3419
+ const modelClass = /**
3420
+ * Types the following value.
3421
+ @type {{constructor: {getModelName?: () => string, name?: string}}} */ (value).constructor
3422
+ const modelName = typeof modelClass.getModelName === "function" ? modelClass.getModelName() : (modelClass.name || "")
3423
+
3424
+ // Wrap the resource-serialized payload in the frontend_model transport
3425
+ // marker. Marker-based decoding routes through `instantiateFromResponse`,
3426
+ // so abilities / queryData / associationCounts / preloadedRelationships
3427
+ // baked into the rich attributes by `resource.serialize` are restored on
3428
+ // the client without callers needing to wrap models manually.
3429
+ return {
3430
+ __velocious_type: "frontend_model",
3431
+ attributes: richSerialized,
3432
+ modelName
3433
+ }
3434
+ }
3435
+
3436
+ if (Array.isArray(value)) {
3437
+ /**
3438
+ * Result.
3439
+ @type {Array<?>} */
3440
+ const result = []
3441
+
3442
+ for (const entry of value) {
3443
+ result.push(await this.autoSerializeFrontendModelsInPayload(entry, resource, action, seen))
3444
+ }
3445
+
3446
+ return result
3447
+ }
3448
+
3449
+ if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
3450
+ const container = /**
3451
+ * Types the following value.
3452
+ @type {Record<string, ?>} */ (value)
3453
+
3454
+ if (seen.has(container)) {
3455
+ // Cyclic back-reference along the current recursion path; the
3456
+ // ancestor frame is still walking this container and will produce
3457
+ // its serialized form. Returning the original container here
3458
+ // breaks the cycle without bypassing the walker for siblings that
3459
+ // share a non-cyclic reference (those re-enter the branch below
3460
+ // because the container is removed from `seen` on stack exit).
3461
+ return container
3462
+ }
3463
+
3464
+ seen.add(container)
3465
+
3466
+ try {
3467
+ /**
3468
+ * Result.
3469
+ @type {Record<string, ?>} */
3470
+ const result = {}
3471
+
3472
+ for (const [key, nested] of Object.entries(container)) {
3473
+ // `assignSafeProperty` stores keys like `__proto__` as own
3474
+ // data properties instead of invoking the prototype setter,
3475
+ // so a custom-command response that echoes parsed client
3476
+ // input cannot pollute `Object.prototype` here. The transport
3477
+ // serializer applies the same protection on its own pass; we
3478
+ // just preserve it across the auto-serialize walk.
3479
+ assignSafeProperty(result, key, await this.autoSerializeFrontendModelsInPayload(nested, resource, action, seen))
3480
+ }
3481
+
3482
+ return result
3483
+ } finally {
3484
+ seen.delete(container)
3485
+ }
3486
+ }
3487
+
3488
+ return value
3489
+ }
3490
+
3491
+ }