velocious 1.0.430 → 1.0.432

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 (831) hide show
  1. package/bin/velocious.js +48 -0
  2. package/build/application.js +229 -0
  3. package/build/authorization/ability.js +329 -0
  4. package/build/authorization/base-resource.js +143 -0
  5. package/build/background-jobs/client.js +50 -0
  6. package/build/background-jobs/cron-expression.js +277 -0
  7. package/build/background-jobs/forked-runner-child.js +86 -0
  8. package/build/background-jobs/job-record.js +13 -0
  9. package/build/background-jobs/job-registry.js +92 -0
  10. package/build/background-jobs/job-runner.js +107 -0
  11. package/build/background-jobs/job.js +77 -0
  12. package/build/background-jobs/json-socket.js +78 -0
  13. package/build/background-jobs/main.js +926 -0
  14. package/build/background-jobs/normalize-error.js +26 -0
  15. package/build/background-jobs/scheduler.js +274 -0
  16. package/build/background-jobs/socket-request.js +68 -0
  17. package/build/background-jobs/status-reporter.js +101 -0
  18. package/build/background-jobs/store.js +994 -0
  19. package/build/background-jobs/types.js +70 -0
  20. package/build/background-jobs/web/authorization.js +89 -0
  21. package/build/background-jobs/web/controller.js +280 -0
  22. package/build/background-jobs/web/index.js +57 -0
  23. package/build/background-jobs/web/path-matcher.js +74 -0
  24. package/build/background-jobs/web/registry.js +49 -0
  25. package/build/background-jobs/worker.js +683 -0
  26. package/build/beacon/client.js +330 -0
  27. package/build/beacon/in-process-broker.js +71 -0
  28. package/build/beacon/in-process-client.js +139 -0
  29. package/build/beacon/server.js +148 -0
  30. package/build/beacon/types.js +55 -0
  31. package/build/bin/velocious.js +39 -34
  32. package/build/cli/base-command.js +67 -0
  33. package/build/cli/browser-cli.js +45 -0
  34. package/build/cli/commands/background-jobs-main.js +7 -0
  35. package/build/cli/commands/background-jobs-runner.js +7 -0
  36. package/build/cli/commands/background-jobs-worker.js +7 -0
  37. package/build/cli/commands/beacon.js +7 -0
  38. package/build/cli/commands/console.js +12 -0
  39. package/build/cli/commands/db/base-command.js +82 -0
  40. package/build/cli/commands/db/create.js +64 -0
  41. package/build/cli/commands/db/drop.js +75 -0
  42. package/build/cli/commands/db/migrate.js +17 -0
  43. package/build/cli/commands/db/reset.js +22 -0
  44. package/build/cli/commands/db/rollback.js +15 -0
  45. package/build/cli/commands/db/schema/dump.js +12 -0
  46. package/build/cli/commands/db/schema/load.js +12 -0
  47. package/build/cli/commands/db/seed.js +12 -0
  48. package/build/cli/commands/db/tenants/check.js +38 -0
  49. package/build/cli/commands/db/tenants/create.js +33 -0
  50. package/build/cli/commands/db/tenants/migrate.js +49 -0
  51. package/build/cli/commands/destroy/migration.js +7 -0
  52. package/build/cli/commands/generate/base-models.js +7 -0
  53. package/build/cli/commands/generate/frontend-models.js +12 -0
  54. package/build/cli/commands/generate/migration.js +7 -0
  55. package/build/cli/commands/generate/model.js +7 -0
  56. package/build/cli/commands/init.js +11 -0
  57. package/build/cli/commands/routes.js +7 -0
  58. package/build/cli/commands/run-script.js +12 -0
  59. package/build/cli/commands/runner.js +12 -0
  60. package/build/cli/commands/server.js +7 -0
  61. package/build/cli/commands/test.js +9 -0
  62. package/build/cli/index.js +152 -0
  63. package/build/cli/tenant-database-command-helper.js +198 -0
  64. package/build/cli/use-browser-cli.js +30 -0
  65. package/build/configuration-resolver.js +65 -0
  66. package/build/configuration-types.js +429 -0
  67. package/build/configuration.js +2590 -0
  68. package/build/controller.js +421 -0
  69. package/build/current-configuration.js +31 -0
  70. package/build/current.js +80 -0
  71. package/build/database/annotations-async-hooks.js +47 -0
  72. package/build/database/annotations.js +40 -0
  73. package/build/database/drivers/base-column.js +182 -0
  74. package/build/database/drivers/base-columns-index.js +81 -0
  75. package/build/database/drivers/base-foreign-key.js +104 -0
  76. package/build/database/drivers/base-table.js +156 -0
  77. package/build/database/drivers/base.js +1609 -0
  78. package/build/database/drivers/mssql/column.js +74 -0
  79. package/build/database/drivers/mssql/columns-index.js +6 -0
  80. package/build/database/drivers/mssql/connect-connection.js +16 -0
  81. package/build/database/drivers/mssql/foreign-key.js +12 -0
  82. package/build/database/drivers/mssql/index.js +590 -0
  83. package/build/database/drivers/mssql/options.js +79 -0
  84. package/build/database/drivers/mssql/query-parser.js +6 -0
  85. package/build/database/drivers/mssql/sql/alter-table.js +4 -0
  86. package/build/database/drivers/mssql/sql/create-database.js +36 -0
  87. package/build/database/drivers/mssql/sql/create-index.js +4 -0
  88. package/build/database/drivers/mssql/sql/create-table.js +4 -0
  89. package/build/database/drivers/mssql/sql/delete.js +19 -0
  90. package/build/database/drivers/mssql/sql/drop-database.js +36 -0
  91. package/build/database/drivers/mssql/sql/drop-table.js +4 -0
  92. package/build/database/drivers/mssql/sql/insert.js +4 -0
  93. package/build/database/drivers/mssql/sql/update.js +31 -0
  94. package/build/database/drivers/mssql/sql/upsert.js +23 -0
  95. package/build/database/drivers/mssql/structure-sql.js +120 -0
  96. package/build/database/drivers/mssql/table.js +145 -0
  97. package/build/database/drivers/mysql/column.js +112 -0
  98. package/build/database/drivers/mysql/columns-index.js +22 -0
  99. package/build/database/drivers/mysql/foreign-key.js +12 -0
  100. package/build/database/drivers/mysql/index.js +473 -0
  101. package/build/database/drivers/mysql/options.js +34 -0
  102. package/build/database/drivers/mysql/query-parser.js +6 -0
  103. package/build/database/drivers/mysql/query.js +37 -0
  104. package/build/database/drivers/mysql/sql/alter-table.js +6 -0
  105. package/build/database/drivers/mysql/sql/create-database.js +39 -0
  106. package/build/database/drivers/mysql/sql/create-index.js +6 -0
  107. package/build/database/drivers/mysql/sql/create-table.js +6 -0
  108. package/build/database/drivers/mysql/sql/delete.js +21 -0
  109. package/build/database/drivers/mysql/sql/drop-database.js +6 -0
  110. package/build/database/drivers/mysql/sql/drop-table.js +6 -0
  111. package/build/database/drivers/mysql/sql/insert.js +6 -0
  112. package/build/database/drivers/mysql/sql/update.js +33 -0
  113. package/build/database/drivers/mysql/sql/upsert.js +13 -0
  114. package/build/database/drivers/mysql/structure-sql.js +93 -0
  115. package/build/database/drivers/mysql/table.js +121 -0
  116. package/build/database/drivers/pgsql/column.js +90 -0
  117. package/build/database/drivers/pgsql/columns-index.js +6 -0
  118. package/build/database/drivers/pgsql/foreign-key.js +12 -0
  119. package/build/database/drivers/pgsql/index.js +441 -0
  120. package/build/database/drivers/pgsql/options.js +32 -0
  121. package/build/database/drivers/pgsql/query-parser.js +6 -0
  122. package/build/database/drivers/pgsql/sql/alter-table.js +6 -0
  123. package/build/database/drivers/pgsql/sql/create-database.js +38 -0
  124. package/build/database/drivers/pgsql/sql/create-index.js +6 -0
  125. package/build/database/drivers/pgsql/sql/create-table.js +6 -0
  126. package/build/database/drivers/pgsql/sql/delete.js +21 -0
  127. package/build/database/drivers/pgsql/sql/drop-database.js +6 -0
  128. package/build/database/drivers/pgsql/sql/drop-table.js +6 -0
  129. package/build/database/drivers/pgsql/sql/insert.js +6 -0
  130. package/build/database/drivers/pgsql/sql/update.js +33 -0
  131. package/build/database/drivers/pgsql/sql/upsert.js +14 -0
  132. package/build/database/drivers/pgsql/structure-sql.js +126 -0
  133. package/build/database/drivers/pgsql/table.js +135 -0
  134. package/build/database/drivers/sqlite/base.js +509 -0
  135. package/build/database/drivers/sqlite/column.js +75 -0
  136. package/build/database/drivers/sqlite/columns-index.js +30 -0
  137. package/build/database/drivers/sqlite/connection-sql-js.js +46 -0
  138. package/build/database/drivers/sqlite/foreign-key.js +24 -0
  139. package/build/database/drivers/sqlite/index.js +394 -0
  140. package/build/database/drivers/sqlite/index.native.js +72 -0
  141. package/build/database/drivers/sqlite/index.web.js +99 -0
  142. package/build/database/drivers/sqlite/options.js +32 -0
  143. package/build/database/drivers/sqlite/query-parser.js +6 -0
  144. package/build/database/drivers/sqlite/query.js +35 -0
  145. package/build/database/drivers/sqlite/query.native.js +35 -0
  146. package/build/database/drivers/sqlite/query.web.js +49 -0
  147. package/build/database/drivers/sqlite/sql/alter-table.js +187 -0
  148. package/build/database/drivers/sqlite/sql/create-index.js +6 -0
  149. package/build/database/drivers/sqlite/sql/create-table.js +6 -0
  150. package/build/database/drivers/sqlite/sql/delete.js +26 -0
  151. package/build/database/drivers/sqlite/sql/drop-table.js +6 -0
  152. package/build/database/drivers/sqlite/sql/insert.js +6 -0
  153. package/build/database/drivers/sqlite/sql/update.js +33 -0
  154. package/build/database/drivers/sqlite/sql/upsert.js +14 -0
  155. package/build/database/drivers/sqlite/structure-sql.js +56 -0
  156. package/build/database/drivers/sqlite/table-rebuilder.js +96 -0
  157. package/build/database/drivers/sqlite/table.js +131 -0
  158. package/build/database/drivers/structure-sql/utils.js +35 -0
  159. package/build/database/handler.js +13 -0
  160. package/build/database/initializer-from-require-context.js +101 -0
  161. package/build/database/migration/index.js +438 -0
  162. package/build/database/migrator/files-finder.js +55 -0
  163. package/build/database/migrator/types.js +31 -0
  164. package/build/database/migrator.js +557 -0
  165. package/build/database/pool/async-tracked-multi-connection.js +1164 -0
  166. package/build/database/pool/base-methods-forward.js +52 -0
  167. package/build/database/pool/base.js +380 -0
  168. package/build/database/pool/single-multi-use.js +118 -0
  169. package/build/database/query/alter-table-base.js +104 -0
  170. package/build/database/query/base.js +49 -0
  171. package/build/database/query/create-database-base.js +42 -0
  172. package/build/database/query/create-index-base.js +117 -0
  173. package/build/database/query/create-table-base.js +205 -0
  174. package/build/database/query/delete-base.js +19 -0
  175. package/build/database/query/drop-database-base.js +38 -0
  176. package/build/database/query/drop-table-base.js +58 -0
  177. package/build/database/query/from-base.js +36 -0
  178. package/build/database/query/from-plain.js +16 -0
  179. package/build/database/query/from-table.js +18 -0
  180. package/build/database/query/index.js +533 -0
  181. package/build/database/query/insert-base.js +172 -0
  182. package/build/database/query/join-base.js +43 -0
  183. package/build/database/query/join-object.js +167 -0
  184. package/build/database/query/join-plain.js +18 -0
  185. package/build/database/query/join-tracker.js +93 -0
  186. package/build/database/query/model-class-query.js +1577 -0
  187. package/build/database/query/order-base.js +33 -0
  188. package/build/database/query/order-column.js +77 -0
  189. package/build/database/query/order-plain.js +28 -0
  190. package/build/database/query/preloader/belongs-to.js +267 -0
  191. package/build/database/query/preloader/ensure-model-class-initialized.js +18 -0
  192. package/build/database/query/preloader/has-many.js +316 -0
  193. package/build/database/query/preloader/has-one.js +123 -0
  194. package/build/database/query/preloader/selection.js +152 -0
  195. package/build/database/query/preloader.js +201 -0
  196. package/build/database/query/query-data.js +305 -0
  197. package/build/database/query/select-base.js +30 -0
  198. package/build/database/query/select-plain.js +18 -0
  199. package/build/database/query/select-table-and-column.js +28 -0
  200. package/build/database/query/update-base.js +41 -0
  201. package/build/database/query/upsert-base.js +103 -0
  202. package/build/database/query/where-base.js +38 -0
  203. package/build/database/query/where-combinator.js +31 -0
  204. package/build/database/query/where-hash.js +77 -0
  205. package/build/database/query/where-model-class-hash.js +505 -0
  206. package/build/database/query/where-not.js +23 -0
  207. package/build/database/query/where-plain.js +20 -0
  208. package/build/database/query/with-count.js +219 -0
  209. package/build/database/query-parser/base-query-parser.js +40 -0
  210. package/build/database/query-parser/from-parser.js +49 -0
  211. package/build/database/query-parser/group-parser.js +55 -0
  212. package/build/database/query-parser/joins-parser.js +37 -0
  213. package/build/database/query-parser/limit-parser.js +77 -0
  214. package/build/database/query-parser/options.js +94 -0
  215. package/build/database/query-parser/order-parser.js +45 -0
  216. package/build/database/query-parser/select-parser.js +67 -0
  217. package/build/database/query-parser/where-parser.js +46 -0
  218. package/build/database/record/acts-as-list.js +374 -0
  219. package/build/database/record/attachments/download.js +49 -0
  220. package/build/database/record/attachments/handle.js +188 -0
  221. package/build/database/record/attachments/normalize-input.js +213 -0
  222. package/build/database/record/attachments/storage-drivers/filesystem.js +114 -0
  223. package/build/database/record/attachments/storage-drivers/native.js +146 -0
  224. package/build/database/record/attachments/storage-drivers/s3.js +245 -0
  225. package/build/database/record/attachments/store.js +591 -0
  226. package/build/database/record/index.js +4094 -0
  227. package/build/database/record/instance-relationships/base.js +289 -0
  228. package/build/database/record/instance-relationships/belongs-to.js +84 -0
  229. package/build/database/record/instance-relationships/has-many.js +284 -0
  230. package/build/database/record/instance-relationships/has-one.js +117 -0
  231. package/build/database/record/record-not-found-error.js +3 -0
  232. package/build/database/record/relationships/base.js +195 -0
  233. package/build/database/record/relationships/belongs-to.js +57 -0
  234. package/build/database/record/relationships/has-many.js +46 -0
  235. package/build/database/record/relationships/has-one.js +46 -0
  236. package/build/database/record/state-machine.js +278 -0
  237. package/build/database/record/user-module.js +43 -0
  238. package/build/database/record/validators/base.js +27 -0
  239. package/build/database/record/validators/format.js +50 -0
  240. package/build/database/record/validators/presence.js +24 -0
  241. package/build/database/record/validators/uniqueness.js +124 -0
  242. package/build/database/table-data/index.js +241 -0
  243. package/build/database/table-data/table-column.js +416 -0
  244. package/build/database/table-data/table-foreign-key.js +69 -0
  245. package/build/database/table-data/table-index.js +46 -0
  246. package/build/database/table-data/table-reference.js +13 -0
  247. package/build/database/use-database.js +48 -0
  248. package/build/environment-handlers/base.js +561 -0
  249. package/build/environment-handlers/browser.js +338 -0
  250. package/build/environment-handlers/node/cli/commands/background-jobs-main.js +21 -0
  251. package/build/environment-handlers/node/cli/commands/background-jobs-runner.js +24 -0
  252. package/build/environment-handlers/node/cli/commands/background-jobs-worker.js +47 -0
  253. package/build/environment-handlers/node/cli/commands/beacon.js +21 -0
  254. package/build/environment-handlers/node/cli/commands/cli-command-context.js +31 -0
  255. package/build/environment-handlers/node/cli/commands/console.js +149 -0
  256. package/build/environment-handlers/node/cli/commands/db/schema/dump.js +43 -0
  257. package/build/environment-handlers/node/cli/commands/db/schema/load.js +69 -0
  258. package/build/environment-handlers/node/cli/commands/db/seed.js +79 -0
  259. package/build/environment-handlers/node/cli/commands/destroy/migration.js +47 -0
  260. package/build/environment-handlers/node/cli/commands/generate/base-models.js +396 -0
  261. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +872 -0
  262. package/build/environment-handlers/node/cli/commands/generate/migration.js +45 -0
  263. package/build/environment-handlers/node/cli/commands/generate/model.js +45 -0
  264. package/build/environment-handlers/node/cli/commands/init.js +68 -0
  265. package/build/environment-handlers/node/cli/commands/routes.js +63 -0
  266. package/build/environment-handlers/node/cli/commands/run-script.js +85 -0
  267. package/build/environment-handlers/node/cli/commands/runner.js +84 -0
  268. package/build/environment-handlers/node/cli/commands/server.js +151 -0
  269. package/build/environment-handlers/node/cli/commands/test.js +118 -0
  270. package/build/environment-handlers/node.js +887 -0
  271. package/build/error-logger.js +30 -0
  272. package/build/frontend-model-controller.js +3491 -0
  273. package/build/frontend-model-resource/base-resource.js +935 -0
  274. package/build/frontend-models/base.js +4004 -0
  275. package/build/frontend-models/clear-pending-debounced-callback.js +16 -0
  276. package/build/frontend-models/event-hook-models.js +49 -0
  277. package/build/frontend-models/model-registry.js +28 -0
  278. package/build/frontend-models/outgoing-event-buffer.js +51 -0
  279. package/build/frontend-models/preloader.js +169 -0
  280. package/build/frontend-models/query.js +2245 -0
  281. package/build/frontend-models/resource-config-validation.js +56 -0
  282. package/build/frontend-models/resource-definition.js +399 -0
  283. package/build/frontend-models/transport-serialization.js +369 -0
  284. package/build/frontend-models/use-created-event.js +21 -0
  285. package/build/frontend-models/use-destroyed-event.js +148 -0
  286. package/build/frontend-models/use-model-class-event.js +164 -0
  287. package/build/frontend-models/use-updated-event.js +152 -0
  288. package/build/frontend-models/websocket-channel.js +494 -0
  289. package/build/frontend-models/websocket-publishers.js +224 -0
  290. package/build/http-client/header.js +17 -0
  291. package/build/http-client/index.js +139 -0
  292. package/build/http-client/request.js +94 -0
  293. package/build/http-client/response.js +151 -0
  294. package/build/http-client/websocket-client.js +27 -0
  295. package/build/http-server/client/index.js +507 -0
  296. package/build/http-server/client/params-to-object.js +152 -0
  297. package/build/http-server/client/request-buffer/form-data-part.js +139 -0
  298. package/build/http-server/client/request-buffer/header.js +19 -0
  299. package/build/http-server/client/request-buffer/index.js +535 -0
  300. package/build/http-server/client/request-parser.js +195 -0
  301. package/build/http-server/client/request-runner.js +321 -0
  302. package/build/http-server/client/request-timing.js +171 -0
  303. package/build/http-server/client/request.js +114 -0
  304. package/build/http-server/client/response.js +251 -0
  305. package/build/http-server/client/uploaded-file/memory-uploaded-file.js +32 -0
  306. package/build/http-server/client/uploaded-file/temporary-uploaded-file.js +32 -0
  307. package/build/http-server/client/uploaded-file/uploaded-file.js +36 -0
  308. package/build/http-server/client/websocket-request.js +147 -0
  309. package/build/http-server/client/websocket-session.js +1755 -0
  310. package/build/http-server/cookie.js +245 -0
  311. package/build/http-server/development-reloader.js +240 -0
  312. package/build/http-server/index.js +561 -0
  313. package/build/http-server/remote-address.js +77 -0
  314. package/build/http-server/server-client.js +222 -0
  315. package/build/http-server/server-lock.js +178 -0
  316. package/build/http-server/websocket-channel-subscribers.js +110 -0
  317. package/build/http-server/websocket-channel.js +137 -0
  318. package/build/http-server/websocket-connection.js +118 -0
  319. package/build/http-server/websocket-event-log-store.js +433 -0
  320. package/build/http-server/websocket-events-host.js +170 -0
  321. package/build/http-server/websocket-events.js +50 -0
  322. package/build/http-server/worker-handler/channel-subscriber-dispatch.js +28 -0
  323. package/build/http-server/worker-handler/in-process.js +155 -0
  324. package/build/http-server/worker-handler/index.js +370 -0
  325. package/build/http-server/worker-handler/worker-script.js +6 -0
  326. package/build/http-server/worker-handler/worker-thread.js +286 -0
  327. package/build/index.js +1 -2
  328. package/build/initializer.js +39 -0
  329. package/build/jobs/mail-delivery.js +22 -0
  330. package/build/logger/base-logger.js +34 -0
  331. package/build/logger/console-logger.js +28 -0
  332. package/build/logger/file-logger.js +36 -0
  333. package/build/logger/outputs/array-output.js +50 -0
  334. package/build/logger/outputs/console-output.js +32 -0
  335. package/build/logger/outputs/file-output.js +55 -0
  336. package/build/logger/outputs/stdout-output.js +64 -0
  337. package/build/logger.js +507 -0
  338. package/build/mailer/backends/smtp.js +197 -0
  339. package/build/mailer/base.js +337 -0
  340. package/build/mailer/delivery.js +70 -0
  341. package/build/mailer/index.js +24 -0
  342. package/build/mailer.js +15 -0
  343. package/build/plugins/sqljs-wasm-route-controller.js +70 -0
  344. package/build/plugins/sqljs-wasm-route.js +71 -0
  345. package/build/record-payload-values.js +83 -0
  346. package/build/routes/app-routes.js +17 -0
  347. package/build/routes/base-route.js +133 -0
  348. package/build/routes/basic-route.js +109 -0
  349. package/build/routes/built-in/debug/controller.js +12 -0
  350. package/build/routes/built-in/errors/controller.js +7 -0
  351. package/build/routes/get-route.js +75 -0
  352. package/build/routes/hooks/frontend-model-command-route-hook.js +100 -0
  353. package/build/routes/index.js +50 -0
  354. package/build/routes/namespace-route.js +51 -0
  355. package/build/routes/plugin-routes.js +141 -0
  356. package/build/routes/post-route.js +74 -0
  357. package/build/routes/resolver.js +535 -0
  358. package/build/routes/resource-route.js +154 -0
  359. package/build/routes/root-route.js +11 -0
  360. package/build/src/authorization/ability.d.ts +24 -23
  361. package/build/src/authorization/ability.d.ts.map +1 -1
  362. package/build/src/authorization/ability.js +14 -13
  363. package/build/src/authorization/base-resource.d.ts +20 -26
  364. package/build/src/authorization/base-resource.d.ts.map +1 -1
  365. package/build/src/authorization/base-resource.js +13 -11
  366. package/build/src/configuration-types.d.ts +21 -2
  367. package/build/src/configuration-types.d.ts.map +1 -1
  368. package/build/src/configuration-types.js +8 -2
  369. package/build/src/database/query/where-combinator.d.ts.map +1 -1
  370. package/build/src/database/query/where-combinator.js +1 -2
  371. package/build/src/database/record/acts-as-list.js +2 -2
  372. package/build/src/database/record/attachments/store.d.ts +1 -1
  373. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  374. package/build/src/database/record/attachments/store.js +2 -2
  375. package/build/src/database/record/index.d.ts +82 -20
  376. package/build/src/database/record/index.d.ts.map +1 -1
  377. package/build/src/database/record/index.js +126 -13
  378. package/build/src/database/record/relationships/base.d.ts +2 -2
  379. package/build/src/database/record/relationships/base.d.ts.map +1 -1
  380. package/build/src/database/record/relationships/base.js +3 -3
  381. package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts +4 -2
  382. package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
  383. package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +59 -36
  384. package/build/src/frontend-model-controller.d.ts +6 -6
  385. package/build/src/frontend-model-controller.d.ts.map +1 -1
  386. package/build/src/frontend-model-controller.js +4 -4
  387. package/build/src/frontend-model-resource/base-resource.d.ts +18 -17
  388. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  389. package/build/src/frontend-model-resource/base-resource.js +22 -10
  390. package/build/src/frontend-models/base.d.ts +19 -12
  391. package/build/src/frontend-models/base.d.ts.map +1 -1
  392. package/build/src/frontend-models/base.js +79 -48
  393. package/build/src/frontend-models/preloader.d.ts +6 -6
  394. package/build/src/frontend-models/preloader.d.ts.map +1 -1
  395. package/build/src/frontend-models/preloader.js +5 -5
  396. package/build/src/frontend-models/query.d.ts.map +1 -1
  397. package/build/src/frontend-models/query.js +1 -4
  398. package/build/src/utils/ransack.d.ts.map +1 -1
  399. package/build/src/utils/ransack.js +1 -2
  400. package/build/templates/configuration.js +61 -0
  401. package/build/templates/generate-migration.js +11 -0
  402. package/build/templates/generate-model.js +6 -0
  403. package/build/templates/routes.js +11 -0
  404. package/build/testing/base-expect.js +17 -0
  405. package/build/testing/browser-frontend-model-event-hook-scenarios.js +520 -0
  406. package/build/testing/browser-test-app.js +32 -0
  407. package/build/testing/expect-to-change.js +55 -0
  408. package/build/testing/expect-utils.js +269 -0
  409. package/build/testing/expect.js +763 -0
  410. package/build/testing/request-client.js +90 -0
  411. package/build/testing/test-files-finder.js +364 -0
  412. package/build/testing/test-filter-parser.js +198 -0
  413. package/build/testing/test-runner.js +1168 -0
  414. package/build/testing/test-suite-splitter.js +177 -0
  415. package/build/testing/test.js +370 -0
  416. package/build/utils/backtrace-cleaner-node.js +87 -0
  417. package/build/utils/backtrace-cleaner.js +266 -0
  418. package/build/utils/ensure-error.js +15 -0
  419. package/build/utils/event-emitter.js +8 -0
  420. package/build/utils/file-exists.js +18 -0
  421. package/build/utils/format-value.js +101 -0
  422. package/build/utils/model-scope.js +56 -0
  423. package/build/utils/nest-callbacks.js +22 -0
  424. package/build/utils/plain-object.js +14 -0
  425. package/build/utils/ransack.js +859 -0
  426. package/build/utils/rest-args-error.js +14 -0
  427. package/build/utils/singularize-model-name.js +18 -0
  428. package/build/utils/split-sql-statements.js +88 -0
  429. package/build/utils/to-import-specifier.js +53 -0
  430. package/build/utils/with-tracked-stack-async-hooks.js +103 -0
  431. package/build/utils/with-tracked-stack.js +38 -0
  432. package/build/velocious-error.js +34 -0
  433. package/index.js +1 -0
  434. package/package.json +10 -4
  435. package/scripts/clean-build.js +8 -0
  436. package/scripts/ensure-bin-executable.js +13 -0
  437. package/scripts/run-tests.js +37 -0
  438. package/scripts/test-browser.js +486 -0
  439. package/src/application.js +229 -0
  440. package/src/authorization/ability.js +329 -0
  441. package/src/authorization/base-resource.js +143 -0
  442. package/src/background-jobs/client.js +50 -0
  443. package/src/background-jobs/cron-expression.js +277 -0
  444. package/src/background-jobs/forked-runner-child.js +86 -0
  445. package/src/background-jobs/job-record.js +13 -0
  446. package/src/background-jobs/job-registry.js +92 -0
  447. package/src/background-jobs/job-runner.js +107 -0
  448. package/src/background-jobs/job.js +77 -0
  449. package/src/background-jobs/json-socket.js +78 -0
  450. package/src/background-jobs/main.js +926 -0
  451. package/src/background-jobs/normalize-error.js +26 -0
  452. package/src/background-jobs/scheduler.js +274 -0
  453. package/src/background-jobs/socket-request.js +68 -0
  454. package/src/background-jobs/status-reporter.js +101 -0
  455. package/src/background-jobs/store.js +994 -0
  456. package/src/background-jobs/types.js +70 -0
  457. package/src/background-jobs/web/authorization.js +89 -0
  458. package/src/background-jobs/web/controller.js +280 -0
  459. package/src/background-jobs/web/index.js +57 -0
  460. package/src/background-jobs/web/path-matcher.js +74 -0
  461. package/src/background-jobs/web/registry.js +49 -0
  462. package/src/background-jobs/worker.js +683 -0
  463. package/src/beacon/client.js +330 -0
  464. package/src/beacon/in-process-broker.js +71 -0
  465. package/src/beacon/in-process-client.js +139 -0
  466. package/src/beacon/server.js +148 -0
  467. package/src/beacon/types.js +55 -0
  468. package/src/cli/base-command.js +67 -0
  469. package/src/cli/browser-cli.js +45 -0
  470. package/src/cli/commands/background-jobs-main.js +7 -0
  471. package/src/cli/commands/background-jobs-runner.js +7 -0
  472. package/src/cli/commands/background-jobs-worker.js +7 -0
  473. package/src/cli/commands/beacon.js +7 -0
  474. package/src/cli/commands/console.js +12 -0
  475. package/src/cli/commands/db/base-command.js +82 -0
  476. package/src/cli/commands/db/create.js +64 -0
  477. package/src/cli/commands/db/drop.js +75 -0
  478. package/src/cli/commands/db/migrate.js +17 -0
  479. package/src/cli/commands/db/reset.js +22 -0
  480. package/src/cli/commands/db/rollback.js +15 -0
  481. package/src/cli/commands/db/schema/dump.js +12 -0
  482. package/src/cli/commands/db/schema/load.js +12 -0
  483. package/src/cli/commands/db/seed.js +12 -0
  484. package/src/cli/commands/db/tenants/check.js +38 -0
  485. package/src/cli/commands/db/tenants/create.js +33 -0
  486. package/src/cli/commands/db/tenants/migrate.js +49 -0
  487. package/src/cli/commands/destroy/migration.js +7 -0
  488. package/src/cli/commands/generate/base-models.js +7 -0
  489. package/src/cli/commands/generate/frontend-models.js +12 -0
  490. package/src/cli/commands/generate/migration.js +7 -0
  491. package/src/cli/commands/generate/model.js +7 -0
  492. package/src/cli/commands/init.js +11 -0
  493. package/src/cli/commands/routes.js +7 -0
  494. package/src/cli/commands/run-script.js +12 -0
  495. package/src/cli/commands/runner.js +12 -0
  496. package/src/cli/commands/server.js +7 -0
  497. package/src/cli/commands/test.js +9 -0
  498. package/src/cli/index.js +152 -0
  499. package/src/cli/tenant-database-command-helper.js +198 -0
  500. package/src/cli/use-browser-cli.js +30 -0
  501. package/src/configuration-resolver.js +65 -0
  502. package/src/configuration-types.js +429 -0
  503. package/src/configuration.js +2590 -0
  504. package/src/controller.js +421 -0
  505. package/src/current-configuration.js +31 -0
  506. package/src/current.js +80 -0
  507. package/src/database/annotations-async-hooks.js +47 -0
  508. package/src/database/annotations.js +40 -0
  509. package/src/database/drivers/base-column.js +182 -0
  510. package/src/database/drivers/base-columns-index.js +81 -0
  511. package/src/database/drivers/base-foreign-key.js +104 -0
  512. package/src/database/drivers/base-table.js +156 -0
  513. package/src/database/drivers/base.js +1609 -0
  514. package/src/database/drivers/mssql/column.js +74 -0
  515. package/src/database/drivers/mssql/columns-index.js +6 -0
  516. package/src/database/drivers/mssql/connect-connection.js +16 -0
  517. package/src/database/drivers/mssql/foreign-key.js +12 -0
  518. package/src/database/drivers/mssql/index.js +590 -0
  519. package/src/database/drivers/mssql/options.js +79 -0
  520. package/src/database/drivers/mssql/query-parser.js +6 -0
  521. package/src/database/drivers/mssql/sql/alter-table.js +4 -0
  522. package/src/database/drivers/mssql/sql/create-database.js +36 -0
  523. package/src/database/drivers/mssql/sql/create-index.js +4 -0
  524. package/src/database/drivers/mssql/sql/create-table.js +4 -0
  525. package/src/database/drivers/mssql/sql/delete.js +19 -0
  526. package/src/database/drivers/mssql/sql/drop-database.js +36 -0
  527. package/src/database/drivers/mssql/sql/drop-table.js +4 -0
  528. package/src/database/drivers/mssql/sql/insert.js +4 -0
  529. package/src/database/drivers/mssql/sql/update.js +31 -0
  530. package/src/database/drivers/mssql/sql/upsert.js +23 -0
  531. package/src/database/drivers/mssql/structure-sql.js +120 -0
  532. package/src/database/drivers/mssql/table.js +145 -0
  533. package/src/database/drivers/mysql/column.js +112 -0
  534. package/src/database/drivers/mysql/columns-index.js +22 -0
  535. package/src/database/drivers/mysql/foreign-key.js +12 -0
  536. package/src/database/drivers/mysql/index.js +473 -0
  537. package/src/database/drivers/mysql/options.js +34 -0
  538. package/src/database/drivers/mysql/query-parser.js +6 -0
  539. package/src/database/drivers/mysql/query.js +37 -0
  540. package/src/database/drivers/mysql/sql/alter-table.js +6 -0
  541. package/src/database/drivers/mysql/sql/create-database.js +39 -0
  542. package/src/database/drivers/mysql/sql/create-index.js +6 -0
  543. package/src/database/drivers/mysql/sql/create-table.js +6 -0
  544. package/src/database/drivers/mysql/sql/delete.js +21 -0
  545. package/src/database/drivers/mysql/sql/drop-database.js +6 -0
  546. package/src/database/drivers/mysql/sql/drop-table.js +6 -0
  547. package/src/database/drivers/mysql/sql/insert.js +6 -0
  548. package/src/database/drivers/mysql/sql/update.js +33 -0
  549. package/src/database/drivers/mysql/sql/upsert.js +13 -0
  550. package/src/database/drivers/mysql/structure-sql.js +93 -0
  551. package/src/database/drivers/mysql/table.js +121 -0
  552. package/src/database/drivers/pgsql/column.js +90 -0
  553. package/src/database/drivers/pgsql/columns-index.js +6 -0
  554. package/src/database/drivers/pgsql/foreign-key.js +12 -0
  555. package/src/database/drivers/pgsql/index.js +441 -0
  556. package/src/database/drivers/pgsql/options.js +32 -0
  557. package/src/database/drivers/pgsql/query-parser.js +6 -0
  558. package/src/database/drivers/pgsql/sql/alter-table.js +6 -0
  559. package/src/database/drivers/pgsql/sql/create-database.js +38 -0
  560. package/src/database/drivers/pgsql/sql/create-index.js +6 -0
  561. package/src/database/drivers/pgsql/sql/create-table.js +6 -0
  562. package/src/database/drivers/pgsql/sql/delete.js +21 -0
  563. package/src/database/drivers/pgsql/sql/drop-database.js +6 -0
  564. package/src/database/drivers/pgsql/sql/drop-table.js +6 -0
  565. package/src/database/drivers/pgsql/sql/insert.js +6 -0
  566. package/src/database/drivers/pgsql/sql/update.js +33 -0
  567. package/src/database/drivers/pgsql/sql/upsert.js +14 -0
  568. package/src/database/drivers/pgsql/structure-sql.js +126 -0
  569. package/src/database/drivers/pgsql/table.js +135 -0
  570. package/src/database/drivers/sqlite/base.js +509 -0
  571. package/src/database/drivers/sqlite/column.js +75 -0
  572. package/src/database/drivers/sqlite/columns-index.js +30 -0
  573. package/src/database/drivers/sqlite/connection-sql-js.js +46 -0
  574. package/src/database/drivers/sqlite/foreign-key.js +24 -0
  575. package/src/database/drivers/sqlite/index.js +394 -0
  576. package/src/database/drivers/sqlite/index.native.js +72 -0
  577. package/src/database/drivers/sqlite/index.web.js +99 -0
  578. package/src/database/drivers/sqlite/options.js +32 -0
  579. package/src/database/drivers/sqlite/query-parser.js +6 -0
  580. package/src/database/drivers/sqlite/query.js +35 -0
  581. package/src/database/drivers/sqlite/query.native.js +35 -0
  582. package/src/database/drivers/sqlite/query.web.js +49 -0
  583. package/src/database/drivers/sqlite/sql/alter-table.js +187 -0
  584. package/src/database/drivers/sqlite/sql/create-index.js +6 -0
  585. package/src/database/drivers/sqlite/sql/create-table.js +6 -0
  586. package/src/database/drivers/sqlite/sql/delete.js +26 -0
  587. package/src/database/drivers/sqlite/sql/drop-table.js +6 -0
  588. package/src/database/drivers/sqlite/sql/insert.js +6 -0
  589. package/src/database/drivers/sqlite/sql/update.js +33 -0
  590. package/src/database/drivers/sqlite/sql/upsert.js +14 -0
  591. package/src/database/drivers/sqlite/structure-sql.js +56 -0
  592. package/src/database/drivers/sqlite/table-rebuilder.js +96 -0
  593. package/src/database/drivers/sqlite/table.js +131 -0
  594. package/src/database/drivers/structure-sql/utils.js +35 -0
  595. package/src/database/handler.js +13 -0
  596. package/src/database/initializer-from-require-context.js +101 -0
  597. package/src/database/migration/index.js +438 -0
  598. package/src/database/migrator/files-finder.js +55 -0
  599. package/src/database/migrator/types.js +31 -0
  600. package/src/database/migrator.js +557 -0
  601. package/src/database/pool/async-tracked-multi-connection.js +1164 -0
  602. package/src/database/pool/base-methods-forward.js +52 -0
  603. package/src/database/pool/base.js +380 -0
  604. package/src/database/pool/single-multi-use.js +118 -0
  605. package/src/database/query/alter-table-base.js +104 -0
  606. package/src/database/query/base.js +49 -0
  607. package/src/database/query/create-database-base.js +42 -0
  608. package/src/database/query/create-index-base.js +117 -0
  609. package/src/database/query/create-table-base.js +205 -0
  610. package/src/database/query/delete-base.js +19 -0
  611. package/src/database/query/drop-database-base.js +38 -0
  612. package/src/database/query/drop-table-base.js +58 -0
  613. package/src/database/query/from-base.js +36 -0
  614. package/src/database/query/from-plain.js +16 -0
  615. package/src/database/query/from-table.js +18 -0
  616. package/src/database/query/index.js +533 -0
  617. package/src/database/query/insert-base.js +172 -0
  618. package/src/database/query/join-base.js +43 -0
  619. package/src/database/query/join-object.js +167 -0
  620. package/src/database/query/join-plain.js +18 -0
  621. package/src/database/query/join-tracker.js +93 -0
  622. package/src/database/query/model-class-query.js +1577 -0
  623. package/src/database/query/order-base.js +33 -0
  624. package/src/database/query/order-column.js +77 -0
  625. package/src/database/query/order-plain.js +28 -0
  626. package/src/database/query/preloader/belongs-to.js +267 -0
  627. package/src/database/query/preloader/ensure-model-class-initialized.js +18 -0
  628. package/src/database/query/preloader/has-many.js +316 -0
  629. package/src/database/query/preloader/has-one.js +123 -0
  630. package/src/database/query/preloader/selection.js +152 -0
  631. package/src/database/query/preloader.js +201 -0
  632. package/src/database/query/query-data.js +305 -0
  633. package/src/database/query/select-base.js +30 -0
  634. package/src/database/query/select-plain.js +18 -0
  635. package/src/database/query/select-table-and-column.js +28 -0
  636. package/src/database/query/update-base.js +41 -0
  637. package/src/database/query/upsert-base.js +103 -0
  638. package/src/database/query/where-base.js +38 -0
  639. package/src/database/query/where-combinator.js +31 -0
  640. package/src/database/query/where-hash.js +77 -0
  641. package/src/database/query/where-model-class-hash.js +505 -0
  642. package/src/database/query/where-not.js +23 -0
  643. package/src/database/query/where-plain.js +20 -0
  644. package/src/database/query/with-count.js +219 -0
  645. package/src/database/query-parser/base-query-parser.js +40 -0
  646. package/src/database/query-parser/from-parser.js +49 -0
  647. package/src/database/query-parser/group-parser.js +55 -0
  648. package/src/database/query-parser/joins-parser.js +37 -0
  649. package/src/database/query-parser/limit-parser.js +77 -0
  650. package/src/database/query-parser/options.js +94 -0
  651. package/src/database/query-parser/order-parser.js +45 -0
  652. package/src/database/query-parser/select-parser.js +67 -0
  653. package/src/database/query-parser/where-parser.js +46 -0
  654. package/src/database/record/acts-as-list.js +374 -0
  655. package/src/database/record/attachments/download.js +49 -0
  656. package/src/database/record/attachments/handle.js +188 -0
  657. package/src/database/record/attachments/normalize-input.js +213 -0
  658. package/src/database/record/attachments/storage-drivers/filesystem.js +114 -0
  659. package/src/database/record/attachments/storage-drivers/native.js +146 -0
  660. package/src/database/record/attachments/storage-drivers/s3.js +245 -0
  661. package/src/database/record/attachments/store.js +591 -0
  662. package/src/database/record/index.js +4094 -0
  663. package/src/database/record/instance-relationships/base.js +289 -0
  664. package/src/database/record/instance-relationships/belongs-to.js +84 -0
  665. package/src/database/record/instance-relationships/has-many.js +284 -0
  666. package/src/database/record/instance-relationships/has-one.js +117 -0
  667. package/src/database/record/record-not-found-error.js +3 -0
  668. package/src/database/record/relationships/base.js +195 -0
  669. package/src/database/record/relationships/belongs-to.js +57 -0
  670. package/src/database/record/relationships/has-many.js +46 -0
  671. package/src/database/record/relationships/has-one.js +46 -0
  672. package/src/database/record/state-machine.js +278 -0
  673. package/src/database/record/user-module.js +43 -0
  674. package/src/database/record/validators/base.js +27 -0
  675. package/src/database/record/validators/format.js +50 -0
  676. package/src/database/record/validators/presence.js +24 -0
  677. package/src/database/record/validators/uniqueness.js +124 -0
  678. package/src/database/table-data/index.js +241 -0
  679. package/src/database/table-data/table-column.js +416 -0
  680. package/src/database/table-data/table-foreign-key.js +69 -0
  681. package/src/database/table-data/table-index.js +46 -0
  682. package/src/database/table-data/table-reference.js +13 -0
  683. package/src/database/use-database.js +48 -0
  684. package/src/environment-handlers/base.js +561 -0
  685. package/src/environment-handlers/browser.js +338 -0
  686. package/src/environment-handlers/node/cli/commands/background-jobs-main.js +21 -0
  687. package/src/environment-handlers/node/cli/commands/background-jobs-runner.js +24 -0
  688. package/src/environment-handlers/node/cli/commands/background-jobs-worker.js +47 -0
  689. package/src/environment-handlers/node/cli/commands/beacon.js +21 -0
  690. package/src/environment-handlers/node/cli/commands/cli-command-context.js +31 -0
  691. package/src/environment-handlers/node/cli/commands/console.js +149 -0
  692. package/src/environment-handlers/node/cli/commands/db/schema/dump.js +43 -0
  693. package/src/environment-handlers/node/cli/commands/db/schema/load.js +69 -0
  694. package/src/environment-handlers/node/cli/commands/db/seed.js +79 -0
  695. package/src/environment-handlers/node/cli/commands/destroy/migration.js +47 -0
  696. package/src/environment-handlers/node/cli/commands/generate/base-models.js +396 -0
  697. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +872 -0
  698. package/src/environment-handlers/node/cli/commands/generate/migration.js +45 -0
  699. package/src/environment-handlers/node/cli/commands/generate/model.js +45 -0
  700. package/src/environment-handlers/node/cli/commands/init.js +68 -0
  701. package/src/environment-handlers/node/cli/commands/routes.js +63 -0
  702. package/src/environment-handlers/node/cli/commands/run-script.js +85 -0
  703. package/src/environment-handlers/node/cli/commands/runner.js +84 -0
  704. package/src/environment-handlers/node/cli/commands/server.js +151 -0
  705. package/src/environment-handlers/node/cli/commands/test.js +118 -0
  706. package/src/environment-handlers/node.js +887 -0
  707. package/src/error-logger.js +30 -0
  708. package/src/frontend-model-controller.js +3491 -0
  709. package/src/frontend-model-resource/base-resource.js +935 -0
  710. package/src/frontend-models/base.js +4004 -0
  711. package/src/frontend-models/clear-pending-debounced-callback.js +16 -0
  712. package/src/frontend-models/event-hook-models.js +49 -0
  713. package/src/frontend-models/model-registry.js +28 -0
  714. package/src/frontend-models/outgoing-event-buffer.js +51 -0
  715. package/src/frontend-models/preloader.js +169 -0
  716. package/src/frontend-models/query.js +2245 -0
  717. package/src/frontend-models/resource-config-validation.js +56 -0
  718. package/src/frontend-models/resource-definition.js +399 -0
  719. package/src/frontend-models/transport-serialization.js +369 -0
  720. package/src/frontend-models/use-created-event.js +21 -0
  721. package/src/frontend-models/use-destroyed-event.js +148 -0
  722. package/src/frontend-models/use-model-class-event.js +164 -0
  723. package/src/frontend-models/use-updated-event.js +152 -0
  724. package/src/frontend-models/websocket-channel.js +494 -0
  725. package/src/frontend-models/websocket-publishers.js +224 -0
  726. package/src/http-client/header.js +17 -0
  727. package/src/http-client/index.js +139 -0
  728. package/src/http-client/request.js +94 -0
  729. package/src/http-client/response.js +151 -0
  730. package/src/http-client/websocket-client.js +27 -0
  731. package/src/http-server/client/index.js +507 -0
  732. package/src/http-server/client/params-to-object.js +152 -0
  733. package/src/http-server/client/request-buffer/form-data-part.js +139 -0
  734. package/src/http-server/client/request-buffer/header.js +19 -0
  735. package/src/http-server/client/request-buffer/index.js +535 -0
  736. package/src/http-server/client/request-parser.js +195 -0
  737. package/src/http-server/client/request-runner.js +321 -0
  738. package/src/http-server/client/request-timing.js +171 -0
  739. package/src/http-server/client/request.js +114 -0
  740. package/src/http-server/client/response.js +251 -0
  741. package/src/http-server/client/uploaded-file/memory-uploaded-file.js +32 -0
  742. package/src/http-server/client/uploaded-file/temporary-uploaded-file.js +32 -0
  743. package/src/http-server/client/uploaded-file/uploaded-file.js +36 -0
  744. package/src/http-server/client/websocket-request.js +147 -0
  745. package/src/http-server/client/websocket-session.js +1755 -0
  746. package/src/http-server/cookie.js +245 -0
  747. package/src/http-server/development-reloader.js +240 -0
  748. package/src/http-server/index.js +561 -0
  749. package/src/http-server/remote-address.js +77 -0
  750. package/src/http-server/server-client.js +222 -0
  751. package/src/http-server/server-lock.js +178 -0
  752. package/src/http-server/websocket-channel-subscribers.js +110 -0
  753. package/src/http-server/websocket-channel.js +137 -0
  754. package/src/http-server/websocket-connection.js +118 -0
  755. package/src/http-server/websocket-event-log-store.js +433 -0
  756. package/src/http-server/websocket-events-host.js +170 -0
  757. package/src/http-server/websocket-events.js +50 -0
  758. package/src/http-server/worker-handler/channel-subscriber-dispatch.js +28 -0
  759. package/src/http-server/worker-handler/in-process.js +155 -0
  760. package/src/http-server/worker-handler/index.js +370 -0
  761. package/src/http-server/worker-handler/worker-script.js +6 -0
  762. package/src/http-server/worker-handler/worker-thread.js +286 -0
  763. package/src/initializer.js +39 -0
  764. package/src/jobs/.gitkeep +1 -0
  765. package/src/jobs/mail-delivery.js +22 -0
  766. package/src/logger/base-logger.js +34 -0
  767. package/src/logger/console-logger.js +28 -0
  768. package/src/logger/file-logger.js +36 -0
  769. package/src/logger/outputs/array-output.js +50 -0
  770. package/src/logger/outputs/console-output.js +32 -0
  771. package/src/logger/outputs/file-output.js +55 -0
  772. package/src/logger/outputs/stdout-output.js +64 -0
  773. package/src/logger.js +507 -0
  774. package/src/mailer/backends/smtp.js +197 -0
  775. package/src/mailer/base.js +337 -0
  776. package/src/mailer/delivery.js +70 -0
  777. package/src/mailer/index.js +24 -0
  778. package/src/mailer.js +15 -0
  779. package/src/plugins/sqljs-wasm-route-controller.js +70 -0
  780. package/src/plugins/sqljs-wasm-route.js +71 -0
  781. package/src/record-payload-values.js +83 -0
  782. package/src/routes/app-routes.js +17 -0
  783. package/src/routes/base-route.js +133 -0
  784. package/src/routes/basic-route.js +109 -0
  785. package/src/routes/built-in/debug/controller.js +12 -0
  786. package/src/routes/built-in/errors/controller.js +7 -0
  787. package/src/routes/built-in/errors/not-found.ejs +1 -0
  788. package/src/routes/get-route.js +75 -0
  789. package/src/routes/hooks/frontend-model-command-route-hook.js +100 -0
  790. package/src/routes/index.js +50 -0
  791. package/src/routes/namespace-route.js +51 -0
  792. package/src/routes/plugin-routes.js +141 -0
  793. package/src/routes/post-route.js +74 -0
  794. package/src/routes/resolver.js +535 -0
  795. package/src/routes/resource-route.js +154 -0
  796. package/src/routes/root-route.js +11 -0
  797. package/src/templates/configuration.js +61 -0
  798. package/src/templates/generate-migration.js +11 -0
  799. package/src/templates/generate-model.js +6 -0
  800. package/src/templates/routes.js +11 -0
  801. package/src/testing/base-expect.js +17 -0
  802. package/src/testing/browser-frontend-model-event-hook-scenarios.js +520 -0
  803. package/src/testing/browser-test-app.js +32 -0
  804. package/src/testing/expect-to-change.js +55 -0
  805. package/src/testing/expect-utils.js +269 -0
  806. package/src/testing/expect.js +763 -0
  807. package/src/testing/request-client.js +90 -0
  808. package/src/testing/test-files-finder.js +364 -0
  809. package/src/testing/test-filter-parser.js +198 -0
  810. package/src/testing/test-runner.js +1168 -0
  811. package/src/testing/test-suite-splitter.js +177 -0
  812. package/src/testing/test.js +370 -0
  813. package/src/types/external-modules.d.ts +57 -0
  814. package/src/utils/backtrace-cleaner-node.js +87 -0
  815. package/src/utils/backtrace-cleaner.js +266 -0
  816. package/src/utils/ensure-error.js +15 -0
  817. package/src/utils/event-emitter.js +8 -0
  818. package/src/utils/file-exists.js +18 -0
  819. package/src/utils/format-value.js +101 -0
  820. package/src/utils/model-scope.js +56 -0
  821. package/src/utils/nest-callbacks.js +22 -0
  822. package/src/utils/plain-object.js +14 -0
  823. package/src/utils/ransack.js +859 -0
  824. package/src/utils/rest-args-error.js +14 -0
  825. package/src/utils/singularize-model-name.js +18 -0
  826. package/src/utils/split-sql-statements.js +88 -0
  827. package/src/utils/to-import-specifier.js +53 -0
  828. package/src/utils/with-tracked-stack-async-hooks.js +103 -0
  829. package/src/utils/with-tracked-stack.js +38 -0
  830. package/src/velocious-error.js +34 -0
  831. package/tsconfig.json +16 -0
@@ -0,0 +1,4004 @@
1
+ // @ts-check
2
+
3
+ import * as inflection from "inflection"
4
+ import timeout from "awaitery/build/timeout.js"
5
+ import wait from "awaitery/build/wait.js"
6
+ import FrontendModelQuery, {frontendModelEventOptionsPayload} from "./query.js"
7
+ import FrontendModelPreloader from "./preloader.js"
8
+ import {registerFrontendModel, resolveFrontendModelClass} from "./model-registry.js"
9
+ import {validateFrontendModelResourceCommandName, validateFrontendModelResourcePath} from "./resource-config-validation.js"
10
+ import {deserializeFrontendModelTransportValue, serializeFrontendModelTransportValue} from "./transport-serialization.js"
11
+ import VelociousWebsocketClient from "../http-client/websocket-client.js"
12
+ import {bufferOutgoingEvent, drainBufferedOutgoingEvents} from "./outgoing-event-buffer.js"
13
+ import {defineModelScope} from "../utils/model-scope.js"
14
+ import isPlainObject from "../utils/plain-object.js"
15
+ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQueryData, setPayloadAssociationCount, setPayloadComputedAbility, setPayloadQueryData} from "../record-payload-values.js"
16
+
17
+ /**
18
+ * FrontendModelCommandType type.
19
+ @typedef {"create" | "find" | "index" | "update" | "destroy" | "attach" | "download" | "url"} FrontendModelCommandType */
20
+ /**
21
+ * FrontendModelRequestCommandType type.
22
+ @typedef {FrontendModelCommandType | string} FrontendModelRequestCommandType */
23
+ /**
24
+ * Defines this typedef.
25
+ * @typedef {{type: "hasOne" | "hasMany"}} FrontendModelAttachmentDefinition
26
+ */
27
+ /**
28
+ * Defines this typedef.
29
+ * @typedef {{attributes?: string[], builtInCollectionCommands?: string[], builtInMemberCommands?: string[], collectionCommands?: string[], commands?: string[], memberCommands?: string[], attachments?: Record<string, FrontendModelAttachmentDefinition>, modelName?: string, nestedAttributes?: Record<string, {allowDestroy?: boolean, limit?: number}>, primaryKey?: string, relationships?: string[]}} FrontendModelResourceConfig
30
+ */
31
+ /**
32
+ * FrontendModelTransportConfig type.
33
+ * @typedef {object} FrontendModelTransportConfig
34
+ * @property {string | (() => string | undefined | null)} [url] - Optional frontend-model URL. This should be the shared endpoint (for example `"/frontend-models"` or `"https://example.com/frontend-models"`).
35
+ * @property {boolean} [shared] - Deprecated shared-endpoint flag retained for compatibility. Frontend-model CRUD/custom commands use the shared frontend-model API envelope by default.
36
+ * @property {string | (() => string | undefined | null)} [websocketUrl] - Optional websocket URL. When set, Velocious creates and manages its own websocket client internally. Subscriptions use the websocket; CRUD uses HTTP and falls back gracefully. Example: `"ws://localhost:3006/websocket"`.
37
+ * @property {{post: (path: string, body?: ?, options?: {headers?: Record<string, string>}) => Promise<{json: () => ?}>, subscribe: (channel: string, options: {params?: Record<string, ?>}, callback: (payload: ?) => void) => (() => void), subscribeAndWait?: (channel: string, options: {params?: Record<string, ?>}, callback: (payload: ?) => void) => Promise<(() => void)>}} [websocketClient] - Optional websocket client for shared frontend-model API requests and subscriptions.
38
+ * @property {Record<string, string> | (() => Record<string, string>)} [requestHeaders] - Extra HTTP/WS headers to attach to every frontend-model API request. Pass a function to compute them at request time (for example to include the current locale).
39
+ * @property {{get: () => string | null | undefined | Promise<string | null | undefined>, set: (sessionId: string) => void | Promise<void>, clear: () => void | Promise<void>}} [sessionStore] - Optional sessionId persistence hook forwarded to the internal `VelociousWebsocketClient` so WS sessions can be resumed across page reloads / app restarts.
40
+ */
41
+ /**
42
+ * FrontendModelIdleWaitArgs type.
43
+ * @typedef {object} FrontendModelIdleWaitArgs
44
+ * @property {number} [quietMs] - Milliseconds the transport must stay idle before resolving.
45
+ * @property {number} [timeout] - Timeout in milliseconds.
46
+ */
47
+
48
+ /**
49
+ * Frontend model transport config.
50
+ @type {FrontendModelTransportConfig} */
51
+ const frontendModelTransportConfig = {}
52
+ const SHARED_FRONTEND_MODEL_API_PATH = "/frontend-models"
53
+ const PRELOADED_RELATIONSHIPS_KEY = "__preloadedRelationships"
54
+ const SELECTED_ATTRIBUTES_KEY = "__selectedAttributes"
55
+ const ASSOCIATION_COUNTS_KEY = "__associationCounts"
56
+ const QUERY_DATA_KEY = "__queryData"
57
+ const ABILITIES_KEY = "__abilities"
58
+ /**
59
+ * Pending shared frontend model requests.
60
+ @type {Array<{commandName?: string, commandType: FrontendModelRequestCommandType, customPath?: string, modelClass: typeof FrontendModelBase, payload: Record<string, ?>, requestId: string, resolve: (response: Record<string, ?>) => void, reject: (error: ?) => void, resourcePath?: string | null}>} */
61
+ let pendingSharedFrontendModelRequests = []
62
+ let sharedFrontendModelRequestId = 0
63
+ let sharedFrontendModelFlushScheduled = false
64
+ let activeFrontendModelTransportRequestCount = 0
65
+ /**
66
+ * Frontend model idle resolvers.
67
+ @type {Array<() => void>} */
68
+ let frontendModelIdleResolvers = []
69
+
70
+ /**
71
+ * Internal websocket client.
72
+ @type {VelociousWebsocketClient | null} */
73
+ let internalWebsocketClient = null
74
+
75
+ /**
76
+ * Runs frontend model transport is idle.
77
+ * @returns {boolean} - Whether all queued and active frontend-model transport requests are done.
78
+ */
79
+ function frontendModelTransportIsIdle() {
80
+ return activeFrontendModelTransportRequestCount === 0
81
+ && pendingSharedFrontendModelRequests.length === 0
82
+ && !sharedFrontendModelFlushScheduled
83
+ }
84
+
85
+ /**
86
+ * Runs resolve frontend model idle waiters.
87
+ @returns {void} */
88
+ function resolveFrontendModelIdleWaiters() {
89
+ if (!frontendModelTransportIsIdle()) return
90
+
91
+ const resolvers = frontendModelIdleResolvers
92
+ frontendModelIdleResolvers = []
93
+
94
+ for (const resolve of resolvers) {
95
+ resolve()
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Runs wait for frontend model transport quiet period.
101
+ * @param {number} milliseconds - Quiet period length.
102
+ * @returns {Promise<void>} Resolves after the quiet period.
103
+ */
104
+ async function waitForFrontendModelTransportQuietPeriod(milliseconds) {
105
+ if (milliseconds <= 0) return
106
+
107
+ await wait(milliseconds)
108
+ }
109
+
110
+ /**
111
+ * Runs wait for frontend model transport idle.
112
+ * @param {number} quietMs - Milliseconds the transport must stay idle before resolving.
113
+ * @returns {Promise<void>} Resolves when transport stays idle.
114
+ */
115
+ async function waitForFrontendModelTransportIdle(quietMs = 0) {
116
+ while (true) {
117
+ if (frontendModelTransportIsIdle()) {
118
+ await new Promise((resolve) => queueMicrotask(() => resolve(undefined)))
119
+
120
+ if (frontendModelTransportIsIdle()) {
121
+ await waitForFrontendModelTransportQuietPeriod(quietMs)
122
+
123
+ if (frontendModelTransportIsIdle()) return
124
+ }
125
+ } else {
126
+ await new Promise((resolve) => {
127
+ frontendModelIdleResolvers.push(() => resolve(undefined))
128
+ })
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Runs track frontend model transport request.
135
+ * @template T
136
+ * @param {() => Promise<T>} callback - Transport callback.
137
+ * @returns {Promise<T>} - Callback result.
138
+ */
139
+ async function trackFrontendModelTransportRequest(callback) {
140
+ activeFrontendModelTransportRequestCount += 1
141
+
142
+ try {
143
+ return await callback()
144
+ } finally {
145
+ activeFrontendModelTransportRequestCount -= 1
146
+ resolveFrontendModelIdleWaiters()
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Resolve the internal websocket client from websocketUrl config.
152
+ * Creates the client lazily on first call. Returns null if WebSocket
153
+ * is not available or websocketUrl is not configured.
154
+ * @returns {VelociousWebsocketClient | null} Websocket client or null.
155
+ */
156
+ function resolveInternalWebsocketClient() {
157
+ if (internalWebsocketClient) return internalWebsocketClient
158
+
159
+ const websocketUrl = frontendModelTransportConfig.websocketUrl
160
+
161
+ if (!websocketUrl) return null
162
+ if (typeof globalThis.WebSocket === "undefined") return null
163
+
164
+ const resolvedUrl = typeof websocketUrl === "function" ? websocketUrl() : websocketUrl
165
+
166
+ if (!resolvedUrl) return null
167
+
168
+ internalWebsocketClient = new VelociousWebsocketClient({
169
+ autoReconnect: true,
170
+ sessionStore: frontendModelTransportConfig.sessionStore,
171
+ url: resolvedUrl
172
+ })
173
+ internalWebsocketClient.onReconnect = flushBufferedOutgoingEventsAfterReconnect
174
+
175
+ return internalWebsocketClient
176
+ }
177
+
178
+ /**
179
+ * Runs flush buffered outgoing events after reconnect.
180
+ @returns {Promise<void>} */
181
+ async function flushBufferedOutgoingEventsAfterReconnect() {
182
+ if (!internalWebsocketClient) return
183
+
184
+ const events = drainBufferedOutgoingEvents()
185
+
186
+ for (let index = 0; index < events.length; index += 1) {
187
+ try {
188
+ await internalWebsocketClient.post(events[index].customPath, events[index].payload)
189
+ } catch {
190
+ const socketOpen = internalWebsocketClient.socket?.readyState === internalWebsocketClient.socket?.OPEN
191
+
192
+ if (socketOpen) {
193
+ continue
194
+ }
195
+
196
+ for (let remaining = index; remaining < events.length; remaining += 1) {
197
+ bufferOutgoingEvent(events[remaining])
198
+ }
199
+
200
+ return
201
+ }
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Runs default frontend model resource path.
207
+ * @param {typeof FrontendModelBase} modelClass - Frontend model class.
208
+ * @returns {string} - Default resource path for the model class.
209
+ */
210
+ function defaultFrontendModelResourcePath(modelClass) {
211
+ return `/${inflection.dasherize(inflection.pluralize(inflection.underscore(modelClass.getModelName())))}`
212
+ }
213
+
214
+ /** Error raised when reading an attribute that was not selected in query payloads. */
215
+ export class AttributeNotSelectedError extends Error {
216
+ /**
217
+ * Runs constructor.
218
+ * @param {string} modelName - Model class name.
219
+ * @param {string} attributeName - Attribute that was requested.
220
+ */
221
+ constructor(modelName, attributeName) {
222
+ super(`${modelName}#${attributeName} was not selected`)
223
+ this.name = "AttributeNotSelectedError"
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Lightweight singular relationship state holder for frontend model instances.
229
+ * @template {typeof FrontendModelBase} S
230
+ * @template {typeof FrontendModelBase} T
231
+ */
232
+ export class FrontendModelSingularRelationship {
233
+ /**
234
+ * Runs constructor.
235
+ * @param {InstanceType<S>} model - Parent model.
236
+ * @param {string} relationshipName - Relationship name.
237
+ * @param {T | null} targetModelClass - Target model class.
238
+ */
239
+ constructor(model, relationshipName, targetModelClass) {
240
+ this.model = model
241
+ this.relationshipName = relationshipName
242
+ this.targetModelClass = targetModelClass
243
+ this._preloaded = false
244
+ this._loadedValue = null
245
+ }
246
+
247
+ /**
248
+ * Runs set loaded.
249
+ * @param {?} loadedValue - Loaded relationship value.
250
+ * @returns {void}
251
+ */
252
+ setLoaded(loadedValue) {
253
+ this._loadedValue = loadedValue == undefined ? null : loadedValue
254
+ this._preloaded = true
255
+ }
256
+
257
+ /**
258
+ * Runs get preloaded.
259
+ * @returns {boolean} - Whether relationship is preloaded.
260
+ */
261
+ getPreloaded() {
262
+ return this._preloaded
263
+ }
264
+
265
+ /**
266
+ * Runs loaded.
267
+ * @returns {?} - Loaded relationship value.
268
+ */
269
+ loaded() {
270
+ if (!this._preloaded) {
271
+ throw new Error(`${this.model.constructor.name}#${this.relationshipName} hasn't been preloaded`)
272
+ }
273
+
274
+ return this._loadedValue
275
+ }
276
+
277
+ /**
278
+ * Runs build.
279
+ * @param {Record<string, ?>} [attributes] - New model attributes.
280
+ * @returns {InstanceType<T>} - Built model.
281
+ */
282
+ build(attributes = {}) {
283
+ if (!this.targetModelClass) {
284
+ throw new Error(`No target model class configured for ${this.model.constructor.name}#${this.relationshipName}`)
285
+ }
286
+
287
+ const model = /**
288
+ * Narrows the runtime value to the documented type.
289
+ @type {InstanceType<T>} */ (new this.targetModelClass(attributes))
290
+
291
+ this.setLoaded(model)
292
+
293
+ return model
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Lightweight has-many relationship state holder for frontend model instances.
299
+ * @template {typeof FrontendModelBase} S
300
+ * @template {typeof FrontendModelBase} T
301
+ */
302
+ export class FrontendModelHasManyRelationship {
303
+ /**
304
+ * Narrows the runtime value to the documented type.
305
+ @type {Array<InstanceType<T>>} */
306
+ _loadedValue
307
+
308
+ /**
309
+ * Runs constructor.
310
+ * @param {InstanceType<S>} model - Parent model.
311
+ * @param {string} relationshipName - Relationship name.
312
+ * @param {T | null} targetModelClass - Target model class.
313
+ */
314
+ constructor(model, relationshipName, targetModelClass) {
315
+ this.model = model
316
+ this.relationshipName = relationshipName
317
+ this.targetModelClass = targetModelClass
318
+ this._preloaded = false
319
+ this._loadedValue = []
320
+ }
321
+
322
+ /**
323
+ * Runs set loaded.
324
+ * @param {Array<InstanceType<T>>} loadedValue - Loaded relationship value.
325
+ * @returns {void}
326
+ */
327
+ setLoaded(loadedValue) {
328
+ this._loadedValue = Array.isArray(loadedValue) ? loadedValue : []
329
+ this._preloaded = true
330
+ }
331
+
332
+ /**
333
+ * Runs get preloaded.
334
+ * @returns {boolean} - Whether relationship is preloaded.
335
+ */
336
+ getPreloaded() {
337
+ return this._preloaded
338
+ }
339
+
340
+ /**
341
+ * Runs loaded.
342
+ * @returns {Array<InstanceType<T>>} - Loaded relationship values.
343
+ */
344
+ loaded() {
345
+ if (!this._preloaded) {
346
+ throw new Error(`${this.model.constructor.name}#${this.relationshipName} hasn't been preloaded`)
347
+ }
348
+
349
+ return this._loadedValue
350
+ }
351
+
352
+ /**
353
+ * Runs add to loaded.
354
+ * @param {Array<InstanceType<T>>} models - Models to append.
355
+ * @returns {void}
356
+ */
357
+ addToLoaded(models) {
358
+ const loadedModels = this.getPreloaded() ? this.loaded() : []
359
+
360
+ this.setLoaded([...loadedModels, ...models])
361
+ }
362
+
363
+ /**
364
+ * Runs build.
365
+ * @param {Record<string, ?>} [attributes] - New model attributes.
366
+ * @returns {InstanceType<T>} - Built model.
367
+ */
368
+ build(attributes = {}) {
369
+ if (!this.targetModelClass) {
370
+ throw new Error(`No target model class configured for ${this.model.constructor.name}#${this.relationshipName}`)
371
+ }
372
+
373
+ const model = /**
374
+ * Narrows the runtime value to the documented type.
375
+ @type {InstanceType<T>} */ (new this.targetModelClass(attributes))
376
+
377
+ this.addToLoaded([model])
378
+
379
+ return model
380
+ }
381
+
382
+ /**
383
+ * Force-reload the relationship. When the parent record was loaded as part
384
+ * of a batch, siblings that have not preloaded this relationship get
385
+ * batched into one request via the cohort preloader. The scoped query path
386
+ * (`Model.where(...).preload([name]).toArray()` directly from user code)
387
+ * bypasses cohort batching by design.
388
+ * @returns {Promise<Array<InstanceType<T>>>} - Loaded relationship models.
389
+ */
390
+ async load() {
391
+ // Reset so the cohort preloader (or single-record fallback) repopulates.
392
+ this._preloaded = false
393
+ this._loadedValue = []
394
+
395
+ const batched = await /**
396
+ * Narrows the runtime value to the documented type.
397
+ @type {?} */ (this.model)._tryCohortPreload(this.relationshipName)
398
+
399
+ if (batched) return this._loadedValue
400
+
401
+ return /** Narrows the runtime value to the documented type. @type {Promise<Array<InstanceType<T>>>} */ (this.model.loadRelationship(this.relationshipName))
402
+ }
403
+
404
+ /**
405
+ * Runs to array.
406
+ * @returns {Promise<Array<InstanceType<T>>>} - Loaded relationship models.
407
+ */
408
+ async toArray() {
409
+ if (this.getPreloaded() || this._loadedValue.length > 0) {
410
+ return this._loadedValue
411
+ }
412
+
413
+ return await this.load()
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Runs relationship type is collection.
419
+ * @param {string} relationshipType - Relationship type.
420
+ * @returns {boolean} - Whether relationship type is has-many.
421
+ */
422
+ function relationshipTypeIsCollection(relationshipType) {
423
+ return relationshipType == "hasMany"
424
+ }
425
+
426
+ /**
427
+ * Downloaded frontend-model attachment payload wrapper.
428
+ */
429
+ export class FrontendModelAttachmentDownload {
430
+ /**
431
+ * Runs constructor.
432
+ * @param {object} args - Options.
433
+ * @param {string} args.id - Attachment id.
434
+ * @param {string} args.filename - Filename.
435
+ * @param {string | null} args.contentType - Content type.
436
+ * @param {number} args.byteSize - File size in bytes.
437
+ * @param {Uint8Array} args.content - File content bytes.
438
+ * @param {string | null} [args.url] - Resolvable attachment URL.
439
+ */
440
+ constructor({byteSize, content, contentType, filename, id, url = null}) {
441
+ this.idValue = id
442
+ this.filenameValue = filename
443
+ this.contentTypeValue = contentType
444
+ this.byteSizeValue = byteSize
445
+ this.contentValue = content
446
+ this.urlValue = url
447
+ }
448
+
449
+ /**
450
+ * Runs byte size.
451
+ * @returns {number} - File size in bytes.
452
+ */
453
+ byteSize() { return this.byteSizeValue }
454
+ /**
455
+ * Runs content.
456
+ * @returns {Uint8Array} - File content bytes.
457
+ */
458
+ content() { return this.contentValue }
459
+ /**
460
+ * Runs content type.
461
+ * @returns {string | null} - Content type.
462
+ */
463
+ contentType() { return this.contentTypeValue }
464
+ /**
465
+ * Runs filename.
466
+ * @returns {string} - Filename.
467
+ */
468
+ filename() { return this.filenameValue }
469
+ /**
470
+ * Runs id.
471
+ * @returns {string} - Attachment id.
472
+ */
473
+ id() { return this.idValue }
474
+ /**
475
+ * Runs url.
476
+ * @returns {string | null} - Resolvable attachment URL.
477
+ */
478
+ url() { return this.urlValue }
479
+ }
480
+
481
+ /**
482
+ * Runs frontend model attachment command payload.
483
+ * @param {FrontendModelAttachmentHandle} attachment - Attachment wrapper.
484
+ * @param {string | undefined} attachmentId - Optional has-many attachment id.
485
+ * @returns {Record<string, ?>} - Command payload.
486
+ */
487
+ function frontendModelAttachmentCommandPayload(attachment, attachmentId) {
488
+ /**
489
+ * Payload.
490
+ @type {Record<string, ?>} */
491
+ const payload = {
492
+ attachmentName: attachment.attachmentName,
493
+ id: attachment.model.primaryKeyValue()
494
+ }
495
+
496
+ if (attachmentId) payload.attachmentId = attachmentId
497
+
498
+ return payload
499
+ }
500
+
501
+ /**
502
+ * Runs frontend attachment value is bytes.
503
+ * @param {?} value - Candidate value.
504
+ * @returns {boolean} - Whether value looks like byte data.
505
+ */
506
+ function frontendAttachmentValueIsBytes(value) {
507
+ return value instanceof Uint8Array || value instanceof ArrayBuffer || (typeof Buffer !== "undefined" && Buffer.isBuffer(value))
508
+ }
509
+
510
+ /**
511
+ * Runs frontend attachment value supports array buffer.
512
+ * @param {?} value - Candidate value.
513
+ * @returns {value is {arrayBuffer: () => Promise<ArrayBuffer>}} - Whether candidate supports arrayBuffer().
514
+ */
515
+ function frontendAttachmentValueSupportsArrayBuffer(value) {
516
+ return Boolean(value && typeof value === "object" && typeof /**
517
+ * Narrows the runtime value to the documented type.
518
+ @type {?} */ (value).arrayBuffer === "function")
519
+ }
520
+
521
+ /**
522
+ * Runs frontend attachment normalize bytes.
523
+ * @param {Uint8Array | Buffer | ArrayBuffer} value - Byte-like value.
524
+ * @returns {Uint8Array} - Uint8Array bytes.
525
+ */
526
+ function frontendAttachmentNormalizeBytes(value) {
527
+ if (value instanceof Uint8Array) return value
528
+ if (value instanceof ArrayBuffer) return new Uint8Array(value)
529
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(/**
530
+ * Narrows the runtime value to the documented type.
531
+ @type {?} */ (value))) {
532
+ return new Uint8Array(/**
533
+ * Narrows the runtime value to the documented type.
534
+ @type {Buffer} */ (value))
535
+ }
536
+
537
+ throw new Error("Unsupported attachment bytes value")
538
+ }
539
+
540
+ /**
541
+ * Runs frontend attachment bytes to base64.
542
+ * @param {Uint8Array} bytes - Bytes.
543
+ * @returns {string} - Base64 value.
544
+ */
545
+ function frontendAttachmentBytesToBase64(bytes) {
546
+ if (typeof Buffer !== "undefined") {
547
+ return Buffer.from(bytes).toString("base64")
548
+ }
549
+
550
+ let binary = ""
551
+
552
+ for (const byte of bytes) {
553
+ binary += String.fromCharCode(byte)
554
+ }
555
+
556
+ if (typeof btoa !== "function") throw new Error("Missing base64 encoder")
557
+
558
+ return btoa(binary)
559
+ }
560
+
561
+ /**
562
+ * Runs frontend attachment base64 to bytes.
563
+ * @param {string} value - Base64 value.
564
+ * @returns {Uint8Array} - Decoded bytes.
565
+ */
566
+ function frontendAttachmentBase64ToBytes(value) {
567
+ if (typeof Buffer !== "undefined") {
568
+ return new Uint8Array(Buffer.from(value, "base64"))
569
+ }
570
+
571
+ if (typeof atob !== "function") throw new Error("Missing base64 decoder")
572
+
573
+ const binary = atob(value)
574
+ const bytes = new Uint8Array(binary.length)
575
+
576
+ for (let index = 0; index < binary.length; index += 1) {
577
+ bytes[index] = binary.charCodeAt(index)
578
+ }
579
+
580
+ return bytes
581
+ }
582
+
583
+ /**
584
+ * Runs frontend attachment value is plain object.
585
+ * @param {?} value - Candidate value.
586
+ * @returns {value is Record<string, ?>} - Whether value is plain object.
587
+ */
588
+ function frontendAttachmentValueIsPlainObject(value) {
589
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false
590
+
591
+ const prototype = Object.getPrototypeOf(value)
592
+
593
+ return prototype === Object.prototype || prototype === null
594
+ }
595
+
596
+ /**
597
+ * Runs frontend model payload contains attachment upload.
598
+ * @param {?} value - Payload candidate.
599
+ * @returns {boolean} - Whether payload contains an attachment upload body.
600
+ */
601
+ function frontendModelPayloadContainsAttachmentUpload(value) {
602
+ if (!value || typeof value !== "object") return false
603
+
604
+ if (Array.isArray(value)) {
605
+ return value.some((entry) => frontendModelPayloadContainsAttachmentUpload(entry))
606
+ }
607
+
608
+ if (!frontendAttachmentValueIsPlainObject(value)) return false
609
+
610
+ if (typeof value.contentBase64 === "string") {
611
+ return true
612
+ }
613
+
614
+ return Object.values(value).some((entry) => frontendModelPayloadContainsAttachmentUpload(entry))
615
+ }
616
+
617
+ /**
618
+ * Runs normalize frontend attachment input.
619
+ * @param {?} input - Attachment input.
620
+ * @returns {Promise<Record<string, ?>>} - Transport-safe attachment payload.
621
+ */
622
+ async function normalizeFrontendAttachmentInput(input) {
623
+ if (frontendAttachmentValueIsPlainObject(input) && "file" in input) {
624
+ const normalizedFile = await normalizeFrontendAttachmentInput(input.file)
625
+ const merged = {
626
+ ...normalizedFile
627
+ }
628
+
629
+ if (typeof input.filename === "string" && input.filename.length > 0) merged.filename = input.filename
630
+ if (typeof input.contentType === "string" && input.contentType.length > 0) merged.contentType = input.contentType
631
+
632
+ return merged
633
+ }
634
+
635
+ if (frontendAttachmentValueIsPlainObject(input)) {
636
+ if (typeof input.path === "string" && input.path.length > 0) {
637
+ throw new Error("Attachment path input is not supported in frontend models")
638
+ }
639
+
640
+ if (typeof input.contentBase64 === "string") {
641
+ return {
642
+ contentBase64: input.contentBase64,
643
+ contentType: typeof input.contentType === "string" && input.contentType.length > 0 ? input.contentType : null,
644
+ filename: typeof input.filename === "string" && input.filename.length > 0 ? input.filename : undefined
645
+ }
646
+ }
647
+ }
648
+
649
+ if (frontendAttachmentValueSupportsArrayBuffer(input)) {
650
+ const bytes = new Uint8Array(await input.arrayBuffer())
651
+
652
+ return {
653
+ contentBase64: frontendAttachmentBytesToBase64(bytes),
654
+ contentType: typeof /**
655
+ * Narrows the runtime value to the documented type.
656
+ @type {?} */ (input).type === "string" && /**
657
+ * Narrows the runtime value to the documented type.
658
+ @type {?} */ (input).type.length > 0
659
+ ? /**
660
+ * Narrows the runtime value to the documented type.
661
+ @type {?} */ (input).type
662
+ : null,
663
+ filename: typeof /**
664
+ * Narrows the runtime value to the documented type.
665
+ @type {?} */ (input).name === "string" && /**
666
+ * Narrows the runtime value to the documented type.
667
+ @type {?} */ (input).name.length > 0
668
+ ? /**
669
+ * Narrows the runtime value to the documented type.
670
+ @type {?} */ (input).name
671
+ : "attachment.bin"
672
+ }
673
+ }
674
+
675
+ if (frontendAttachmentValueIsBytes(input)) {
676
+ const bytes = frontendAttachmentNormalizeBytes(/**
677
+ * Narrows the runtime value to the documented type.
678
+ @type {Uint8Array | Buffer | ArrayBuffer} */ (input))
679
+
680
+ return {
681
+ contentBase64: frontendAttachmentBytesToBase64(bytes),
682
+ contentType: null,
683
+ filename: "attachment.bin"
684
+ }
685
+ }
686
+
687
+ throw new Error("Unsupported frontend attachment input")
688
+ }
689
+
690
+ /**
691
+ * Frontend-model attachment helper for one attachment name.
692
+ */
693
+ export class FrontendModelAttachmentHandle {
694
+ /**
695
+ * Runs constructor.
696
+ * @param {object} args - Options.
697
+ * @param {FrontendModelBase} args.model - Model instance.
698
+ * @param {string} args.attachmentName - Attachment name.
699
+ */
700
+ constructor({attachmentName, model}) {
701
+ this.model = model
702
+ this.attachmentName = attachmentName
703
+ }
704
+
705
+ /**
706
+ * Runs attach.
707
+ * @param {?} input - Attachment input.
708
+ * @returns {Promise<void>} - Resolves when attached.
709
+ */
710
+ async attach(input) {
711
+ const ModelClass = /**
712
+ * Narrows the runtime value to the documented type.
713
+ @type {typeof FrontendModelBase} */ (this.model.constructor)
714
+ const normalizedInput = await normalizeFrontendAttachmentInput(input)
715
+ const response = await ModelClass.executeCommand("attach", {
716
+ attachment: normalizedInput,
717
+ attachmentName: this.attachmentName,
718
+ id: this.model.primaryKeyValue()
719
+ })
720
+
721
+ this.model.assignAttributes(ModelClass.attributesFromResponse(response))
722
+ }
723
+
724
+ /**
725
+ * Runs download.
726
+ * @param {string} [attachmentId] - Optional attachment id for has-many attachments.
727
+ * @returns {Promise<FrontendModelAttachmentDownload | null>} - Downloaded attachment payload.
728
+ */
729
+ async download(attachmentId) {
730
+ const ModelClass = /**
731
+ * Narrows the runtime value to the documented type.
732
+ @type {typeof FrontendModelBase} */ (this.model.constructor)
733
+ const response = await ModelClass.executeCommand("download", frontendModelAttachmentCommandPayload(this, attachmentId))
734
+ const attachmentPayload = response.attachment
735
+
736
+ if (!attachmentPayload || typeof attachmentPayload !== "object") return null
737
+
738
+ const contentBase64 = typeof attachmentPayload.contentBase64 === "string" ? attachmentPayload.contentBase64 : ""
739
+ const content = frontendAttachmentBase64ToBytes(contentBase64)
740
+ const byteSize = Number(attachmentPayload.byteSize)
741
+
742
+ return new FrontendModelAttachmentDownload({
743
+ byteSize: Number.isFinite(byteSize) ? byteSize : content.length,
744
+ content,
745
+ contentType: typeof attachmentPayload.contentType === "string" && attachmentPayload.contentType.length > 0 ? attachmentPayload.contentType : null,
746
+ filename: typeof attachmentPayload.filename === "string" && attachmentPayload.filename.length > 0 ? attachmentPayload.filename : "attachment.bin",
747
+ id: typeof attachmentPayload.id === "string" ? attachmentPayload.id : "",
748
+ url: typeof attachmentPayload.url === "string" && attachmentPayload.url.length > 0 ? attachmentPayload.url : null
749
+ })
750
+ }
751
+
752
+ /**
753
+ * Runs url.
754
+ * @param {string} [attachmentId] - Optional attachment id for has-many attachments.
755
+ * @returns {Promise<string | null>} - Resolvable attachment URL.
756
+ */
757
+ async url(attachmentId) {
758
+ const ModelClass = /**
759
+ * Narrows the runtime value to the documented type.
760
+ @type {typeof FrontendModelBase} */ (this.model.constructor)
761
+ const response = await ModelClass.executeCommand("url", frontendModelAttachmentCommandPayload(this, attachmentId))
762
+
763
+ if (typeof response.url === "string" && response.url.length > 0) {
764
+ return response.url
765
+ }
766
+
767
+ return null
768
+ }
769
+
770
+ /**
771
+ * Runs download url.
772
+ * @returns {string} - Download URL for this attachment on the configured backend.
773
+ */
774
+ downloadUrl() {
775
+ const ModelClass = /**
776
+ * Narrows the runtime value to the documented type.
777
+ @type {typeof FrontendModelBase} */ (this.model.constructor)
778
+ const commandName = ModelClass.commandName("download")
779
+ const resourcePath = ModelClass.resourcePath()
780
+ const commandUrl = frontendModelCommandUrl(resourcePath, commandName)
781
+ const params = new URLSearchParams({
782
+ attachmentName: this.attachmentName,
783
+ id: String(this.model.primaryKeyValue())
784
+ })
785
+
786
+ return `${commandUrl}?${params.toString()}`
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Runs normalize frontend model transport url.
792
+ * @param {string | undefined | null} value - URL candidate.
793
+ * @returns {string} - Normalized URL without trailing slash.
794
+ */
795
+ function normalizeFrontendModelTransportUrl(value) {
796
+ if (typeof value !== "string") return ""
797
+
798
+ const trimmed = value.trim()
799
+
800
+ if (!trimmed.length) return ""
801
+
802
+ return trimmed.replace(/\/+$/, "")
803
+ }
804
+
805
+ /**
806
+ * Runs frontend model transport url.
807
+ * @returns {string} - Resolved frontend-model transport URL.
808
+ */
809
+ function frontendModelTransportUrl() {
810
+ const configuredUrl = typeof frontendModelTransportConfig.url === "function"
811
+ ? frontendModelTransportConfig.url()
812
+ : frontendModelTransportConfig.url
813
+
814
+ return normalizeFrontendModelTransportUrl(configuredUrl)
815
+ }
816
+
817
+ /**
818
+ * Runs clone frontend model attributes.
819
+ * @param {Record<string, ?>} value - Attributes hash.
820
+ * @returns {Record<string, ?>} - Cloned attributes hash.
821
+ */
822
+ function cloneFrontendModelAttributes(value) {
823
+ return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(serializeFrontendModelTransportValue(value)))
824
+ }
825
+
826
+ /**
827
+ * Shared channel name for model lifecycle events (Phase 3).
828
+ * Matches the backend `FRONTEND_MODELS_CHANNEL_NAME`.
829
+ */
830
+ const FRONTEND_MODELS_CHANNEL_NAME = "frontend-models"
831
+
832
+ /**
833
+ * Defines this typedef.
834
+ * @typedef {{callback: (payload: {id: string, model: InstanceType<typeof FrontendModelBase>}) => void, eventFilterKey: string | null, eventFilterPayload: import("./query.js").FrontendModelEventFilterPayload | null, projectionPayload: import("./query.js").FrontendModelProjectionPayload}} FrontendModelModelEventCallbackEntry
835
+ */
836
+ /**
837
+ * Defines this typedef.
838
+ * @typedef {{callback: (payload: {id: string}) => void}} FrontendModelDestroyEventCallbackEntry
839
+ */
840
+
841
+ /**
842
+ * Runs merge frontend model event preload.
843
+ * @param {Record<string, import("./query.js").FrontendModelTransportValue>} target - Target preload payload.
844
+ * @param {Record<string, import("./query.js").FrontendModelTransportValue>} source - Source preload payload.
845
+ * @returns {void}
846
+ */
847
+ function mergeFrontendModelEventPreload(target, source) {
848
+ for (const [relationshipName, value] of Object.entries(source)) {
849
+ const existingValue = target[relationshipName]
850
+
851
+ if (value === true || value === false) {
852
+ if (existingValue === undefined) target[relationshipName] = value
853
+ continue
854
+ }
855
+
856
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
857
+ target[relationshipName] = value
858
+ continue
859
+ }
860
+
861
+ if (!existingValue || typeof existingValue !== "object" || Array.isArray(existingValue)) {
862
+ target[relationshipName] = {}
863
+ }
864
+
865
+ mergeFrontendModelEventPreload(
866
+ /**
867
+ * Narrows the runtime value to the documented type.
868
+ @type {Record<string, import("./query.js").FrontendModelTransportValue>} */ (target[relationshipName]),
869
+ /**
870
+ * Narrows the runtime value to the documented type.
871
+ @type {Record<string, import("./query.js").FrontendModelTransportValue>} */ (value)
872
+ )
873
+ }
874
+ }
875
+
876
+ /**
877
+ * Runs merge frontend model event select.
878
+ * @param {Record<string, string[]>} target - Target select map.
879
+ * @param {Record<string, string[]>} source - Source select map.
880
+ * @returns {void}
881
+ */
882
+ function mergeFrontendModelEventSelect(target, source) {
883
+ for (const [modelName, attributes] of Object.entries(source)) {
884
+ const existingAttributes = target[modelName] || []
885
+
886
+ target[modelName] = Array.from(new Set(existingAttributes.concat(attributes)))
887
+ }
888
+ }
889
+
890
+ /**
891
+ * Runs merge unique frontend model event entries.
892
+ * @param {Array<import("./query.js").FrontendModelWithCountPayloadEntry | import("./query.js").FrontendModelAbilitiesPayloadEntry>} target - Target array.
893
+ * @param {Array<import("./query.js").FrontendModelWithCountPayloadEntry | import("./query.js").FrontendModelAbilitiesPayloadEntry>} source - Source array.
894
+ * @returns {void}
895
+ */
896
+ function mergeUniqueFrontendModelEventEntries(target, source) {
897
+ const existingKeys = new Set(target.map((entry) => JSON.stringify(entry)))
898
+
899
+ for (const entry of source) {
900
+ const key = JSON.stringify(entry)
901
+
902
+ if (existingKeys.has(key)) continue
903
+
904
+ target.push(entry)
905
+ existingKeys.add(key)
906
+ }
907
+ }
908
+
909
+ /**
910
+ * Runs merge frontend model event projection payload.
911
+ * @param {import("./query.js").FrontendModelProjectionPayload} target - Target payload.
912
+ * @param {import("./query.js").FrontendModelProjectionPayload} source - Source payload.
913
+ * @returns {void}
914
+ */
915
+ function mergeFrontendModelEventProjectionPayload(target, source) {
916
+ if (source.preload) {
917
+ if (!target.preload) target.preload = {}
918
+ mergeFrontendModelEventPreload(target.preload, source.preload)
919
+ }
920
+
921
+ if (source.select) {
922
+ if (!target.select) target.select = {}
923
+ mergeFrontendModelEventSelect(target.select, source.select)
924
+ }
925
+
926
+ if (source.selectsExtra) {
927
+ if (!target.selectsExtra) target.selectsExtra = {}
928
+ mergeFrontendModelEventSelect(target.selectsExtra, source.selectsExtra)
929
+ }
930
+
931
+ if (source.withCount) {
932
+ if (!target.withCount) target.withCount = []
933
+ mergeUniqueFrontendModelEventEntries(target.withCount, source.withCount)
934
+ }
935
+
936
+ if (source.abilities) {
937
+ if (!target.abilities) target.abilities = []
938
+ mergeUniqueFrontendModelEventEntries(target.abilities, source.abilities)
939
+ }
940
+
941
+ if (source.queryData !== undefined) {
942
+ const targetQueryData = Array.isArray(target.queryData) ? target.queryData : []
943
+
944
+ target.queryData = targetQueryData
945
+ const queryDataEntries = Array.isArray(source.queryData) ? source.queryData : [source.queryData]
946
+
947
+ for (const entry of queryDataEntries) {
948
+ targetQueryData.push(entry)
949
+ }
950
+ }
951
+ }
952
+
953
+ /**
954
+ * Runs frontend model matched event filter keys.
955
+ * @param {?} body - Raw websocket event body.
956
+ * @returns {Set<string>} - Matched event filter keys delivered by the backend.
957
+ */
958
+ function frontendModelMatchedEventFilterKeys(body) {
959
+ if (!body || typeof body !== "object") return new Set()
960
+
961
+ const keys = /**
962
+ * Narrows the runtime value to the documented type.
963
+ @type {{matchedEventFilterKeys?: ?}} */ (body).matchedEventFilterKeys
964
+
965
+ if (!Array.isArray(keys)) return new Set()
966
+
967
+ return new Set(keys.map((key) => String(key)))
968
+ }
969
+
970
+ /**
971
+ * Runs frontend model event entry matches.
972
+ * @param {FrontendModelModelEventCallbackEntry} entry - Callback entry.
973
+ * @param {Set<string>} matchedEventFilterKeys - Backend matched filter keys.
974
+ * @returns {boolean} Whether the callback should receive the event.
975
+ */
976
+ function frontendModelEventEntryMatches(entry, matchedEventFilterKeys) {
977
+ if (!entry.eventFilterKey) return true
978
+
979
+ return matchedEventFilterKeys.has(entry.eventFilterKey)
980
+ }
981
+
982
+ /**
983
+ * Runs assert no destroy event filter.
984
+ * @param {typeof FrontendModelBase} ModelClass - Event model class.
985
+ * @param {import("./query.js").FrontendModelEventOptions} options - Event options.
986
+ * @returns {void}
987
+ */
988
+ function assertNoDestroyEventFilter(ModelClass, options) {
989
+ const eventOptionsPayload = frontendModelEventOptionsPayload(ModelClass, options)
990
+
991
+ if (!eventOptionsPayload.eventFilterKey) return
992
+
993
+ throw new Error("Frontend model destroy event subscriptions do not support query filters")
994
+ }
995
+
996
+ /**
997
+ * Per-model class singleton that multiplexes all registered onCreate /
998
+ * onUpdate / onDestroy callbacks — class-level + instance-level —
999
+ * over one WebsocketChannelV2 subscription. Subscription opens on the
1000
+ * first listener and closes when the last one unsubscribes.
1001
+ *
1002
+ * Instance-level listeners also receive auto-merge: when an `update`
1003
+ * event arrives for a registered instance id, the instance's
1004
+ * attributes are updated in place before the callback fires, so
1005
+ * callers can read fresh values from the same instance handle.
1006
+ */
1007
+ class FrontendModelEventSubscription {
1008
+ /**
1009
+ * Runs constructor.
1010
+ * @param {typeof FrontendModelBase} ModelClass - Frontend model class for this subscription bucket.
1011
+ */
1012
+ constructor(ModelClass) {
1013
+ this.ModelClass = ModelClass
1014
+ /**
1015
+ * Narrows the runtime value to the documented type.
1016
+ @type {Set<FrontendModelModelEventCallbackEntry>} */
1017
+ this.classCreateCallbacks = new Set()
1018
+ /**
1019
+ * Narrows the runtime value to the documented type.
1020
+ @type {Set<FrontendModelModelEventCallbackEntry>} */
1021
+ this.classUpdateCallbacks = new Set()
1022
+ /**
1023
+ * Narrows the runtime value to the documented type.
1024
+ @type {Set<FrontendModelDestroyEventCallbackEntry>} */
1025
+ this.classDestroyCallbacks = new Set()
1026
+ /**
1027
+ * Narrows the runtime value to the documented type.
1028
+ @type {Map<string, {instance: InstanceType<typeof FrontendModelBase>, updateCallbacks: Set<FrontendModelModelEventCallbackEntry>, destroyCallbacks: Set<FrontendModelDestroyEventCallbackEntry>}>} */
1029
+ this.instanceListeners = new Map()
1030
+ /**
1031
+ * Narrows the runtime value to the documented type.
1032
+ @type {?} */
1033
+ this.channelHandle = null
1034
+ /**
1035
+ * Narrows the runtime value to the documented type.
1036
+ @type {Promise<void> | null} */
1037
+ this.readyPromise = null
1038
+ /**
1039
+ * Narrows the runtime value to the documented type.
1040
+ @type {string | null} */
1041
+ this.subscriptionParamsKey = null
1042
+ }
1043
+
1044
+ /**
1045
+ * Runs subscription params.
1046
+ * @returns {{model: string, eventFilters?: import("./query.js").FrontendModelEventFilterPayloadEntry[], unfilteredEventDelivery?: boolean} & import("./query.js").FrontendModelProjectionPayload} - Current websocket subscription params.
1047
+ */
1048
+ subscriptionParams() {
1049
+ /**
1050
+ * Projection payload.
1051
+ @type {import("./query.js").FrontendModelProjectionPayload} */
1052
+ const projectionPayload = {}
1053
+ /**
1054
+ * Event filters by key.
1055
+ @type {Record<string, import("./query.js").FrontendModelEventFilterPayloadEntry>} */
1056
+ const eventFiltersByKey = {}
1057
+ const projectionEntries = []
1058
+ let hasUnfilteredEventDelivery = this.classDestroyCallbacks.size > 0
1059
+
1060
+ for (const entry of this.classCreateCallbacks) projectionEntries.push(entry)
1061
+ for (const entry of this.classUpdateCallbacks) projectionEntries.push(entry)
1062
+
1063
+ for (const listener of this.instanceListeners.values()) {
1064
+ for (const entry of listener.updateCallbacks) projectionEntries.push(entry)
1065
+ if (listener.destroyCallbacks.size > 0) hasUnfilteredEventDelivery = true
1066
+ }
1067
+
1068
+ for (const entry of projectionEntries) {
1069
+ mergeFrontendModelEventProjectionPayload(projectionPayload, entry.projectionPayload)
1070
+
1071
+ if (entry.eventFilterKey && entry.eventFilterPayload) {
1072
+ eventFiltersByKey[entry.eventFilterKey] = {
1073
+ ...entry.eventFilterPayload,
1074
+ key: entry.eventFilterKey
1075
+ }
1076
+ } else {
1077
+ hasUnfilteredEventDelivery = true
1078
+ }
1079
+ }
1080
+
1081
+ const eventFilters = Object.values(eventFiltersByKey)
1082
+ const eventFilterParams = eventFilters.length > 0
1083
+ ? {
1084
+ eventFilters,
1085
+ ...(hasUnfilteredEventDelivery ? {unfilteredEventDelivery: true} : {})
1086
+ }
1087
+ : {}
1088
+
1089
+ return {
1090
+ model: this.ModelClass.getModelName(),
1091
+ ...eventFilterParams,
1092
+ ...projectionPayload
1093
+ }
1094
+ }
1095
+
1096
+ /**
1097
+ * Runs subscription params json.
1098
+ * @returns {string} - Stable key for current subscription params.
1099
+ */
1100
+ subscriptionParamsJson() {
1101
+ return JSON.stringify(this.subscriptionParams())
1102
+ }
1103
+
1104
+ /**
1105
+ * Runs ensure subscribed.
1106
+ @returns {Promise<void>} */
1107
+ async ensureSubscribed() {
1108
+ const paramsJson = this.subscriptionParamsJson()
1109
+
1110
+ if (this.channelHandle && !this.channelHandle.isClosed()) {
1111
+ if (this.subscriptionParamsKey !== paramsJson) {
1112
+ this.channelHandle.close()
1113
+ this.channelHandle = null
1114
+ this.readyPromise = null
1115
+ } else {
1116
+ if (this.readyPromise) await this.readyPromise
1117
+ return
1118
+ }
1119
+ }
1120
+
1121
+ // Serialize parallel calls (e.g. Promise.all([onCreate, onUpdate,
1122
+ // onDestroy])) so we open exactly one subscription per model class
1123
+ // instead of racing three concurrent subscribeChannel calls.
1124
+ if (this.readyPromise) {
1125
+ await this.readyPromise
1126
+ return
1127
+ }
1128
+
1129
+ const client = /**
1130
+ * Narrows the runtime value to the documented type.
1131
+ @type {?} */ (frontendModelTransportConfig.websocketClient || resolveInternalWebsocketClient())
1132
+
1133
+ if (!client || typeof client.subscribeChannel !== "function") {
1134
+ throw new Error("Frontend model event subscriptions require configureTransport({websocketUrl}) or configureTransport({websocketClient})")
1135
+ }
1136
+
1137
+ this.readyPromise = (async () => {
1138
+ if (typeof client.connect === "function") await client.connect()
1139
+
1140
+ const params = this.subscriptionParams()
1141
+
1142
+ this.subscriptionParamsKey = JSON.stringify(params)
1143
+ this.channelHandle = client.subscribeChannel(FRONTEND_MODELS_CHANNEL_NAME, {
1144
+ params,
1145
+ onMessage: (/**
1146
+ * Narrows the runtime value to the documented type.
1147
+ @type {?} */ body) => this._dispatchEvent(body),
1148
+ onClose: () => {
1149
+ this.channelHandle = null
1150
+ this.readyPromise = null
1151
+ this.subscriptionParamsKey = null
1152
+ this.instanceListeners.clear()
1153
+
1154
+ const hasCallbacks = this.classCreateCallbacks.size > 0
1155
+ || this.classUpdateCallbacks.size > 0
1156
+ || this.classDestroyCallbacks.size > 0
1157
+
1158
+ if (hasCallbacks && client.autoReconnect) {
1159
+ void this.ensureSubscribed()
1160
+ }
1161
+ }
1162
+ })
1163
+ await this.channelHandle.ready
1164
+ })()
1165
+
1166
+ await this.readyPromise
1167
+ }
1168
+
1169
+ /**
1170
+ * Runs dispatch event.
1171
+ * @param {?} body - WebSocket event payload.
1172
+ */
1173
+ _dispatchEvent(body) {
1174
+ if (!body || typeof body !== "object") return
1175
+
1176
+ const action = body.action
1177
+ const rawId = body.id
1178
+
1179
+ if (action !== "create" && action !== "update" && action !== "destroy") return
1180
+ if (rawId === undefined || rawId === null) return
1181
+
1182
+ const id = String(rawId)
1183
+ const matchedEventFilterKeys = frontendModelMatchedEventFilterKeys(body)
1184
+
1185
+ if (action === "destroy") {
1186
+ const listener = this.instanceListeners.get(id)
1187
+
1188
+ if (listener) {
1189
+ for (const entry of listener.destroyCallbacks) {
1190
+ try { entry.callback({id}) } catch (error) { console.error(error) }
1191
+ }
1192
+ this.instanceListeners.delete(id)
1193
+ }
1194
+ for (const entry of this.classDestroyCallbacks) {
1195
+ try { entry.callback({id}) } catch (error) { console.error(error) }
1196
+ }
1197
+ return
1198
+ }
1199
+
1200
+ if (!body.record || typeof body.record !== "object") return
1201
+
1202
+ const deserializedRecord = /**
1203
+ * Narrows the runtime value to the documented type.
1204
+ @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(body.record))
1205
+ const freshModel = /**
1206
+ * Narrows the runtime value to the documented type.
1207
+ @type {?} */ (this.ModelClass).instantiateFromResponse(deserializedRecord)
1208
+ const listener = this.instanceListeners.get(id)
1209
+
1210
+ if (action === "update" && listener) {
1211
+ const matchingUpdateCallbacks = Array.from(listener.updateCallbacks).filter((entry) =>
1212
+ frontendModelEventEntryMatches(entry, matchedEventFilterKeys)
1213
+ )
1214
+
1215
+ if (matchingUpdateCallbacks.length > 0) {
1216
+ // Auto-merge into the registered instance so callers reading
1217
+ // through the same handle see fresh attributes.
1218
+ const instanceAny = /**
1219
+ * Narrows the runtime value to the documented type.
1220
+ @type {?} */ (listener.instance)
1221
+
1222
+ instanceAny.assignAttributes(freshModel.attributes())
1223
+ instanceAny._persistedAttributes = cloneFrontendModelAttributes(listener.instance.attributes())
1224
+
1225
+ for (const entry of matchingUpdateCallbacks) {
1226
+ try { entry.callback({id, model: listener.instance}) } catch (error) { console.error(error) }
1227
+ }
1228
+ }
1229
+ }
1230
+
1231
+ const classCallbacks = action === "create" ? this.classCreateCallbacks : this.classUpdateCallbacks
1232
+
1233
+ for (const entry of classCallbacks) {
1234
+ if (!frontendModelEventEntryMatches(entry, matchedEventFilterKeys)) continue
1235
+
1236
+ try { entry.callback({id, model: freshModel}) } catch (error) { console.error(error) }
1237
+ }
1238
+ }
1239
+
1240
+ /**
1241
+ * Runs maybe teardown.
1242
+ @returns {void} */
1243
+ maybeTeardown() {
1244
+ const hasAnyListener = this.classCreateCallbacks.size > 0
1245
+ || this.classUpdateCallbacks.size > 0
1246
+ || this.classDestroyCallbacks.size > 0
1247
+ || this.instanceListeners.size > 0
1248
+
1249
+ if (hasAnyListener) return
1250
+ if (!this.channelHandle) return
1251
+
1252
+ try {
1253
+ this.channelHandle.close()
1254
+ } catch (error) {
1255
+ console.error(error)
1256
+ }
1257
+
1258
+ this.channelHandle = null
1259
+ this.readyPromise = null
1260
+ this.subscriptionParamsKey = null
1261
+ }
1262
+ }
1263
+
1264
+ /**
1265
+ * Frontend model event subscriptions.
1266
+ @type {WeakMap<typeof FrontendModelBase, FrontendModelEventSubscription>} */
1267
+ const frontendModelEventSubscriptions = new WeakMap()
1268
+
1269
+ /**
1270
+ * Runs ensure frontend model event subscription.
1271
+ * @param {typeof FrontendModelBase} ModelClass - Model class.
1272
+ * @returns {FrontendModelEventSubscription} - Per-class subscription helper.
1273
+ */
1274
+ function ensureFrontendModelEventSubscription(ModelClass) {
1275
+ let sub = frontendModelEventSubscriptions.get(ModelClass)
1276
+
1277
+ if (!sub) {
1278
+ sub = new FrontendModelEventSubscription(ModelClass)
1279
+ frontendModelEventSubscriptions.set(ModelClass, sub)
1280
+ }
1281
+
1282
+ return sub
1283
+ }
1284
+
1285
+ /**
1286
+ * Runs ensure frontend model instance listener.
1287
+ * @param {FrontendModelEventSubscription} sub - Event subscription bucket.
1288
+ * @param {string} id - Model id.
1289
+ * @param {InstanceType<typeof FrontendModelBase>} instance - Listener instance.
1290
+ * @returns {{instance: InstanceType<typeof FrontendModelBase>, updateCallbacks: Set<FrontendModelModelEventCallbackEntry>, destroyCallbacks: Set<FrontendModelDestroyEventCallbackEntry>}} - Instance listener bucket.
1291
+ */
1292
+ function ensureFrontendModelInstanceListener(sub, id, instance) {
1293
+ let listener = sub.instanceListeners.get(id)
1294
+
1295
+ if (!listener) {
1296
+ listener = {instance, updateCallbacks: new Set(), destroyCallbacks: new Set()}
1297
+ sub.instanceListeners.set(id, listener)
1298
+ } else {
1299
+ listener.instance = instance
1300
+ }
1301
+
1302
+ return listener
1303
+ }
1304
+
1305
+ /**
1306
+ * Runs frontend model command url.
1307
+ * @param {string} resourcePath - Resource path prefix.
1308
+ * @param {string} commandName - Command path segment.
1309
+ * @returns {string} - Frontend model API URL.
1310
+ */
1311
+ function frontendModelCommandUrl(resourcePath, commandName) {
1312
+ const configuredUrl = frontendModelTransportUrl()
1313
+ const normalizedResourcePath = resourcePath.startsWith("/") ? resourcePath : `/${resourcePath}`
1314
+
1315
+ return `${configuredUrl}${normalizedResourcePath}/${commandName}`
1316
+ }
1317
+
1318
+ /**
1319
+ * Runs frontend model api url.
1320
+ * @returns {string} - Shared frontend-model API URL.
1321
+ */
1322
+ function frontendModelApiUrl() {
1323
+ return `${frontendModelTransportUrl()}${SHARED_FRONTEND_MODEL_API_PATH}`
1324
+ }
1325
+
1326
+ /**
1327
+ * Runs frontend model transport path.
1328
+ * @param {string} url - Request URL or path.
1329
+ * @returns {string} - Websocket-safe request path.
1330
+ */
1331
+ function frontendModelTransportPath(url) {
1332
+ if (typeof url !== "string" || url.length < 1) {
1333
+ throw new Error(`Expected frontend model transport URL/path, got: ${url}`)
1334
+ }
1335
+
1336
+ if (url.startsWith("/")) {
1337
+ return url
1338
+ }
1339
+
1340
+ try {
1341
+ const parsedUrl = new URL(url)
1342
+
1343
+ return `${parsedUrl.pathname}${parsedUrl.search}`
1344
+ } catch {
1345
+ return url
1346
+ }
1347
+ }
1348
+
1349
+ /**
1350
+ * Runs frontend model request headers.
1351
+ * @returns {Record<string, string>} - Headers for frontend-model HTTP requests.
1352
+ */
1353
+ function frontendModelRequestHeaders() {
1354
+ const dynamicHeaders = typeof frontendModelTransportConfig.requestHeaders === "function"
1355
+ ? (frontendModelTransportConfig.requestHeaders() || {})
1356
+ : (frontendModelTransportConfig.requestHeaders || {})
1357
+
1358
+ return {"Content-Type": "application/json", ...dynamicHeaders}
1359
+ }
1360
+
1361
+ /**
1362
+ * Runs perform shared frontend model api request.
1363
+ * @param {Record<string, ?>} requestPayload - Shared request payload.
1364
+ * @returns {Promise<Record<string, ?>>} - Decoded shared frontend-model API response.
1365
+ */
1366
+ async function performSharedFrontendModelApiRequest(requestPayload) {
1367
+ const serializedRequestPayload = serializeFrontendModelTransportValue(requestPayload)
1368
+ const websocketClient = frontendModelTransportConfig.websocketClient
1369
+ const url = frontendModelApiUrl()
1370
+ const mergedHeaders = frontendModelRequestHeaders()
1371
+
1372
+ if (websocketClient) {
1373
+ const response = await websocketClient.post(frontendModelTransportPath(url), serializedRequestPayload, {
1374
+ headers: mergedHeaders
1375
+ })
1376
+ const responseJson = response.json()
1377
+
1378
+ return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(responseJson))
1379
+ }
1380
+
1381
+ const response = await fetch(url, {
1382
+ body: JSON.stringify(serializedRequestPayload),
1383
+ credentials: "include",
1384
+ headers: mergedHeaders,
1385
+ method: "POST"
1386
+ })
1387
+
1388
+ const responseText = await response.text()
1389
+
1390
+ if (!response.ok) {
1391
+ throwFrontendModelHttpError({
1392
+ commandLabel: "shared frontend model API",
1393
+ response,
1394
+ responseText
1395
+ })
1396
+ }
1397
+
1398
+ const json = responseText.length > 0 ? JSON.parse(responseText) : {}
1399
+
1400
+ return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(json))
1401
+ }
1402
+
1403
+ /**
1404
+ * Throws a frontend-model HTTP error with backend-provided envelope details when available.
1405
+ * @param {{commandLabel: string, response: Response, responseText: string}} args - Error response details.
1406
+ * @returns {never}
1407
+ */
1408
+ function throwFrontendModelHttpError({commandLabel, response, responseText}) {
1409
+ // Surface the backend's friendly errorMessage envelope (the
1410
+ // `{status: "error", errorMessage: "..."}` shape every controller
1411
+ // ships on its 4xx/5xx responses) instead of the generic status
1412
+ // string. Fall through to the status-only message when the body is
1413
+ // missing, non-JSON, or has no usable errorMessage field.
1414
+ const responseContentType = response.headers.get("content-type")
1415
+
1416
+ if (responseContentType && responseContentType.includes("application/json") && responseText.length > 0) {
1417
+ /**
1418
+ * Defines errorBody.
1419
+ @type {Record<string, ?> | null} */
1420
+ let errorBody
1421
+
1422
+ try {
1423
+ errorBody = JSON.parse(responseText)
1424
+ } catch {
1425
+ errorBody = null
1426
+ }
1427
+
1428
+ if (errorBody && typeof errorBody.errorMessage === "string" && errorBody.errorMessage.trim().length > 0) {
1429
+ throw new Error(errorBody.errorMessage.trim())
1430
+ }
1431
+ }
1432
+
1433
+ throw new Error(`Request failed (${response.status}) for ${commandLabel}`)
1434
+ }
1435
+
1436
+ /**
1437
+ * Runs flush pending shared frontend model requests.
1438
+ * @returns {Promise<void>} - Resolves after pending shared frontend-model requests flush.
1439
+ */
1440
+ async function flushPendingSharedFrontendModelRequests() {
1441
+ sharedFrontendModelFlushScheduled = false
1442
+
1443
+ if (pendingSharedFrontendModelRequests.length < 1) {
1444
+ resolveFrontendModelIdleWaiters()
1445
+ return
1446
+ }
1447
+
1448
+ const batchedRequests = pendingSharedFrontendModelRequests
1449
+ pendingSharedFrontendModelRequests = []
1450
+
1451
+ const url = frontendModelApiUrl()
1452
+ const requestPayload = {
1453
+ requests: batchedRequests.map((request) => {
1454
+ if (request.customPath) {
1455
+ return {
1456
+ commandType: request.commandType,
1457
+ customPath: request.customPath,
1458
+ model: request.modelClass.getModelName(),
1459
+ payload: request.payload,
1460
+ requestId: request.requestId
1461
+ }
1462
+ }
1463
+
1464
+ return {
1465
+ commandType: request.commandType,
1466
+ model: request.modelClass.getModelName(),
1467
+ payload: request.payload,
1468
+ requestId: request.requestId
1469
+ }
1470
+ })
1471
+ }
1472
+
1473
+ await trackFrontendModelTransportRequest(async () => {
1474
+ try {
1475
+ void url
1476
+ const decodedResponse = await performSharedFrontendModelApiRequest(requestPayload)
1477
+ const responses = Array.isArray(decodedResponse.responses) ? decodedResponse.responses : []
1478
+ const responsesById = new Map(responses.map((entry) => [entry.requestId, entry.response]))
1479
+
1480
+ for (const request of batchedRequests) {
1481
+ const responsePayload = responsesById.get(request.requestId)
1482
+
1483
+ if (!responsePayload || typeof responsePayload !== "object") {
1484
+ request.reject(new Error(`Missing batched response for ${request.modelClass.name}#${request.commandType}`))
1485
+ continue
1486
+ }
1487
+
1488
+ request.resolve(/**
1489
+ * Narrows the runtime value to the documented type.
1490
+ @type {Record<string, ?>} */ (responsePayload))
1491
+ }
1492
+ } catch (error) {
1493
+ for (const request of batchedRequests) {
1494
+ request.reject(error)
1495
+ }
1496
+ }
1497
+ })
1498
+ }
1499
+
1500
+ /**
1501
+ * Runs schedule shared frontend model request flush.
1502
+ @returns {void} */
1503
+ function scheduleSharedFrontendModelRequestFlush() {
1504
+ if (sharedFrontendModelFlushScheduled) return
1505
+
1506
+ sharedFrontendModelFlushScheduled = true
1507
+ queueMicrotask(() => {
1508
+ void flushPendingSharedFrontendModelRequests()
1509
+ })
1510
+ }
1511
+
1512
+ /**
1513
+ * Custom commands still use the shared frontend-model API. This helper only builds the backend route path the server should dispatch after validating the segments.
1514
+ * @param {object} args - Arguments.
1515
+ * @param {string} args.commandName - Command path segment.
1516
+ * @param {string} args.modelName - Frontend model class name.
1517
+ * @param {string | number | null | undefined} [args.memberId] - Optional member id.
1518
+ * @param {string} args.resourcePath - Resource path prefix.
1519
+ * @returns {string} - Custom backend route path.
1520
+ */
1521
+ function frontendModelCustomCommandPath({commandName, memberId, modelName, resourcePath}) {
1522
+ const validatedResourcePath = validateFrontendModelResourcePath({modelName, resourcePath})
1523
+ const validatedCommandName = validateFrontendModelResourceCommandName({commandName, commandType: commandName, modelName})
1524
+
1525
+ if (memberId === undefined || memberId === null || memberId === "") {
1526
+ return `${validatedResourcePath}/${validatedCommandName}`
1527
+ }
1528
+
1529
+ return `${validatedResourcePath}/${encodeURIComponent(String(memberId))}/${validatedCommandName}`
1530
+ }
1531
+
1532
+ /**
1533
+ * Runs assert find by conditions shape.
1534
+ * @param {?} conditions - findBy conditions.
1535
+ * @returns {void}
1536
+ */
1537
+ function assertFindByConditionsShape(conditions) {
1538
+ if (!conditions || typeof conditions !== "object" || Array.isArray(conditions)) {
1539
+ throw new Error(`findBy expects conditions to be a plain object, got: ${conditions}`)
1540
+ }
1541
+
1542
+ const conditionsPrototype = Object.getPrototypeOf(conditions)
1543
+
1544
+ if (conditionsPrototype !== Object.prototype && conditionsPrototype !== null) {
1545
+ throw new Error(`findBy expects conditions to be a plain object, got: ${conditions}`)
1546
+ }
1547
+
1548
+ const symbolKeys = Object.getOwnPropertySymbols(conditions)
1549
+
1550
+ if (symbolKeys.length > 0) {
1551
+ throw new Error(`findBy does not support symbol condition keys (keys: ${symbolKeys.map((key) => key.toString()).join(", ")})`)
1552
+ }
1553
+ }
1554
+
1555
+ /**
1556
+ * Runs assert defined find by condition value.
1557
+ * @param {?} value - Condition value to validate.
1558
+ * @param {string} keyPath - Key path for error output.
1559
+ * @returns {void}
1560
+ */
1561
+ function assertDefinedFindByConditionValue(value, keyPath) {
1562
+ if (value === undefined) {
1563
+ throw new Error(`findBy does not support undefined condition values (key: ${keyPath})`)
1564
+ }
1565
+
1566
+ if (typeof value === "function") {
1567
+ throw new Error(`findBy does not support function condition values (key: ${keyPath})`)
1568
+ }
1569
+
1570
+ if (typeof value === "symbol") {
1571
+ throw new Error(`findBy does not support symbol condition values (key: ${keyPath})`)
1572
+ }
1573
+
1574
+ if (typeof value === "bigint") {
1575
+ throw new Error(`findBy does not support bigint condition values (key: ${keyPath})`)
1576
+ }
1577
+
1578
+ if (typeof value === "number" && !Number.isFinite(value)) {
1579
+ throw new Error(`findBy does not support non-finite number condition values (key: ${keyPath})`)
1580
+ }
1581
+
1582
+ if (Array.isArray(value)) {
1583
+ value.forEach((entry, index) => {
1584
+ assertDefinedFindByConditionValue(entry, `${keyPath}[${index}]`)
1585
+ })
1586
+ return
1587
+ }
1588
+
1589
+ if (value && typeof value === "object") {
1590
+ if (value instanceof Date) {
1591
+ return
1592
+ }
1593
+
1594
+ const objectValue = /**
1595
+ * Narrows the runtime value to the documented type.
1596
+ @type {Record<string, ?>} */ (value)
1597
+ const prototype = Object.getPrototypeOf(objectValue)
1598
+
1599
+ if (prototype !== Object.prototype && prototype !== null) {
1600
+ throw new Error(`findBy does not support non-plain object condition values (key: ${keyPath})`)
1601
+ }
1602
+
1603
+ const symbolKeys = Object.getOwnPropertySymbols(objectValue)
1604
+
1605
+ if (symbolKeys.length > 0) {
1606
+ throw new Error(`findBy does not support symbol condition keys (key: ${keyPath})`)
1607
+ }
1608
+
1609
+ const valueObject = /**
1610
+ * Narrows the runtime value to the documented type.
1611
+ @type {Record<string, ?>} */ (value)
1612
+
1613
+ Object.keys(valueObject).forEach((nestedKey) => {
1614
+ assertDefinedFindByConditionValue(valueObject[nestedKey], `${keyPath}.${nestedKey}`)
1615
+ })
1616
+ }
1617
+ }
1618
+
1619
+ /** Base class for generated frontend model classes. */
1620
+ export default class FrontendModelBase {
1621
+ /**
1622
+ * Narrows the runtime value to the documented type.
1623
+ @type {string | undefined} */
1624
+ static modelName
1625
+
1626
+ /**
1627
+ * Autoload.
1628
+ * @type {boolean} - Global auto-batch-preload toggle. Apps can opt out via FrontendModelBase.setAutoload(false).
1629
+ */
1630
+ static _autoload = true
1631
+
1632
+ /**
1633
+ * Runs get autoload.
1634
+ * @returns {boolean} Whether auto-batch-preload of relationships on lazy access is enabled globally.
1635
+ */
1636
+ static getAutoload() { return FrontendModelBase._autoload }
1637
+
1638
+ /**
1639
+ * Runs set autoload.
1640
+ * @param {boolean} newValue - Whether auto-batch-preload of relationships is enabled.
1641
+ * @returns {void}
1642
+ */
1643
+ static setAutoload(newValue) { FrontendModelBase._autoload = newValue }
1644
+
1645
+ /**
1646
+ * Narrows the runtime value to the documented type.
1647
+ @type {Record<string, ?>} */
1648
+ _attributes
1649
+ /**
1650
+ * Narrows the runtime value to the documented type.
1651
+ @type {Record<string, FrontendModelHasManyRelationship<typeof FrontendModelBase, typeof FrontendModelBase> | FrontendModelSingularRelationship<typeof FrontendModelBase, typeof FrontendModelBase>>} */
1652
+ _relationships
1653
+ /**
1654
+ * Narrows the runtime value to the documented type.
1655
+ @type {Record<string, FrontendModelAttachmentHandle>} */
1656
+ _attachments
1657
+ /**
1658
+ * Narrows the runtime value to the documented type.
1659
+ @type {Set<string> | null} */
1660
+ _selectedAttributes
1661
+ /**
1662
+ * Narrows the runtime value to the documented type.
1663
+ @type {boolean} */
1664
+ _isNewRecord
1665
+ /**
1666
+ * Narrows the runtime value to the documented type.
1667
+ @type {boolean} */
1668
+ _markedForDestruction
1669
+ /**
1670
+ * Narrows the runtime value to the documented type.
1671
+ @type {Record<string, ?>} */
1672
+ _persistedAttributes
1673
+ /**
1674
+ * Narrows the runtime value to the documented type.
1675
+ * @type {Array<FrontendModelBase> | undefined} - Shared reference to sibling records loaded in the same batch. Used by auto-batch-preload.
1676
+ */
1677
+ _loadCohort
1678
+
1679
+ /**
1680
+ * Runs constructor.
1681
+ * @param {Record<string, ?>} [attributes] - Initial attributes.
1682
+ */
1683
+ constructor(attributes = {}) {
1684
+ const ModelClass = /**
1685
+ * Narrows the runtime value to the documented type.
1686
+ @type {typeof FrontendModelBase} */ (this.constructor)
1687
+
1688
+ ModelClass.ensureGeneratedAttachmentMethods()
1689
+ this._attributes = {}
1690
+ this._relationships = {}
1691
+ this._attachments = {}
1692
+ this._selectedAttributes = null
1693
+ this._isNewRecord = true
1694
+ this._markedForDestruction = false
1695
+ this._persistedAttributes = {}
1696
+ this.assignAttributes(attributes)
1697
+ }
1698
+
1699
+ /**
1700
+ * Runs ensure generated attachment methods.
1701
+ * @this {typeof FrontendModelBase}
1702
+ * @returns {void} - Ensures attachment helper methods exist on the prototype.
1703
+ */
1704
+ static ensureGeneratedAttachmentMethods() {
1705
+ if (this._generatedAttachmentMethods) return
1706
+
1707
+ const attachments = this.attachmentDefinitions()
1708
+ const prototype = /**
1709
+ * Narrows the runtime value to the documented type.
1710
+ @type {Record<string, ?>} */ (this.prototype)
1711
+
1712
+ for (const attachmentName of Object.keys(attachments)) {
1713
+ if (!(attachmentName in prototype)) {
1714
+ prototype[attachmentName] = function() {
1715
+ return this.getAttachmentByName(attachmentName)
1716
+ }
1717
+ }
1718
+ }
1719
+
1720
+ this._generatedAttachmentMethods = true
1721
+ }
1722
+
1723
+ /**
1724
+ * Runs resource config.
1725
+ * @returns {FrontendModelResourceConfig} - Resource configuration.
1726
+ */
1727
+ static resourceConfig() {
1728
+ throw new Error("resourceConfig() must be implemented by subclasses")
1729
+ // eslint-disable-next-line no-unreachable
1730
+ return {}
1731
+ }
1732
+
1733
+ /**
1734
+ * Runs relationship model classes.
1735
+ * @this {typeof FrontendModelBase}
1736
+ * @returns {Record<string, typeof FrontendModelBase | string>} - Relationship model classes (or class name strings) keyed by relationship name.
1737
+ */
1738
+ static relationshipModelClasses() {
1739
+ return {}
1740
+ }
1741
+
1742
+ /**
1743
+ * Register a frontend model class so it can be resolved by name in relationship lookups.
1744
+ * @param {typeof FrontendModelBase} modelClass - Model class to register.
1745
+ * @returns {void}
1746
+ */
1747
+ static registerModel(modelClass) {
1748
+ registerFrontendModel(modelClass)
1749
+ }
1750
+
1751
+ /**
1752
+ * Runs define scope.
1753
+ * @param {(...args: Array<?>) => ?} callback - Scope callback.
1754
+ * @returns {((...args: Array<?>) => import("./query.js").default<typeof FrontendModelBase>) & {scope: (...args: Array<?>) => import("../utils/model-scope.js").ModelScopeDescriptor}} - Scope helper.
1755
+ */
1756
+ static defineScope(callback) {
1757
+ return defineModelScope({
1758
+ callback,
1759
+ modelClass: this,
1760
+ startQuery: () => this.query()
1761
+ })
1762
+ }
1763
+
1764
+ /**
1765
+ * Resolve a relationship model class value that may be a class reference or a string name.
1766
+ * @param {typeof FrontendModelBase | string | null | undefined} value - Class or class name.
1767
+ * @returns {typeof FrontendModelBase | null} - Resolved model class.
1768
+ */
1769
+ static resolveModelClass(value) {
1770
+ return resolveFrontendModelClass(value)
1771
+ }
1772
+
1773
+ /**
1774
+ * Runs relationship definitions.
1775
+ * @this {typeof FrontendModelBase}
1776
+ * @returns {Record<string, {type: "belongsTo" | "hasOne" | "hasMany", autoload?: boolean}>} - Relationship definitions keyed by relationship name.
1777
+ */
1778
+ static relationshipDefinitions() {
1779
+ return {}
1780
+ }
1781
+
1782
+ /**
1783
+ * Runs attachment definitions.
1784
+ * @this {typeof FrontendModelBase}
1785
+ * @returns {Record<string, FrontendModelAttachmentDefinition>} - Attachment definitions keyed by attachment name.
1786
+ */
1787
+ static attachmentDefinitions() {
1788
+ return this.resourceConfig().attachments || {}
1789
+ }
1790
+
1791
+ /**
1792
+ * Runs attachment definition.
1793
+ * @this {typeof FrontendModelBase}
1794
+ * @param {string} attachmentName - Attachment name.
1795
+ * @returns {FrontendModelAttachmentDefinition | null} - Attachment definition.
1796
+ */
1797
+ static attachmentDefinition(attachmentName) {
1798
+ return this.attachmentDefinitions()[attachmentName] || null
1799
+ }
1800
+
1801
+ /**
1802
+ * Runs relationship definition.
1803
+ * @this {typeof FrontendModelBase}
1804
+ * @param {string} relationshipName - Relationship name.
1805
+ * @returns {{type: "belongsTo" | "hasOne" | "hasMany", autoload?: boolean} | null} - Relationship definition.
1806
+ */
1807
+ static relationshipDefinition(relationshipName) {
1808
+ const definitions = this.relationshipDefinitions()
1809
+
1810
+ return definitions[relationshipName] || null
1811
+ }
1812
+
1813
+ /**
1814
+ * Runs relationship model class.
1815
+ * @this {typeof FrontendModelBase}
1816
+ * @param {string} relationshipName - Relationship name.
1817
+ * @returns {typeof FrontendModelBase | null} - Target relationship model class.
1818
+ */
1819
+ static relationshipModelClass(relationshipName) {
1820
+ const relationshipModelClasses = this.relationshipModelClasses()
1821
+ const value = relationshipModelClasses[relationshipName]
1822
+
1823
+ return FrontendModelBase.resolveModelClass(value)
1824
+ }
1825
+
1826
+ /**
1827
+ * Runs attributes.
1828
+ * @returns {Record<string, ?>} - Attributes hash.
1829
+ */
1830
+ attributes() {
1831
+ return this._attributes
1832
+ }
1833
+
1834
+ /**
1835
+ * Runs is new record.
1836
+ * @returns {boolean} - Whether this model has not yet been persisted.
1837
+ */
1838
+ isNewRecord() {
1839
+ return this._isNewRecord
1840
+ }
1841
+
1842
+ /**
1843
+ * Runs is persisted.
1844
+ * @returns {boolean} - Whether this model has been persisted.
1845
+ */
1846
+ isPersisted() {
1847
+ return !this.isNewRecord()
1848
+ }
1849
+
1850
+ /**
1851
+ * Runs set is new record.
1852
+ * @param {boolean} newIsNewRecord - New persisted-state flag.
1853
+ * @returns {void}
1854
+ */
1855
+ setIsNewRecord(newIsNewRecord) {
1856
+ this._isNewRecord = newIsNewRecord
1857
+ }
1858
+
1859
+ /**
1860
+ * Marks this record for destruction when its parent is next saved through
1861
+ * nested-attribute support. The record is not removed from the parent's
1862
+ * relationship collection until the server confirms the delete.
1863
+ * @returns {void} - No return value.
1864
+ */
1865
+ markForDestruction() {
1866
+ this._markedForDestruction = true
1867
+ }
1868
+
1869
+ /**
1870
+ * Runs marked for destruction.
1871
+ * @returns {boolean} - Whether this record is queued for nested destruction on next parent save.
1872
+ */
1873
+ markedForDestruction() {
1874
+ return this._markedForDestruction
1875
+ }
1876
+
1877
+ /**
1878
+ * Runs changes.
1879
+ * @returns {Record<string, Array<?>>} - Changed attributes as `[oldValue, newValue]`.
1880
+ */
1881
+ changes() {
1882
+ /**
1883
+ * Changed attributes.
1884
+ @type {Record<string, Array<?>>} */
1885
+ const changedAttributes = {}
1886
+ const attributeNames = new Set([
1887
+ ...Object.keys(this._persistedAttributes),
1888
+ ...Object.keys(this._attributes)
1889
+ ])
1890
+
1891
+ for (const attributeName of attributeNames) {
1892
+ const previousValue = this._persistedAttributes[attributeName]
1893
+ const currentValue = this._attributes[attributeName]
1894
+
1895
+ if (JSON.stringify(serializeFrontendModelTransportValue(previousValue)) !== JSON.stringify(serializeFrontendModelTransportValue(currentValue))) {
1896
+ changedAttributes[attributeName] = [previousValue, currentValue]
1897
+ }
1898
+ }
1899
+
1900
+ return changedAttributes
1901
+ }
1902
+
1903
+ /**
1904
+ * Runs is changed.
1905
+ * @returns {boolean} - Whether any tracked attribute has changed.
1906
+ */
1907
+ isChanged() {
1908
+ return Object.keys(this.changes()).length > 0
1909
+ }
1910
+
1911
+ /**
1912
+ * Runs get relationship by name.
1913
+ * @param {string} relationshipName - Relationship name.
1914
+ * @returns {FrontendModelHasManyRelationship<typeof FrontendModelBase, typeof FrontendModelBase> | FrontendModelSingularRelationship<typeof FrontendModelBase, typeof FrontendModelBase>} - Relationship state object.
1915
+ */
1916
+ getRelationshipByName(relationshipName) {
1917
+ if (!this._relationships[relationshipName]) {
1918
+ const ModelClass = /**
1919
+ * Narrows the runtime value to the documented type.
1920
+ @type {typeof FrontendModelBase} */ (this.constructor)
1921
+ const relationshipDefinition = ModelClass.relationshipDefinition(relationshipName)
1922
+ const targetModelClass = ModelClass.relationshipModelClass(relationshipName)
1923
+
1924
+ if (relationshipDefinition && relationshipTypeIsCollection(relationshipDefinition.type)) {
1925
+ this._relationships[relationshipName] = new FrontendModelHasManyRelationship(this, relationshipName, targetModelClass)
1926
+ } else {
1927
+ this._relationships[relationshipName] = new FrontendModelSingularRelationship(this, relationshipName, targetModelClass)
1928
+ }
1929
+ }
1930
+
1931
+ return this._relationships[relationshipName]
1932
+ }
1933
+
1934
+ /**
1935
+ * Runs get attachment by name.
1936
+ * @param {string} attachmentName - Attachment name.
1937
+ * @returns {FrontendModelAttachmentHandle} - Attachment helper.
1938
+ */
1939
+ getAttachmentByName(attachmentName) {
1940
+ const ModelClass = /**
1941
+ * Narrows the runtime value to the documented type.
1942
+ @type {typeof FrontendModelBase} */ (this.constructor)
1943
+ const attachmentDefinition = ModelClass.attachmentDefinition(attachmentName)
1944
+
1945
+ if (!attachmentDefinition) {
1946
+ throw new Error(`Unknown attachment: ${ModelClass.name}#${attachmentName}`)
1947
+ }
1948
+
1949
+ if (!this._attachments[attachmentName]) {
1950
+ this._attachments[attachmentName] = new FrontendModelAttachmentHandle({
1951
+ attachmentName,
1952
+ model: this
1953
+ })
1954
+ }
1955
+
1956
+ return this._attachments[attachmentName]
1957
+ }
1958
+
1959
+ /**
1960
+ * Runs load relationship.
1961
+ * @param {string} relationshipName - Relationship name.
1962
+ * @returns {Promise<?>} - Loaded relationship value.
1963
+ */
1964
+ async loadRelationship(relationshipName) {
1965
+ const ModelClass = /**
1966
+ * Narrows the runtime value to the documented type.
1967
+ @type {typeof FrontendModelBase} */ (this.constructor)
1968
+ const id = this.primaryKeyValue()
1969
+ const reloadedModel = await ModelClass
1970
+ .preload([relationshipName])
1971
+ .find(id)
1972
+ const loadedValue = reloadedModel.getRelationshipByName(relationshipName).loaded()
1973
+
1974
+ this.getRelationshipByName(relationshipName).setLoaded(loadedValue)
1975
+
1976
+ return loadedValue
1977
+ }
1978
+
1979
+ /**
1980
+ * Preloads relationship(s) onto this already-loaded record. Accepts either a
1981
+ * query built via `Model.preload(...).select(...)` or a raw preload spec
1982
+ * (string / array / nested object). Relationships already preloaded with the
1983
+ * required columns present are left untouched unless `force` is set. Carries
1984
+ * the query's preload graph, select, selectsExtra, withCount, abilities, and
1985
+ * queryData when re-fetching.
1986
+ * @param {import("./query.js").default<typeof FrontendModelBase> | import("../database/query/index.js").NestedPreloadRecord | string | Array<string | import("../database/query/index.js").NestedPreloadRecord>} queryOrSpec - Preload source.
1987
+ * @param {{force?: boolean}} [options] - Options.
1988
+ * @returns {Promise<void>} - Resolves when preloading completes.
1989
+ */
1990
+ async preload(queryOrSpec, options = {}) {
1991
+ await FrontendModelPreloader.preload([this], queryOrSpec, options)
1992
+ }
1993
+
1994
+ /**
1995
+ * Runs relationship or load.
1996
+ * @param {string} relationshipName - Relationship name.
1997
+ * @returns {Promise<?>} - Loaded relationship value.
1998
+ */
1999
+ async relationshipOrLoad(relationshipName) {
2000
+ const relationship = this.getRelationshipByName(relationshipName)
2001
+
2002
+ if (relationship.getPreloaded()) {
2003
+ return relationship.loaded()
2004
+ }
2005
+
2006
+ const batched = await this._tryCohortPreload(relationshipName)
2007
+
2008
+ if (batched) return relationship.loaded()
2009
+
2010
+ return await this.loadRelationship(relationshipName)
2011
+ }
2012
+
2013
+ /**
2014
+ * Attempts to batch-load `relationshipName` across cohort siblings via a
2015
+ * single `preload([name]).where({pk: [ids]}).toArray()` request, then copies
2016
+ * the preloaded relationship state onto each sibling. Returns true when a
2017
+ * batch ran, false when autoload is off, there is no cohort, or no batch
2018
+ * candidates remain. Siblings whose relationship state is already set
2019
+ * (preloaded or locally manipulated via `build` / `setRelationship`) are
2020
+ * skipped so their cached/edited value is preserved.
2021
+ * @param {string} relationshipName - Relationship name.
2022
+ * @returns {Promise<boolean>} - Whether a cohort batch preload ran.
2023
+ */
2024
+ async _tryCohortPreload(relationshipName) {
2025
+ if (!FrontendModelBase.getAutoload()) return false
2026
+
2027
+ const ModelClass = /**
2028
+ * Narrows the runtime value to the documented type.
2029
+ @type {typeof FrontendModelBase} */ (this.constructor)
2030
+ const cohort = this._loadCohort
2031
+
2032
+ if (!cohort || cohort.length <= 1) return false
2033
+
2034
+ const definition = ModelClass.relationshipDefinition(relationshipName)
2035
+
2036
+ if (!definition) return false
2037
+ if (definition.autoload === false) return false
2038
+
2039
+ /**
2040
+ * Batch.
2041
+ @type {Array<FrontendModelBase>} */
2042
+ const batch = []
2043
+
2044
+ // Exact same class, persisted, no existing in-memory relationship state.
2045
+ // `setLoaded` sets `_preloaded = true` on every mutation path (preload,
2046
+ // setRelationship, build, addToLoaded), so `getPreloaded()` alone is a
2047
+ // reliable "already touched" signal on the frontend.
2048
+ for (const sibling of cohort) {
2049
+ if (sibling.constructor !== ModelClass) continue
2050
+ if (sibling.isNewRecord()) continue
2051
+
2052
+ const siblingRelationship = sibling.getRelationshipByName(relationshipName)
2053
+
2054
+ if (siblingRelationship.getPreloaded()) continue
2055
+
2056
+ batch.push(sibling)
2057
+ }
2058
+
2059
+ if (batch.length === 0) return false
2060
+
2061
+ const primaryKey = ModelClass.primaryKey()
2062
+ const batchIds = batch.map((sibling) => sibling.primaryKeyValue())
2063
+ const reloadedBatch = await ModelClass
2064
+ .preload([relationshipName])
2065
+ .where({[primaryKey]: batchIds})
2066
+ .toArray()
2067
+
2068
+ /**
2069
+ * Reloaded by id.
2070
+ @type {Map<string, FrontendModelBase>} */
2071
+ const reloadedById = new Map()
2072
+
2073
+ for (const reloaded of reloadedBatch) {
2074
+ reloadedById.set(String(reloaded.primaryKeyValue()), reloaded)
2075
+ }
2076
+
2077
+ for (const sibling of batch) {
2078
+ const key = String(sibling.primaryKeyValue())
2079
+ const reloaded = reloadedById.get(key)
2080
+
2081
+ if (!reloaded) continue
2082
+
2083
+ const reloadedValue = reloaded.getRelationshipByName(relationshipName).loaded()
2084
+
2085
+ sibling.getRelationshipByName(relationshipName).setLoaded(reloadedValue)
2086
+ }
2087
+
2088
+ // If the caller itself was not populated (record deleted/filtered between
2089
+ // the list fetch and this preload request), fall back to per-record load
2090
+ // so the caller gets a real not-found error instead of a misleading
2091
+ // "hasn't been preloaded" throw from loaded().
2092
+ if (!this.getRelationshipByName(relationshipName).getPreloaded()) return false
2093
+
2094
+ return true
2095
+ }
2096
+
2097
+ /**
2098
+ * Runs set relationship.
2099
+ * @param {string} relationshipName - Relationship name.
2100
+ * @param {?} relationshipValue - Relationship value.
2101
+ * @returns {?} - Assigned relationship value.
2102
+ */
2103
+ setRelationship(relationshipName, relationshipValue) {
2104
+ const ModelClass = /**
2105
+ * Narrows the runtime value to the documented type.
2106
+ @type {typeof FrontendModelBase} */ (this.constructor)
2107
+ const relationshipDefinition = ModelClass.relationshipDefinition(relationshipName)
2108
+
2109
+ if (!relationshipDefinition) {
2110
+ throw new Error(`Unknown relationship: ${ModelClass.name}#${relationshipName}`)
2111
+ }
2112
+
2113
+ if (relationshipTypeIsCollection(relationshipDefinition.type)) {
2114
+ throw new Error(`Cannot set has-many relationship with setRelationship(): ${ModelClass.name}#${relationshipName}`)
2115
+ }
2116
+
2117
+ this.getRelationshipByName(relationshipName).setLoaded(relationshipValue)
2118
+
2119
+ return relationshipValue
2120
+ }
2121
+
2122
+ /**
2123
+ * Runs assign attributes.
2124
+ * @param {Record<string, ?>} attributes - Attributes to assign.
2125
+ * @returns {void} - No return value.
2126
+ */
2127
+ assignAttributes(attributes) {
2128
+ for (const key in attributes) {
2129
+ this.setAttribute(key, attributes[key])
2130
+ }
2131
+ }
2132
+
2133
+ /**
2134
+ * Runs clear relationship cache.
2135
+ * @returns {void} - Clears cached relationship state.
2136
+ */
2137
+ clearRelationshipCache() {
2138
+ this._relationships = {}
2139
+ }
2140
+
2141
+ /**
2142
+ * Runs primary key.
2143
+ * @this {typeof FrontendModelBase}
2144
+ * @returns {string} - Primary key name.
2145
+ */
2146
+ static primaryKey() {
2147
+ return this.resourceConfig().primaryKey || "id"
2148
+ }
2149
+
2150
+ /**
2151
+ * Runs primary key value.
2152
+ * @returns {number | string} - Primary key value.
2153
+ */
2154
+ primaryKeyValue() {
2155
+ const ModelClass = /**
2156
+ * Narrows the runtime value to the documented type.
2157
+ @type {typeof FrontendModelBase} */ (this.constructor)
2158
+ const value = this.readAttribute(ModelClass.primaryKey())
2159
+
2160
+ if (value === undefined || value === null) {
2161
+ throw new Error(`Missing primary key '${ModelClass.primaryKey()}' on ${ModelClass.name}`)
2162
+ }
2163
+
2164
+ return value
2165
+ }
2166
+
2167
+ /**
2168
+ * Runs read attribute.
2169
+ * @param {string} attributeName - Attribute name.
2170
+ * @returns {?} - Attribute value.
2171
+ */
2172
+ readAttribute(attributeName) {
2173
+ if (this._selectedAttributes && !this._selectedAttributes.has(attributeName)) {
2174
+ throw new AttributeNotSelectedError(this.constructor.name, attributeName)
2175
+ }
2176
+
2177
+ return this._attributes[attributeName]
2178
+ }
2179
+
2180
+ /**
2181
+ * Whether an attribute value is currently loaded on this record. Used by the
2182
+ * preloader to decide whether a relationship can be skipped because the
2183
+ * requested columns are already present.
2184
+ * @param {string} attributeName - Attribute name.
2185
+ * @returns {boolean} - Whether the attribute is loaded.
2186
+ */
2187
+ hasLoadedAttribute(attributeName) {
2188
+ if (!this._selectedAttributes) return true
2189
+
2190
+ return this._selectedAttributes.has(attributeName)
2191
+ }
2192
+
2193
+ /**
2194
+ * Read an association count attached by `.withCount(...)`. Counts
2195
+ * live on a dedicated map separate from the record's attributes so
2196
+ * a virtual count like `tasksCount` can't silently shadow a real
2197
+ * column of the same name. Returns the attached value, or 0 when
2198
+ * `.withCount(...)` wasn't requested for this attribute.
2199
+ * @param {string} attributeName - Attribute name, e.g. `"tasksCount"` or a custom name from `.withCount({customName: {...}})`.
2200
+ * @returns {number}
2201
+ */
2202
+ readCount(attributeName) {
2203
+ return readPayloadAssociationCount(/**
2204
+ * Narrows the runtime value to the documented type.
2205
+ @type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
2206
+ * Narrows the runtime value to the documented type.
2207
+ @type {?} */ (this)), attributeName)
2208
+ }
2209
+
2210
+ /**
2211
+ * Internal setter called by `instantiateFromResponse` when hydrating
2212
+ * association counts that rode along with the record payload.
2213
+ * @param {string} attributeName - Attribute name.
2214
+ * @param {number} value - Count value.
2215
+ * @returns {void}
2216
+ */
2217
+ _setAssociationCount(attributeName, value) {
2218
+ setPayloadAssociationCount(/**
2219
+ * Narrows the runtime value to the documented type.
2220
+ @type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
2221
+ * Narrows the runtime value to the documented type.
2222
+ @type {?} */ (this)), attributeName, value)
2223
+ }
2224
+
2225
+ /**
2226
+ * Read a per-record ability result attached by `.abilities(...)`. The
2227
+ * backend evaluates each requested action against the current
2228
+ * ability for this record instance and ships the result alongside
2229
+ * the record's attributes. Returns `false` when the action wasn't
2230
+ * requested (or the ability denied it), so UI code can safely branch
2231
+ * on `record.can("update")` without first checking whether the
2232
+ * ability was loaded.
2233
+ * @param {string} action - Ability action name, e.g. `"update"`.
2234
+ * @returns {boolean}
2235
+ */
2236
+ can(action) {
2237
+ return readPayloadComputedAbility(/**
2238
+ * Narrows the runtime value to the documented type.
2239
+ @type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
2240
+ * Narrows the runtime value to the documented type.
2241
+ @type {?} */ (this)), action)
2242
+ }
2243
+
2244
+ /**
2245
+ * Internal setter called by `instantiateFromResponse` when hydrating
2246
+ * per-record ability results that rode along with the record
2247
+ * payload.
2248
+ * @param {string} action - Ability action name.
2249
+ * @param {boolean} value - Whether the current ability permits the action on this record.
2250
+ * @returns {void}
2251
+ */
2252
+ _setComputedAbility(action, value) {
2253
+ setPayloadComputedAbility(/**
2254
+ * Narrows the runtime value to the documented type.
2255
+ @type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
2256
+ * Narrows the runtime value to the documented type.
2257
+ @type {?} */ (this)), action, value)
2258
+ }
2259
+
2260
+ /**
2261
+ * Read a consumer-defined value attached by `.queryData(...)`. Stored
2262
+ * on a dedicated map rather than `_attributes`, so a virtual alias
2263
+ * like `tasksCount` cannot silently shadow a real column of the same
2264
+ * name. Returns `null` when no registered fn produced that alias for
2265
+ * this record (e.g. no child rows matched the aggregate).
2266
+ * @param {string} name - queryData alias name.
2267
+ * @returns {?}
2268
+ */
2269
+ queryData(name) {
2270
+ return readPayloadQueryData(/**
2271
+ * Narrows the runtime value to the documented type.
2272
+ @type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
2273
+ * Narrows the runtime value to the documented type.
2274
+ @type {?} */ (this)), name)
2275
+ }
2276
+
2277
+ /**
2278
+ * Internal setter used by `instantiateFromResponse` when hydrating
2279
+ * queryData values that rode along with the record payload.
2280
+ * @param {string} name - queryData alias name.
2281
+ * @param {?} value - Attached value.
2282
+ * @returns {void}
2283
+ */
2284
+ _setQueryData(name, value) {
2285
+ setPayloadQueryData(/**
2286
+ * Narrows the runtime value to the documented type.
2287
+ @type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
2288
+ * Narrows the runtime value to the documented type.
2289
+ @type {?} */ (this)), name, value)
2290
+ }
2291
+
2292
+ /**
2293
+ * Runs set attribute.
2294
+ * @param {string} attributeName - Attribute name.
2295
+ * @param {?} newValue - New value.
2296
+ * @returns {?} - Assigned value.
2297
+ */
2298
+ setAttribute(attributeName, newValue) {
2299
+ const previousValue = this._attributes[attributeName]
2300
+
2301
+ this._attributes[attributeName] = newValue
2302
+
2303
+ if (this._selectedAttributes) {
2304
+ this._selectedAttributes.add(attributeName)
2305
+ }
2306
+
2307
+ // Only invalidate relationship cache entries whose foreign key matches the changed attribute.
2308
+ // Blanket-clearing all relationships on any attribute change destroys nested-save state
2309
+ // and preloaded children the caller never asked to invalidate.
2310
+ if (!Object.is(previousValue, newValue)) {
2311
+ this._invalidateRelationshipsForAttribute(attributeName)
2312
+ }
2313
+
2314
+ return newValue
2315
+ }
2316
+
2317
+ /**
2318
+ * Invalidates any cached belongsTo relationship whose foreign key matches the
2319
+ * changed attribute. HasMany / hasOne relationships are left untouched because
2320
+ * their foreign key lives on the child, not on this model, and blanket-clearing
2321
+ * them would destroy nested-save state and preloaded children the caller never
2322
+ * asked to invalidate.
2323
+ *
2324
+ * Foreign keys are inferred when not declared: for belongsTo `projectId` is
2325
+ * inferred from relationship name `project`. Explicit `foreignKey` on the
2326
+ * relationship definition takes precedence.
2327
+ * @param {string} attributeName - Attribute name that changed.
2328
+ * @returns {void}
2329
+ */
2330
+ _invalidateRelationshipsForAttribute(attributeName) {
2331
+ if (!this._relationships || Object.keys(this._relationships).length === 0) return
2332
+
2333
+ const ModelClass = /**
2334
+ * Narrows the runtime value to the documented type.
2335
+ @type {typeof FrontendModelBase} */ (this.constructor)
2336
+ const definitions = typeof ModelClass.relationshipDefinitions === "function" ? ModelClass.relationshipDefinitions() : {}
2337
+
2338
+ for (const relationshipName of Object.keys(this._relationships)) {
2339
+ const definition = /**
2340
+ * Narrows the runtime value to the documented type.
2341
+ @type {?} */ (definitions[relationshipName])
2342
+
2343
+ if (!definition || definition.type !== "belongsTo") continue
2344
+
2345
+ const foreignKey = definition.foreignKey || `${relationshipName}Id`
2346
+
2347
+ if (foreignKey === attributeName) {
2348
+ delete this._relationships[relationshipName]
2349
+ }
2350
+ }
2351
+ }
2352
+
2353
+ /**
2354
+ * Runs resource path.
2355
+ * @this {typeof FrontendModelBase}
2356
+ * @returns {string} - Derived resource path.
2357
+ */
2358
+ static resourcePath() {
2359
+ return validateFrontendModelResourcePath({
2360
+ modelName: this.getModelName(),
2361
+ resourcePath: defaultFrontendModelResourcePath(this)
2362
+ })
2363
+ }
2364
+
2365
+ /**
2366
+ * Runs command name.
2367
+ * @this {typeof FrontendModelBase}
2368
+ * @param {FrontendModelCommandType} commandType - Command type.
2369
+ * @returns {string} - Resolved command name.
2370
+ */
2371
+ static commandName(commandType) {
2372
+ const resourceConfig = this.resourceConfig()
2373
+ const builtInCollectionCommands = resourceConfig.builtInCollectionCommands || []
2374
+ const builtInMemberCommands = resourceConfig.builtInMemberCommands || []
2375
+ const commands = resourceConfig.commands || []
2376
+ const isExposed = builtInCollectionCommands.includes(commandType) || builtInMemberCommands.includes(commandType) || commands.includes(commandType)
2377
+ const commandName = isExposed ? inflection.dasherize(inflection.underscore(commandType)) : commandType
2378
+
2379
+ return validateFrontendModelResourceCommandName({
2380
+ commandName,
2381
+ commandType,
2382
+ modelName: this.getModelName()
2383
+ })
2384
+ }
2385
+
2386
+ /**
2387
+ * Runs normalize custom command payload arguments.
2388
+ * @this {typeof FrontendModelBase}
2389
+ * @param {Array<?>} args - Command arguments.
2390
+ * @returns {Record<string, ?>} - Command payload.
2391
+ */
2392
+ static normalizeCustomCommandPayloadArguments(args) {
2393
+ if (args.length === 0) return {}
2394
+ if (args.length === 1) {
2395
+ const payload = args[0]
2396
+ if (payload === undefined) {
2397
+ return {}
2398
+ }
2399
+
2400
+ if (typeof payload !== "object" || payload === null) {
2401
+ return {arg1: payload}
2402
+ }
2403
+
2404
+ return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (payload)
2405
+ }
2406
+
2407
+ /**
2408
+ * Payload.
2409
+ @type {Record<string, number | string | Array<?>>} */
2410
+ const payload = {}
2411
+
2412
+ for (let index = 0; index < args.length; index += 1) {
2413
+ payload[`arg${index + 1}`] = args[index]
2414
+ }
2415
+
2416
+ return payload
2417
+ }
2418
+
2419
+ /**
2420
+ * Returns the model name, preferring an explicit `static modelName` declaration
2421
+ * over the JavaScript class `.name` property. This allows minified builds to
2422
+ * preserve correct model names without relying on `keep_classnames`.
2423
+ * @this {typeof FrontendModelBase}
2424
+ * @returns {string} - The model name.
2425
+ */
2426
+ static getModelName() {
2427
+ const resourceConfig = typeof this.resourceConfig === "function" ? this.resourceConfig() : null
2428
+ const modelName = resourceConfig?.modelName
2429
+
2430
+ return (typeof modelName === "string" && modelName.length > 0) ? modelName : this.name
2431
+ }
2432
+
2433
+ /**
2434
+ * Runs configure transport.
2435
+ * @param {FrontendModelTransportConfig} config - Frontend model transport configuration.
2436
+ * @returns {void} - No return value.
2437
+ */
2438
+ static configureTransport(config) {
2439
+ if (!config || typeof config !== "object") {
2440
+ return
2441
+ }
2442
+
2443
+ if (Object.prototype.hasOwnProperty.call(config, "url")) {
2444
+ frontendModelTransportConfig.url = config.url
2445
+ }
2446
+
2447
+ if (Object.prototype.hasOwnProperty.call(config, "shared")) {
2448
+ frontendModelTransportConfig.shared = config.shared
2449
+ }
2450
+
2451
+ if (Object.prototype.hasOwnProperty.call(config, "websocketClient")) {
2452
+ frontendModelTransportConfig.websocketClient = config.websocketClient
2453
+ }
2454
+
2455
+ if (Object.prototype.hasOwnProperty.call(config, "websocketUrl")) {
2456
+ frontendModelTransportConfig.websocketUrl = config.websocketUrl
2457
+ // Reset cached internal client so the new URL takes effect on next subscribe
2458
+ internalWebsocketClient = null
2459
+ }
2460
+
2461
+ if (Object.prototype.hasOwnProperty.call(config, "requestHeaders")) {
2462
+ frontendModelTransportConfig.requestHeaders = config.requestHeaders
2463
+ }
2464
+
2465
+ if (Object.prototype.hasOwnProperty.call(config, "sessionStore")) {
2466
+ frontendModelTransportConfig.sessionStore = config.sessionStore
2467
+ // Reset cached internal client so the new sessionStore is picked up.
2468
+ internalWebsocketClient = null
2469
+ }
2470
+ }
2471
+
2472
+ /**
2473
+ * Connect the internal WebSocket and enable auto-reconnect.
2474
+ * @returns {Promise<void>} - Resolves when connected.
2475
+ */
2476
+ static async connectWebsocket() {
2477
+ const client = resolveInternalWebsocketClient()
2478
+
2479
+ if (!client) {
2480
+ throw new Error("connectWebsocket requires configureTransport({websocketUrl})")
2481
+ }
2482
+
2483
+ await client.connect()
2484
+ }
2485
+
2486
+ /**
2487
+ * Disconnect the internal WebSocket and disable auto-reconnect.
2488
+ * @returns {Promise<void>} - Resolves when closed.
2489
+ */
2490
+ static async disconnectWebsocket() {
2491
+ if (!internalWebsocketClient) return
2492
+
2493
+ await internalWebsocketClient.disconnectAndStopReconnect()
2494
+ }
2495
+
2496
+ /**
2497
+ * Waits until queued and active frontend-model transport requests finish.
2498
+ * @param {FrontendModelIdleWaitArgs} [args] - Wait options.
2499
+ * @returns {Promise<void>} - Resolves when transport is idle.
2500
+ */
2501
+ static async waitForIdle(args = {}) {
2502
+ const {quietMs = 0, timeout: timeoutMs = 5000, ...restArgs} = args
2503
+ const restArgKeys = Object.keys(restArgs)
2504
+
2505
+ if (restArgKeys.length > 0) {
2506
+ throw new Error(`Unknown waitForIdle args: ${restArgKeys.join(", ")}`)
2507
+ }
2508
+
2509
+ if (!Number.isFinite(quietMs) || quietMs < 0) {
2510
+ throw new Error(`Expected waitForIdle quietMs to be a non-negative number, got: ${quietMs}`)
2511
+ }
2512
+
2513
+ await timeout(
2514
+ {timeout: timeoutMs, errorMessage: "Timed out waiting for frontend model transport to become idle"},
2515
+ async () => await waitForFrontendModelTransportIdle(quietMs)
2516
+ )
2517
+ }
2518
+
2519
+ /**
2520
+ * Returns the current WebSocket connection state.
2521
+ * @returns {{disconnectedSince: number | null, hasClient: boolean, isOpen: boolean, listenerCount: number}} - Snapshot of the managed websocket connection state.
2522
+ */
2523
+ static websocketState() {
2524
+ if (!internalWebsocketClient) {
2525
+ return {disconnectedSince: null, hasClient: false, isOpen: false, listenerCount: 0}
2526
+ }
2527
+
2528
+ return {
2529
+ ...internalWebsocketClient.state(),
2530
+ hasClient: true
2531
+ }
2532
+ }
2533
+
2534
+ /**
2535
+ * Close the raw WebSocket without disabling auto-reconnect. Used by tests to
2536
+ * simulate an unexpected network drop and verify reconnection behavior.
2537
+ * @returns {Promise<void>} - Resolves when the socket has closed.
2538
+ */
2539
+ static async dropWebsocket() {
2540
+ if (!internalWebsocketClient) return
2541
+
2542
+ await internalWebsocketClient.dropConnection()
2543
+ }
2544
+
2545
+ /**
2546
+ * Sets global metadata on the WebSocket connection. Sent to the server immediately
2547
+ * over WebSocket and exposed to WebSocket-borne requests as request metadata.
2548
+ * @param {string} key - Metadata key.
2549
+ * @param {?} value - Metadata value (null to clear).
2550
+ * @returns {void}
2551
+ */
2552
+ static setWebsocketMetadata(key, value) {
2553
+ const client = /**
2554
+ * Narrows the runtime value to the documented type.
2555
+ @type {?} */ (frontendModelTransportConfig.websocketClient || resolveInternalWebsocketClient())
2556
+
2557
+ if (!client || typeof client.setMetadata !== "function") return
2558
+
2559
+ client.setMetadata(key, value)
2560
+ }
2561
+
2562
+ /**
2563
+ * Opens a managed connection that auto-opens, auto-closes, and
2564
+ * auto-reconnects based on `shouldConnect()` and `params()`.
2565
+ * Call `handle.sync()` whenever the inputs that drive those
2566
+ * functions change (e.g. current-user sign-in/out). The handle
2567
+ * retries when the WS client isn't ready and reopens on close.
2568
+ * @param {string} connectionType - Connection class name registered on the server.
2569
+ * @param {{shouldConnect: () => boolean, params: () => Record<string, ?>, onMessage?: (body: ?) => void}} options - Connection lifecycle and payload callbacks.
2570
+ * @returns {{sync: () => void, close: () => void}} - Handle used to resync or close the managed connection.
2571
+ */
2572
+ static openManagedConnection(connectionType, options) {
2573
+ /**
2574
+ * Connection.
2575
+ @type {?} */
2576
+ let connection = null
2577
+ let closed = false
2578
+ /**
2579
+ * Retry timer.
2580
+ @type {ReturnType<typeof setTimeout> | null} */
2581
+ let retryTimer = null
2582
+ let lastParamsJson = ""
2583
+
2584
+ const sync = () => {
2585
+ if (closed) return
2586
+
2587
+ if (!options.shouldConnect()) {
2588
+ if (connection && !connection.isClosed()) connection.close()
2589
+ connection = null
2590
+ lastParamsJson = ""
2591
+ return
2592
+ }
2593
+
2594
+ const nextParams = options.params()
2595
+ const nextParamsJson = JSON.stringify(nextParams)
2596
+
2597
+ // Already connected with same params — nothing to do.
2598
+ if (connection && !connection.isClosed() && nextParamsJson === lastParamsJson) return
2599
+
2600
+ // Connected but params changed — send update message.
2601
+ // Guard with try/catch: the connection handle stays live during
2602
+ // reconnect but the underlying socket may be closed.
2603
+ if (connection && !connection.isClosed()) {
2604
+ try {
2605
+ connection.sendMessage(nextParams)
2606
+ lastParamsJson = nextParamsJson
2607
+ return
2608
+ } catch {
2609
+ connection = null
2610
+ lastParamsJson = ""
2611
+ }
2612
+ }
2613
+
2614
+ // WS client not ready — retry. Check the actual client (which
2615
+ // may be an injected websocketClient) instead of websocketState()
2616
+ // which only reflects the internal client.
2617
+ const client = /**
2618
+ * Narrows the runtime value to the documented type.
2619
+ @type {?} */ (frontendModelTransportConfig.websocketClient || resolveInternalWebsocketClient())
2620
+
2621
+ if (!client || !client.isOpen()) {
2622
+ if (retryTimer === null) {
2623
+ retryTimer = globalThis.setTimeout(() => {
2624
+ retryTimer = null
2625
+ sync()
2626
+ }, 250)
2627
+ }
2628
+ return
2629
+ }
2630
+
2631
+ lastParamsJson = nextParamsJson
2632
+ connection = FrontendModelBase.openWebsocketConnection(connectionType, {
2633
+ params: nextParams,
2634
+ onMessage: options.onMessage,
2635
+ onClose: () => {
2636
+ if (connection?.isClosed()) {
2637
+ connection = null
2638
+ lastParamsJson = ""
2639
+ sync()
2640
+ }
2641
+ }
2642
+ })
2643
+ }
2644
+
2645
+ const close = () => {
2646
+ closed = true
2647
+ if (retryTimer !== null) globalThis.clearTimeout(retryTimer)
2648
+ if (connection && !connection.isClosed()) connection.close()
2649
+ connection = null
2650
+ }
2651
+
2652
+ sync()
2653
+
2654
+ return {sync, close}
2655
+ }
2656
+
2657
+ /**
2658
+ * Opens a 1:1 `WebsocketConnection` of the given type. Thin
2659
+ * convenience wrapper around the internal WS client's
2660
+ * `openConnection`. Apps use this for per-session state/messaging
2661
+ * that doesn't fit the pub/sub Channel model (locale, presence).
2662
+ * @param {string} connectionType - Name the server registered the class under.
2663
+ * @param {{params?: Record<string, ?>, onConnect?: () => void, onMessage?: (body: ?) => void, onDisconnect?: () => void, onResume?: () => void, onClose?: (reason: string) => void}} [options] - Connection options and event handlers.
2664
+ * @returns {?} - VelociousWebsocketClientConnection handle (typed loosely to avoid a cross-module import cycle).
2665
+ */
2666
+ static openWebsocketConnection(connectionType, options) {
2667
+ const client = /**
2668
+ * Narrows the runtime value to the documented type.
2669
+ @type {?} */ (frontendModelTransportConfig.websocketClient || resolveInternalWebsocketClient())
2670
+
2671
+ if (!client || typeof client.openConnection !== "function") {
2672
+ throw new Error("openWebsocketConnection requires configureTransport({websocketUrl})")
2673
+ }
2674
+
2675
+ return client.openConnection(connectionType, options || {})
2676
+ }
2677
+
2678
+ /**
2679
+ * Subscribes to a pub/sub `WebsocketChannel`. Thin wrapper around
2680
+ * the internal client's `subscribeChannel`.
2681
+ * @param {string} channelType - Channel class name registered on the server.
2682
+ * @param {{params?: Record<string, ?>, onMessage?: (body: ?) => void, onDisconnect?: () => void, onResume?: () => void, onClose?: (reason: string) => void}} [options] - Channel subscription options and event handlers.
2683
+ * @returns {?} - Websocket channel handle from the configured client.
2684
+ */
2685
+ static subscribeWebsocketChannel(channelType, options) {
2686
+ const client = /**
2687
+ * Narrows the runtime value to the documented type.
2688
+ @type {?} */ (frontendModelTransportConfig.websocketClient || resolveInternalWebsocketClient())
2689
+
2690
+ if (!client || typeof client.subscribeChannel !== "function") {
2691
+ throw new Error("subscribeWebsocketChannel requires configureTransport({websocketUrl})")
2692
+ }
2693
+
2694
+ return client.subscribeChannel(channelType, options || {})
2695
+ }
2696
+
2697
+ /**
2698
+ * Installs WebSocket lifecycle hooks on globalThis for system test access.
2699
+ * Tests can call `globalThis.__velocious_websocket_hooks.connect()` etc.
2700
+ * @returns {void}
2701
+ */
2702
+ static installWebsocketTestHooks() {
2703
+ if (typeof globalThis === "undefined") return
2704
+
2705
+ /** Narrows the runtime value to the documented type. @type {?} */ (globalThis).__velocious_websocket_hooks = {
2706
+ connect: () => this.connectWebsocket(),
2707
+ disconnect: () => this.disconnectWebsocket(),
2708
+ drop: () => this.dropWebsocket(),
2709
+ state: () => this.websocketState()
2710
+ }
2711
+ }
2712
+
2713
+ /**
2714
+ * Runs attributes from response.
2715
+ * @this {typeof FrontendModelBase}
2716
+ * @param {object} response - Response payload.
2717
+ * @returns {Record<string, ?>} - Attributes from payload.
2718
+ */
2719
+ static attributesFromResponse(response) {
2720
+ const modelData = this.modelDataFromResponse(response)
2721
+
2722
+ return modelData.attributes
2723
+ }
2724
+
2725
+ /**
2726
+ * Runs model data from response.
2727
+ * @this {typeof FrontendModelBase}
2728
+ * @param {object} response - Response payload.
2729
+ * @returns {{abilities: Record<string, boolean>, attributes: Record<string, ?>, associationCounts: Record<string, number>, queryData: Record<string, ?>, preloadedRelationships: Record<string, ?>, selectedAttributes: Set<string>}} - Attributes, preloaded relationships, association counts, queryData, abilities, and the selected-attributes set.
2730
+ */
2731
+ static modelDataFromResponse(response) {
2732
+ if (!response || typeof response !== "object") {
2733
+ throw new Error(`Expected object response but got: ${response}`)
2734
+ }
2735
+
2736
+ const responseObject = /**
2737
+ * Narrows the runtime value to the documented type.
2738
+ @type {Record<string, ?>} */ (response)
2739
+
2740
+ /**
2741
+ * Defines modelData.
2742
+ @type {Record<string, ?>} */
2743
+ let modelData
2744
+
2745
+ if (responseObject.model && typeof responseObject.model === "object") {
2746
+ modelData = /**
2747
+ * Narrows the runtime value to the documented type.
2748
+ @type {Record<string, ?>} */ (responseObject.model)
2749
+ } else if (responseObject.attributes && typeof responseObject.attributes === "object") {
2750
+ modelData = /**
2751
+ * Narrows the runtime value to the documented type.
2752
+ @type {Record<string, ?>} */ (responseObject.attributes)
2753
+ } else {
2754
+ modelData = responseObject
2755
+ }
2756
+
2757
+ const attributes = {...modelData}
2758
+ const preloadedRelationships = isPlainObject(attributes[PRELOADED_RELATIONSHIPS_KEY])
2759
+ ? /**
2760
+ * Narrows the runtime value to the documented type.
2761
+ @type {Record<string, ?>} */ (attributes[PRELOADED_RELATIONSHIPS_KEY])
2762
+ : {}
2763
+ const associationCounts = isPlainObject(attributes[ASSOCIATION_COUNTS_KEY])
2764
+ ? /**
2765
+ * Narrows the runtime value to the documented type.
2766
+ @type {Record<string, number>} */ (attributes[ASSOCIATION_COUNTS_KEY])
2767
+ : {}
2768
+ const queryData = isPlainObject(attributes[QUERY_DATA_KEY])
2769
+ ? /**
2770
+ * Narrows the runtime value to the documented type.
2771
+ @type {Record<string, ?>} */ (attributes[QUERY_DATA_KEY])
2772
+ : {}
2773
+ const abilities = isPlainObject(attributes[ABILITIES_KEY])
2774
+ ? /**
2775
+ * Narrows the runtime value to the documented type.
2776
+ @type {Record<string, boolean>} */ (attributes[ABILITIES_KEY])
2777
+ : {}
2778
+ const selectedAttributesFromPayload = Array.isArray(attributes[SELECTED_ATTRIBUTES_KEY])
2779
+ ? new Set(/**
2780
+ * Narrows the runtime value to the documented type.
2781
+ @type {string[]} */ (attributes[SELECTED_ATTRIBUTES_KEY]).filter((attributeName) => typeof attributeName === "string"))
2782
+ : null
2783
+
2784
+ delete attributes[PRELOADED_RELATIONSHIPS_KEY]
2785
+ delete attributes[SELECTED_ATTRIBUTES_KEY]
2786
+ delete attributes[ASSOCIATION_COUNTS_KEY]
2787
+ delete attributes[QUERY_DATA_KEY]
2788
+ delete attributes[ABILITIES_KEY]
2789
+
2790
+ const selectedAttributes = selectedAttributesFromPayload || new Set(Object.keys(attributes))
2791
+
2792
+ return {abilities, attributes, associationCounts, queryData, preloadedRelationships, selectedAttributes}
2793
+ }
2794
+
2795
+ /**
2796
+ * Runs apply preloaded relationships.
2797
+ * @this {typeof FrontendModelBase}
2798
+ * @param {FrontendModelBase} model - Model instance.
2799
+ * @param {Record<string, ?>} preloadedRelationships - Preloaded relationship payload.
2800
+ * @returns {void}
2801
+ */
2802
+ static applyPreloadedRelationships(model, preloadedRelationships) {
2803
+ for (const [relationshipName, relationshipPayload] of Object.entries(preloadedRelationships)) {
2804
+ const relationship = model.getRelationshipByName(relationshipName)
2805
+ const targetModelClass = this.relationshipModelClass(relationshipName)
2806
+
2807
+ if (Array.isArray(relationshipPayload)) {
2808
+ relationship.setLoaded(relationshipPayload.map((entry) => this.instantiateRelationshipValue(entry, targetModelClass)))
2809
+ continue
2810
+ }
2811
+
2812
+ relationship.setLoaded(this.instantiateRelationshipValue(relationshipPayload, targetModelClass))
2813
+ }
2814
+ }
2815
+
2816
+ /**
2817
+ * Runs instantiate relationship value.
2818
+ * @this {typeof FrontendModelBase}
2819
+ * @param {?} relationshipPayload - Relationship payload value.
2820
+ * @param {typeof FrontendModelBase | null} targetModelClass - Target model class.
2821
+ * @returns {?} - Instantiated relationship value.
2822
+ */
2823
+ static instantiateRelationshipValue(relationshipPayload, targetModelClass) {
2824
+ if (!targetModelClass) return relationshipPayload
2825
+
2826
+ if (!relationshipPayload || typeof relationshipPayload !== "object") return relationshipPayload
2827
+
2828
+ return targetModelClass.instantiateFromResponse(relationshipPayload)
2829
+ }
2830
+
2831
+ /**
2832
+ * Runs instantiate from response.
2833
+ * @template {typeof FrontendModelBase} T
2834
+ * @this {T}
2835
+ * @param {Record<string, ?> | InstanceType<T>} response - Response payload, or an already-hydrated instance of this class.
2836
+ * @returns {InstanceType<T>} - New model instance, or the same instance unchanged if it was already hydrated.
2837
+ */
2838
+ static instantiateFromResponse(response) {
2839
+ // Idempotent: if a caller hands us an already-hydrated instance of this
2840
+ // class (now common because the shared frontend-model API auto-serializes
2841
+ // backend `Record` instances returned from custom commands and the
2842
+ // transport deserializer hydrates them into models before the call site
2843
+ // sees the response), return it as-is. Without this, code that has
2844
+ // historically wrapped custom-command responses in
2845
+ // `Model.instantiateFromResponse(response.field)` would spread the live
2846
+ // model instance into a new constructor call and produce a broken model
2847
+ // with internal state keys promoted to attributes.
2848
+ if (response instanceof this) {
2849
+ return /** Narrows the runtime value to the documented type. @type {InstanceType<T>} */ (response)
2850
+ }
2851
+
2852
+ const modelData = this.modelDataFromResponse(response)
2853
+ const attributes = modelData.attributes
2854
+ const preloadedRelationships = modelData.preloadedRelationships
2855
+ const associationCounts = modelData.associationCounts
2856
+ const queryData = modelData.queryData
2857
+ const abilities = modelData.abilities
2858
+ const selectedAttributes = modelData.selectedAttributes
2859
+ const model = /**
2860
+ * Narrows the runtime value to the documented type.
2861
+ @type {InstanceType<T>} */ (new this(attributes))
2862
+ model._selectedAttributes = selectedAttributes ? new Set(selectedAttributes) : null
2863
+
2864
+ this.applyPreloadedRelationships(model, preloadedRelationships)
2865
+
2866
+ for (const [attributeName, value] of Object.entries(associationCounts || {})) {
2867
+ model._setAssociationCount(attributeName, Number(value) || 0)
2868
+ }
2869
+
2870
+ for (const [name, value] of Object.entries(queryData || {})) {
2871
+ model._setQueryData(name, value)
2872
+ }
2873
+
2874
+ for (const [action, value] of Object.entries(abilities || {})) {
2875
+ model._setComputedAbility(action, Boolean(value))
2876
+ }
2877
+
2878
+ model.setIsNewRecord(false)
2879
+ model._persistedAttributes = cloneFrontendModelAttributes(model.attributes())
2880
+
2881
+ return model
2882
+ }
2883
+
2884
+ /**
2885
+ * Runs find.
2886
+ * @template {typeof FrontendModelBase} T
2887
+ * @this {T}
2888
+ * @param {number | string} id - Record identifier.
2889
+ * @returns {Promise<InstanceType<T>>} - Resolved model.
2890
+ */
2891
+ static async find(id) {
2892
+ return await this.query().find(id)
2893
+ }
2894
+
2895
+ /**
2896
+ * Runs find by.
2897
+ * @template {typeof FrontendModelBase} T
2898
+ * @this {T}
2899
+ * @param {Record<string, ?>} conditions - Attribute match conditions.
2900
+ * @returns {Promise<InstanceType<T> | null>} - Found model or null.
2901
+ */
2902
+ static async findBy(conditions) {
2903
+ return await this.query().findBy(conditions)
2904
+ }
2905
+
2906
+ /**
2907
+ * Runs find by or fail.
2908
+ * @template {typeof FrontendModelBase} T
2909
+ * @this {T}
2910
+ * @param {Record<string, ?>} conditions - Attribute match conditions.
2911
+ * @returns {Promise<InstanceType<T>>} - Found model.
2912
+ */
2913
+ static async findByOrFail(conditions) {
2914
+ return await this.query().findByOrFail(conditions)
2915
+ }
2916
+
2917
+ /**
2918
+ * Runs to array.
2919
+ * @template {typeof FrontendModelBase} T
2920
+ * @this {T}
2921
+ * @returns {Promise<InstanceType<T>[]>} - Loaded model instances.
2922
+ */
2923
+ static async toArray() {
2924
+ return await this.query().toArray()
2925
+ }
2926
+
2927
+ /**
2928
+ * Runs load.
2929
+ * @template {typeof FrontendModelBase} T
2930
+ * @this {T}
2931
+ * @returns {Promise<InstanceType<T>[]>} - Loaded model instances.
2932
+ */
2933
+ static async load() {
2934
+ return await this.query().load()
2935
+ }
2936
+
2937
+ /**
2938
+ * Runs all.
2939
+ * @template {typeof FrontendModelBase} T
2940
+ * @this {T}
2941
+ * @returns {FrontendModelQuery<T>} - Query builder.
2942
+ */
2943
+ static all() {
2944
+ return this.query()
2945
+ }
2946
+
2947
+ /**
2948
+ * Runs where.
2949
+ * @template {typeof FrontendModelBase} T
2950
+ * @this {T}
2951
+ * @param {Record<string, ?>} conditions - Root-model where conditions.
2952
+ * @returns {import("./query.js").default<T>} - Query with where conditions.
2953
+ */
2954
+ static where(conditions) {
2955
+ return this.query().where(conditions)
2956
+ }
2957
+
2958
+ /**
2959
+ * Runs joins.
2960
+ * @template {typeof FrontendModelBase} T
2961
+ * @this {T}
2962
+ * @param {Record<string, ?> | Array<Record<string, ?>>} joins - Relationship descriptor joins.
2963
+ * @returns {import("./query.js").default<T>} - Query with joins.
2964
+ */
2965
+ static joins(joins) {
2966
+ return this.query().joins(joins)
2967
+ }
2968
+
2969
+ /**
2970
+ * Runs limit.
2971
+ * @template {typeof FrontendModelBase} T
2972
+ * @this {T}
2973
+ * @param {number} value - Maximum number of records.
2974
+ * @returns {import("./query.js").default<T>} - Query with limit.
2975
+ */
2976
+ static limit(value) {
2977
+ return this.query().limit(value)
2978
+ }
2979
+
2980
+ /**
2981
+ * Runs offset.
2982
+ * @template {typeof FrontendModelBase} T
2983
+ * @this {T}
2984
+ * @param {number} value - Number of records to skip.
2985
+ * @returns {import("./query.js").default<T>} - Query with offset.
2986
+ */
2987
+ static offset(value) {
2988
+ return this.query().offset(value)
2989
+ }
2990
+
2991
+ /**
2992
+ * Runs page.
2993
+ * @template {typeof FrontendModelBase} T
2994
+ * @this {T}
2995
+ * @param {number} pageNumber - 1-based page number.
2996
+ * @returns {import("./query.js").default<T>} - Query with page applied.
2997
+ */
2998
+ static page(pageNumber) {
2999
+ return this.query().page(pageNumber)
3000
+ }
3001
+
3002
+ /**
3003
+ * Runs per page.
3004
+ * @template {typeof FrontendModelBase} T
3005
+ * @this {T}
3006
+ * @param {number} value - Number of records per page.
3007
+ * @returns {import("./query.js").default<T>} - Query with page size.
3008
+ */
3009
+ static perPage(value) {
3010
+ return this.query().perPage(value)
3011
+ }
3012
+
3013
+ /**
3014
+ * Runs count.
3015
+ * @template {typeof FrontendModelBase} T
3016
+ * @this {T}
3017
+ * @returns {Promise<number>} - Number of loaded model instances.
3018
+ */
3019
+ static async count() {
3020
+ return await this.query().count()
3021
+ }
3022
+
3023
+ /**
3024
+ * Class-level hook fired when any record of this model is created.
3025
+ * Subscribe-time authorization only — once a subscription is
3026
+ * accepted, future `create` events for this model are delivered
3027
+ * without re-checking per-record visibility. Query options can still
3028
+ * narrow which events reach this callback.
3029
+ * @this {typeof FrontendModelBase}
3030
+ * @param {(payload: {id: string, model: InstanceType<typeof FrontendModelBase>}) => void} callback - Event callback.
3031
+ * @param {import("./query.js").FrontendModelEventOptions} [options] - Event query or record projection options.
3032
+ * @returns {Promise<() => void>} - Unsubscribe callback.
3033
+ */
3034
+ static async onCreate(callback, options = {}) {
3035
+ const sub = ensureFrontendModelEventSubscription(this)
3036
+ const entry = {callback, ...frontendModelEventOptionsPayload(this, options)}
3037
+
3038
+ sub.classCreateCallbacks.add(entry)
3039
+ await sub.ensureSubscribed()
3040
+
3041
+ return () => {
3042
+ sub.classCreateCallbacks.delete(entry)
3043
+ sub.maybeTeardown()
3044
+ }
3045
+ }
3046
+
3047
+ /**
3048
+ * Class-level hook fired when any record of this model is updated.
3049
+ * @this {typeof FrontendModelBase}
3050
+ * @param {(payload: {id: string, model: InstanceType<typeof FrontendModelBase>}) => void} callback - Event callback.
3051
+ * @param {import("./query.js").FrontendModelEventOptions} [options] - Event query or record projection options.
3052
+ * @returns {Promise<() => void>} - Unsubscribe callback.
3053
+ */
3054
+ static async onUpdate(callback, options = {}) {
3055
+ const sub = ensureFrontendModelEventSubscription(this)
3056
+ const entry = {callback, ...frontendModelEventOptionsPayload(this, options)}
3057
+
3058
+ sub.classUpdateCallbacks.add(entry)
3059
+ await sub.ensureSubscribed()
3060
+
3061
+ return () => {
3062
+ sub.classUpdateCallbacks.delete(entry)
3063
+ sub.maybeTeardown()
3064
+ }
3065
+ }
3066
+
3067
+ /**
3068
+ * Class-level hook fired when any record of this model is destroyed.
3069
+ * @this {typeof FrontendModelBase}
3070
+ * @param {(payload: {id: string}) => void} callback - Event callback.
3071
+ * @param {import("./query.js").FrontendModelEventOptions} [options] - Accepted for API symmetry; destroy events carry ids only.
3072
+ * @returns {Promise<() => void>} - Unsubscribe callback.
3073
+ */
3074
+ static async onDestroy(callback, options = {}) {
3075
+ assertNoDestroyEventFilter(this, options)
3076
+
3077
+ const sub = ensureFrontendModelEventSubscription(this)
3078
+ const entry = {callback}
3079
+
3080
+ sub.classDestroyCallbacks.add(entry)
3081
+ await sub.ensureSubscribed()
3082
+
3083
+ return () => {
3084
+ sub.classDestroyCallbacks.delete(entry)
3085
+ sub.maybeTeardown()
3086
+ }
3087
+ }
3088
+
3089
+ /**
3090
+ * Instance-level hook fired when THIS record is updated. The
3091
+ * instance's attributes are auto-merged with the broadcast payload
3092
+ * before the callback runs, so callers can read fresh values via
3093
+ * `this.someAttr()` without re-fetching.
3094
+ * @param {(payload: {id: string, model: InstanceType<typeof FrontendModelBase>}) => void} callback - Event callback.
3095
+ * @param {import("./query.js").FrontendModelEventOptions} [options] - Event query or record projection options.
3096
+ * @returns {Promise<() => void>} - Unsubscribe callback.
3097
+ */
3098
+ async onUpdate(callback, options = {}) {
3099
+ const self = /**
3100
+ * Narrows the runtime value to the documented type.
3101
+ @type {?} */ (this)
3102
+ const ModelClass = /**
3103
+ * Narrows the runtime value to the documented type.
3104
+ @type {typeof FrontendModelBase} */ (this.constructor)
3105
+ const sub = ensureFrontendModelEventSubscription(ModelClass)
3106
+ const id = String(self.id())
3107
+ const entry = {callback, ...frontendModelEventOptionsPayload(ModelClass, options)}
3108
+ const listener = ensureFrontendModelInstanceListener(sub, id, this)
3109
+
3110
+ listener.updateCallbacks.add(entry)
3111
+ await sub.ensureSubscribed()
3112
+
3113
+ return () => {
3114
+ const current = sub.instanceListeners.get(id)
3115
+
3116
+ if (!current) return
3117
+ current.updateCallbacks.delete(entry)
3118
+
3119
+ if (current.updateCallbacks.size === 0 && current.destroyCallbacks.size === 0) {
3120
+ sub.instanceListeners.delete(id)
3121
+ }
3122
+ sub.maybeTeardown()
3123
+ }
3124
+ }
3125
+
3126
+ /**
3127
+ * Instance-level hook fired when THIS record is destroyed.
3128
+ * @param {(payload: {id: string}) => void} callback - Event callback.
3129
+ * @param {import("./query.js").FrontendModelEventOptions} [options] - Accepted for API symmetry; destroy events carry ids only.
3130
+ * @returns {Promise<() => void>} - Unsubscribe callback.
3131
+ */
3132
+ async onDestroy(callback, options = {}) {
3133
+ const self = /**
3134
+ * Narrows the runtime value to the documented type.
3135
+ @type {?} */ (this)
3136
+ const ModelClass = /**
3137
+ * Narrows the runtime value to the documented type.
3138
+ @type {typeof FrontendModelBase} */ (this.constructor)
3139
+
3140
+ assertNoDestroyEventFilter(ModelClass, options)
3141
+
3142
+ const sub = ensureFrontendModelEventSubscription(ModelClass)
3143
+ const id = String(self.id())
3144
+ const entry = {callback}
3145
+ const listener = ensureFrontendModelInstanceListener(sub, id, this)
3146
+
3147
+ listener.destroyCallbacks.add(entry)
3148
+ await sub.ensureSubscribed()
3149
+
3150
+ return () => {
3151
+ const current = sub.instanceListeners.get(id)
3152
+
3153
+ if (!current) return
3154
+ current.destroyCallbacks.delete(entry)
3155
+
3156
+ if (current.updateCallbacks.size === 0 && current.destroyCallbacks.size === 0) {
3157
+ sub.instanceListeners.delete(id)
3158
+ }
3159
+ sub.maybeTeardown()
3160
+ }
3161
+ }
3162
+
3163
+ /**
3164
+ * Runs pluck.
3165
+ * @template {typeof FrontendModelBase} T
3166
+ * @this {T}
3167
+ * @param {...(string | string[] | Record<string, ?> | Array<Record<string, ?>>)} columns - Pluck definition(s).
3168
+ * @returns {Promise<Array<?>>} - Plucked values.
3169
+ */
3170
+ static async pluck(...columns) {
3171
+ return await this.query().pluck(...columns)
3172
+ }
3173
+
3174
+ /**
3175
+ * Runs search.
3176
+ * @template {typeof FrontendModelBase} T
3177
+ * @this {T}
3178
+ * @param {string[]} path - Relationship path.
3179
+ * @param {string} column - Column or attribute name.
3180
+ * @param {"eq" | "like" | "notEq" | "gt" | "gteq" | "lt" | "lteq" | ">" | ">=" | "<" | "<="} operator - Search operator.
3181
+ * @param {?} value - Search value.
3182
+ * @returns {FrontendModelQuery<T>} - Query builder with search filter.
3183
+ */
3184
+ static search(path, column, operator, value) {
3185
+ return this.query().search(path, column, operator, value)
3186
+ }
3187
+
3188
+ /**
3189
+ * Runs ransack.
3190
+ * @template {typeof FrontendModelBase} T
3191
+ * @this {T}
3192
+ * @param {Record<string, ?>} params - Ransack-style params hash.
3193
+ * @returns {FrontendModelQuery<T>} - Query builder with Ransack filters applied.
3194
+ */
3195
+ static ransack(params) {
3196
+ return this.query().ransack(params)
3197
+ }
3198
+
3199
+ /**
3200
+ * Runs sort.
3201
+ * @template {typeof FrontendModelBase} T
3202
+ * @this {T}
3203
+ * @param {string | string[] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} sort - Sort definition(s).
3204
+ * @returns {FrontendModelQuery<T>} - Query builder with sort definitions.
3205
+ */
3206
+ static sort(sort) {
3207
+ return this.query().sort(sort)
3208
+ }
3209
+
3210
+ /**
3211
+ * Runs order.
3212
+ * @template {typeof FrontendModelBase} T
3213
+ * @this {T}
3214
+ * @param {string | string[] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} sort - Sort definition(s).
3215
+ * @returns {FrontendModelQuery<T>} - Query builder with sort definitions.
3216
+ */
3217
+ static order(sort) {
3218
+ return this.query().order(sort)
3219
+ }
3220
+
3221
+ /**
3222
+ * Runs group.
3223
+ * @template {typeof FrontendModelBase} T
3224
+ * @this {T}
3225
+ * @param {string | string[] | Record<string, ?> | Array<Record<string, ?>>} group - Group definition(s).
3226
+ * @returns {FrontendModelQuery<T>} - Query builder with group definitions.
3227
+ */
3228
+ static group(group) {
3229
+ return this.query().group(group)
3230
+ }
3231
+
3232
+ /**
3233
+ * Runs distinct.
3234
+ * @template {typeof FrontendModelBase} T
3235
+ * @this {T}
3236
+ * @param {boolean} [value] - Whether to request distinct rows.
3237
+ * @returns {FrontendModelQuery<T>} - Query builder with distinct flag.
3238
+ */
3239
+ static distinct(value = true) {
3240
+ return this.query().distinct(value)
3241
+ }
3242
+
3243
+ /**
3244
+ * Runs query.
3245
+ * @template {typeof FrontendModelBase} T
3246
+ * @this {T}
3247
+ * @returns {FrontendModelQuery<T>} - Query builder.
3248
+ */
3249
+ static query() {
3250
+ return /** Narrows the runtime value to the documented type. @type {FrontendModelQuery<T>} */ (new FrontendModelQuery({modelClass: this}))
3251
+ }
3252
+
3253
+ /**
3254
+ * Runs preload.
3255
+ * @template {typeof FrontendModelBase} T
3256
+ * @this {T}
3257
+ * @param {import("../database/query/index.js").NestedPreloadRecord | string | Array<string | import("../database/query/index.js").NestedPreloadRecord>} preload - Preload graph.
3258
+ * @returns {FrontendModelQuery<T>} - Query with preload.
3259
+ */
3260
+ static preload(preload) {
3261
+ return /** Narrows the runtime value to the documented type. @type {FrontendModelQuery<T>} */ (this.query().preload(preload))
3262
+ }
3263
+
3264
+ /**
3265
+ * Runs select.
3266
+ * @template {typeof FrontendModelBase} T
3267
+ * @this {T}
3268
+ * @param {Record<string, string[] | string> | string | string[]} select - Model-aware attribute select map or root-model shorthand.
3269
+ * @returns {FrontendModelQuery<T>} - Query with selected attributes.
3270
+ */
3271
+ static select(select) {
3272
+ return /** Narrows the runtime value to the documented type. @type {FrontendModelQuery<T>} */ (this.query().select(select))
3273
+ }
3274
+
3275
+ /**
3276
+ * Runs selects extra.
3277
+ * @template {typeof FrontendModelBase} T
3278
+ * @this {T}
3279
+ * @param {Record<string, string[] | string> | string | string[]} select - Extra attributes to load in addition to the defaults, keyed by model name or root-model shorthand.
3280
+ * @returns {FrontendModelQuery<T>} - Query with extra selected attributes.
3281
+ */
3282
+ static selectsExtra(select) {
3283
+ return /** Narrows the runtime value to the documented type. @type {FrontendModelQuery<T>} */ (this.query().selectsExtra(select))
3284
+ }
3285
+
3286
+ /**
3287
+ * Runs first.
3288
+ * @template {typeof FrontendModelBase} T
3289
+ * @this {T}
3290
+ * @returns {Promise<InstanceType<T> | null>} - First model or null.
3291
+ */
3292
+ static async first() {
3293
+ return await this.query().first()
3294
+ }
3295
+
3296
+ /**
3297
+ * Runs last.
3298
+ * @template {typeof FrontendModelBase} T
3299
+ * @this {T}
3300
+ * @returns {Promise<InstanceType<T> | null>} - Last model or null.
3301
+ */
3302
+ static async last() {
3303
+ return await this.query().last()
3304
+ }
3305
+
3306
+ /**
3307
+ * Runs find or initialize by.
3308
+ * @template {typeof FrontendModelBase} T
3309
+ * @this {T}
3310
+ * @param {Record<string, ?>} conditions - Attribute match conditions.
3311
+ * @returns {Promise<InstanceType<T>>} - Existing or initialized model.
3312
+ */
3313
+ static async findOrInitializeBy(conditions) {
3314
+ return await this.query().findOrInitializeBy(conditions)
3315
+ }
3316
+
3317
+ /**
3318
+ * Runs find or create by.
3319
+ * @template {typeof FrontendModelBase} T
3320
+ * @this {T}
3321
+ * @param {Record<string, ?>} conditions - Attribute match conditions.
3322
+ * @param {(model: InstanceType<T>) => Promise<void> | void} [callback] - Optional callback before save when created.
3323
+ * @returns {Promise<InstanceType<T>>} - Existing or newly created model.
3324
+ */
3325
+ static async findOrCreateBy(conditions, callback) {
3326
+ return await this.query().findOrCreateBy(conditions, callback)
3327
+ }
3328
+
3329
+ /**
3330
+ * Runs create.
3331
+ * @template {typeof FrontendModelBase} T
3332
+ * @this {T}
3333
+ * @param {Record<string, ?>} [attributes] - Initial attributes.
3334
+ * @returns {Promise<InstanceType<T>>} - Persisted model.
3335
+ */
3336
+ static async create(attributes = {}) {
3337
+ const model = /**
3338
+ * Narrows the runtime value to the documented type.
3339
+ @type {InstanceType<T>} */ (new this(attributes))
3340
+
3341
+ await model.save()
3342
+
3343
+ return model
3344
+ }
3345
+
3346
+ /**
3347
+ * Runs assert find by conditions.
3348
+ * @this {typeof FrontendModelBase}
3349
+ * @param {Record<string, ?>} conditions - findBy conditions.
3350
+ * @returns {void}
3351
+ */
3352
+ static assertFindByConditions(conditions) {
3353
+ assertFindByConditionsShape(conditions)
3354
+
3355
+ Object.keys(conditions).forEach((key) => {
3356
+ assertDefinedFindByConditionValue(conditions[key], key)
3357
+ })
3358
+ }
3359
+
3360
+ /**
3361
+ * Runs matches find by conditions.
3362
+ * @this {typeof FrontendModelBase}
3363
+ * @param {FrontendModelBase} model - Candidate model.
3364
+ * @param {Record<string, ?>} conditions - Match conditions.
3365
+ * @returns {boolean} - Whether the model matches all conditions.
3366
+ */
3367
+ static matchesFindByConditions(model, conditions) {
3368
+ const modelAttributes = model.attributes()
3369
+
3370
+ for (const key of Object.keys(conditions)) {
3371
+ const expectedValue = conditions[key]
3372
+ const actualValue = modelAttributes[key]
3373
+
3374
+ if (Array.isArray(expectedValue)) {
3375
+ if (Array.isArray(actualValue)) {
3376
+ if (!this.findByConditionValueMatches(actualValue, expectedValue)) {
3377
+ return false
3378
+ }
3379
+ } else if (!expectedValue.some((entry) => this.findByConditionValueMatches(actualValue, entry))) {
3380
+ return false
3381
+ }
3382
+ } else if (!this.findByConditionValueMatches(actualValue, expectedValue)) {
3383
+ return false
3384
+ }
3385
+ }
3386
+
3387
+ return true
3388
+ }
3389
+
3390
+ /**
3391
+ * Runs find by condition value matches.
3392
+ * @this {typeof FrontendModelBase}
3393
+ * @param {?} actualValue - Actual model value.
3394
+ * @param {?} expectedValue - Expected find condition value.
3395
+ * @returns {boolean} - Whether values match.
3396
+ */
3397
+ static findByConditionValueMatches(actualValue, expectedValue) {
3398
+ if (expectedValue === null) {
3399
+ return actualValue === null
3400
+ }
3401
+
3402
+ if (Array.isArray(expectedValue)) {
3403
+ if (!Array.isArray(actualValue)) {
3404
+ return false
3405
+ }
3406
+
3407
+ if (actualValue.length !== expectedValue.length) {
3408
+ return false
3409
+ }
3410
+
3411
+ for (let index = 0; index < expectedValue.length; index += 1) {
3412
+ if (!this.findByConditionValueMatches(actualValue[index], expectedValue[index])) {
3413
+ return false
3414
+ }
3415
+ }
3416
+
3417
+ return true
3418
+ }
3419
+
3420
+ if (expectedValue && typeof expectedValue === "object") {
3421
+ if (!actualValue || typeof actualValue !== "object" || Array.isArray(actualValue)) {
3422
+ return false
3423
+ }
3424
+
3425
+ const actualObject = /**
3426
+ * Narrows the runtime value to the documented type.
3427
+ @type {Record<string, ?>} */ (actualValue)
3428
+ const expectedObject = /**
3429
+ * Narrows the runtime value to the documented type.
3430
+ @type {Record<string, ?>} */ (expectedValue)
3431
+ const actualKeys = Object.keys(actualObject)
3432
+ const expectedKeys = Object.keys(expectedObject)
3433
+
3434
+ if (actualKeys.length !== expectedKeys.length) {
3435
+ return false
3436
+ }
3437
+
3438
+ for (const key of expectedKeys) {
3439
+ if (!Object.prototype.hasOwnProperty.call(actualObject, key)) {
3440
+ return false
3441
+ }
3442
+
3443
+ if (!this.findByConditionValueMatches(actualObject[key], expectedObject[key])) {
3444
+ return false
3445
+ }
3446
+ }
3447
+
3448
+ return true
3449
+ }
3450
+
3451
+ if (actualValue === expectedValue) {
3452
+ return true
3453
+ }
3454
+
3455
+ return this.findByPrimitiveValuesMatch(actualValue, expectedValue)
3456
+ }
3457
+
3458
+ /**
3459
+ * Runs find by primitive values match.
3460
+ * @this {typeof FrontendModelBase}
3461
+ * @param {?} actualValue - Actual model value.
3462
+ * @param {?} expectedValue - Expected find condition value.
3463
+ * @returns {boolean} - Whether primitive values match after safe coercion.
3464
+ */
3465
+ static findByPrimitiveValuesMatch(actualValue, expectedValue) {
3466
+ if (actualValue instanceof Date && typeof expectedValue === "string") {
3467
+ return actualValue.toISOString() === expectedValue
3468
+ }
3469
+
3470
+ if (typeof actualValue === "string" && expectedValue instanceof Date) {
3471
+ return actualValue === expectedValue.toISOString()
3472
+ }
3473
+
3474
+ if (actualValue instanceof Date && expectedValue instanceof Date) {
3475
+ return actualValue.toISOString() === expectedValue.toISOString()
3476
+ }
3477
+
3478
+ if (typeof actualValue === "number" && typeof expectedValue === "string") {
3479
+ return this.findByNumericStringMatchesNumber(expectedValue, actualValue)
3480
+ }
3481
+
3482
+ if (typeof actualValue === "string" && typeof expectedValue === "number") {
3483
+ return this.findByNumericStringMatchesNumber(actualValue, expectedValue)
3484
+ }
3485
+
3486
+ return false
3487
+ }
3488
+
3489
+ /**
3490
+ * Runs find by numeric string matches number.
3491
+ * @this {typeof FrontendModelBase}
3492
+ * @param {string} numericString - Numeric string value.
3493
+ * @param {number} expectedNumber - Number value.
3494
+ * @returns {boolean} - Whether values represent the same number.
3495
+ */
3496
+ static findByNumericStringMatchesNumber(numericString, expectedNumber) {
3497
+ if (!Number.isFinite(expectedNumber)) {
3498
+ return false
3499
+ }
3500
+
3501
+ if (!/^-?\d+(?:\.\d+)?$/.test(numericString)) {
3502
+ return false
3503
+ }
3504
+
3505
+ return Number(numericString) === expectedNumber
3506
+ }
3507
+
3508
+ /**
3509
+ * Runs update.
3510
+ * @param {Record<string, ?>} [newAttributes] - New values to assign before update.
3511
+ * @returns {Promise<this>} - Updated model.
3512
+ */
3513
+ async update(newAttributes = {}) {
3514
+ const ModelClass = /**
3515
+ * Narrows the runtime value to the documented type.
3516
+ @type {typeof FrontendModelBase} */ (this.constructor)
3517
+ const attachmentDefinitions = ModelClass.attachmentDefinitions()
3518
+ /**
3519
+ * Regular attributes.
3520
+ @type {Record<string, ?>} */
3521
+ const regularAttributes = {}
3522
+ /**
3523
+ * Pending attachments.
3524
+ @type {Array<{attachmentName: string, value: ?}>} */
3525
+ const pendingAttachments = []
3526
+
3527
+ for (const [attributeName, attributeValue] of Object.entries(newAttributes)) {
3528
+ if (attachmentDefinitions[attributeName]) {
3529
+ if (attributeValue !== undefined && attributeValue !== null) {
3530
+ pendingAttachments.push({attachmentName: attributeName, value: attributeValue})
3531
+ }
3532
+ } else {
3533
+ regularAttributes[attributeName] = attributeValue
3534
+ }
3535
+ }
3536
+
3537
+ if (Object.keys(regularAttributes).length > 0) {
3538
+ this.assignAttributes(regularAttributes)
3539
+ const changedAttributes = Object.fromEntries(
3540
+ Object.entries(this.changes()).map(([attributeName, [, currentValue]]) => [attributeName, currentValue])
3541
+ )
3542
+
3543
+ const response = await ModelClass.executeCommand("update", {
3544
+ attributes: changedAttributes,
3545
+ id: this.primaryKeyValue()
3546
+ })
3547
+
3548
+ this.assignAttributes(ModelClass.attributesFromResponse(response))
3549
+ this.setIsNewRecord(false)
3550
+ this._persistedAttributes = cloneFrontendModelAttributes(this.attributes())
3551
+ }
3552
+
3553
+ for (const pendingAttachment of pendingAttachments) {
3554
+ await this.getAttachmentByName(pendingAttachment.attachmentName).attach(pendingAttachment.value)
3555
+ }
3556
+
3557
+ return this
3558
+ }
3559
+
3560
+ /**
3561
+ * Runs attach.
3562
+ * @param {?} attachmentInput - Attachment input or named attachment payload.
3563
+ * @returns {Promise<void>} - Resolves when attached.
3564
+ */
3565
+ async attach(attachmentInput) {
3566
+ const ModelClass = /**
3567
+ * Narrows the runtime value to the documented type.
3568
+ @type {typeof FrontendModelBase} */ (this.constructor)
3569
+ const attachmentDefinitions = ModelClass.attachmentDefinitions()
3570
+ const attachmentNames = Object.keys(attachmentDefinitions)
3571
+ let attachmentName = attachmentNames[0]
3572
+ let actualAttachmentInput = attachmentInput
3573
+
3574
+ if (frontendAttachmentValueIsPlainObject(attachmentInput)) {
3575
+ if ("file" in attachmentInput && attachmentDefinitions.file) {
3576
+ attachmentName = "file"
3577
+ }
3578
+
3579
+ for (const candidateName of attachmentNames) {
3580
+ if (candidateName in attachmentInput) {
3581
+ attachmentName = candidateName
3582
+ actualAttachmentInput = attachmentInput[candidateName]
3583
+ break
3584
+ }
3585
+ }
3586
+ }
3587
+
3588
+ if (!attachmentName) {
3589
+ throw new Error(`No attachment definitions on ${ModelClass.name}`)
3590
+ }
3591
+
3592
+ await this.getAttachmentByName(attachmentName).attach(actualAttachmentInput)
3593
+ }
3594
+
3595
+ /**
3596
+ * Runs save.
3597
+ * @returns {Promise<this>} - Saved model.
3598
+ */
3599
+ async save() {
3600
+ const ModelClass = /**
3601
+ * Narrows the runtime value to the documented type.
3602
+ @type {typeof FrontendModelBase} */ (this.constructor)
3603
+ const isNew = this.isNewRecord()
3604
+ const commandType = isNew ? "create" : "update"
3605
+ /**
3606
+ * Payload.
3607
+ @type {Record<string, ?>} */
3608
+ const payload = {
3609
+ attributes: this._changedAttributesForSave()
3610
+ }
3611
+
3612
+ if (!isNew) {
3613
+ payload.id = this.primaryKeyValue()
3614
+ }
3615
+
3616
+ const nestedAttributes = this._buildNestedAttributesPayload()
3617
+
3618
+ if (nestedAttributes && Object.keys(nestedAttributes).length > 0) {
3619
+ payload.nestedAttributes = nestedAttributes
3620
+ }
3621
+
3622
+ const response = await ModelClass.executeCommand(commandType, payload)
3623
+
3624
+ this.assignAttributes(ModelClass.attributesFromResponse(response))
3625
+ this.setIsNewRecord(false)
3626
+ this._persistedAttributes = cloneFrontendModelAttributes(this.attributes())
3627
+
3628
+ this._reconcileNestedAttributesFromResponse(response)
3629
+
3630
+ return this
3631
+ }
3632
+
3633
+ /**
3634
+ * Returns the subset of `_attributes` whose value has diverged from
3635
+ * `_persistedAttributes`. Used by `save()` so the server receives only the
3636
+ * fields the caller actually changed — avoiding strict permit rejections on
3637
+ * framework-managed fields like `id`, `createdAt`, `updatedAt`, or owner
3638
+ * foreign keys that the resource never lists in `permittedParams`.
3639
+ * @returns {Record<string, ?>} - Changed attributes hash.
3640
+ */
3641
+ _changedAttributesForSave() {
3642
+ /**
3643
+ * Changed attributes.
3644
+ @type {Record<string, ?>} */
3645
+ const changedAttributes = {}
3646
+
3647
+ for (const [attributeName, [previousValue, currentValue]] of Object.entries(this.changes())) {
3648
+ if (this.isNewRecord() && previousValue === undefined && currentValue === null) continue
3649
+
3650
+ changedAttributes[attributeName] = currentValue
3651
+ }
3652
+
3653
+ return changedAttributes
3654
+ }
3655
+
3656
+ /**
3657
+ * Marks the current value for an attribute as already persisted so the next
3658
+ * save does not send it unless the caller changes it again.
3659
+ * @param {string} attributeName - Attribute to mark unchanged.
3660
+ * @returns {void}
3661
+ */
3662
+ markAttributeUnchanged(attributeName) {
3663
+ this._persistedAttributes[attributeName] = cloneFrontendModelAttributes({value: this._attributes[attributeName]}).value
3664
+ }
3665
+
3666
+ /**
3667
+ * Runs destroy.
3668
+ * @returns {Promise<void>} - Resolves when destroyed on backend.
3669
+ */
3670
+ async destroy() {
3671
+ const ModelClass = /**
3672
+ * Narrows the runtime value to the documented type.
3673
+ @type {typeof FrontendModelBase} */ (this.constructor)
3674
+
3675
+ await ModelClass.executeCommand("destroy", {
3676
+ id: this.primaryKeyValue()
3677
+ })
3678
+ }
3679
+
3680
+ /**
3681
+ * Walks relationships declared in this resource's `nestedAttributes` config
3682
+ * and builds the per-relationship payload of dirty children for a parent save.
3683
+ *
3684
+ * Included children:
3685
+ * - new records (isNewRecord()) → create entry with attributes
3686
+ * - records marked for destruction (markedForDestruction()) → destroy entry
3687
+ * - records with changed attributes (isChanged()) → update entry with attributes
3688
+ * - records with dirty descendants in their own nestedAttributes → recurse
3689
+ *
3690
+ * Loaded but untouched records are omitted so nested save preserves Rails-style
3691
+ * "children not referenced in payload are left alone" semantics.
3692
+ * @returns {Record<string, Array<Record<string, ?>>>} - Per-relationship list of nested-attribute entries.
3693
+ */
3694
+ _buildNestedAttributesPayload() {
3695
+ const ModelClass = /**
3696
+ * Narrows the runtime value to the documented type.
3697
+ @type {typeof FrontendModelBase} */ (this.constructor)
3698
+ const resourceConfig = typeof ModelClass.resourceConfig === "function" ? ModelClass.resourceConfig() : null
3699
+ const nestedAttributesConfig = resourceConfig?.nestedAttributes
3700
+
3701
+ if (!nestedAttributesConfig) return {}
3702
+
3703
+ /**
3704
+ * Payload.
3705
+ @type {Record<string, Array<Record<string, ?>>>} */
3706
+ const payload = {}
3707
+
3708
+ for (const relationshipName of Object.keys(nestedAttributesConfig)) {
3709
+ const relationship = this._relationships[relationshipName]
3710
+
3711
+ if (!relationship || !(relationship instanceof FrontendModelHasManyRelationship)) continue
3712
+ if (!Array.isArray(relationship._loadedValue) || relationship._loadedValue.length === 0) continue
3713
+
3714
+ /**
3715
+ * Entries.
3716
+ @type {Array<Record<string, ?>>} */
3717
+ const entries = []
3718
+
3719
+ for (const child of relationship._loadedValue) {
3720
+ const childEntry = child._nestedAttributesEntryForParentSave()
3721
+
3722
+ if (childEntry) entries.push(childEntry)
3723
+ }
3724
+
3725
+ if (entries.length > 0) {
3726
+ payload[relationshipName] = entries
3727
+ }
3728
+ }
3729
+
3730
+ return payload
3731
+ }
3732
+
3733
+ /**
3734
+ * Builds the payload entry for this child when walked by a parent's
3735
+ * `_buildNestedAttributesPayload`. Returns `null` when the child has no
3736
+ * dirty state and no dirty descendants, so the parent can omit it.
3737
+ * @returns {Record<string, ?> | null} - Nested-attribute entry or null if clean.
3738
+ */
3739
+ _nestedAttributesEntryForParentSave() {
3740
+ if (this.markedForDestruction()) {
3741
+ if (this.isNewRecord()) return null
3742
+ return {id: this.primaryKeyValue(), _destroy: true}
3743
+ }
3744
+
3745
+ const nestedAttributes = this._buildNestedAttributesPayload()
3746
+ const hasNestedDirty = Object.keys(nestedAttributes).length > 0
3747
+
3748
+ if (this.isNewRecord()) {
3749
+ /**
3750
+ * Entry.
3751
+ @type {Record<string, ?>} */
3752
+ const entry = {attributes: this.attributes()}
3753
+
3754
+ if (hasNestedDirty) entry.nestedAttributes = nestedAttributes
3755
+
3756
+ return entry
3757
+ }
3758
+
3759
+ if (!this.isChanged() && !hasNestedDirty) return null
3760
+
3761
+ /**
3762
+ * Entry.
3763
+ @type {Record<string, ?>} */
3764
+ const entry = {id: this.primaryKeyValue()}
3765
+
3766
+ if (this.isChanged()) entry.attributes = this.attributes()
3767
+ if (hasNestedDirty) entry.nestedAttributes = nestedAttributes
3768
+
3769
+ return entry
3770
+ }
3771
+
3772
+ /**
3773
+ * After a parent save with `nestedAttributes`, the server response includes
3774
+ * preloaded versions of the affected relationships. This replaces the local
3775
+ * `_loadedValue` for each nested-writable relationship with the server's
3776
+ * authoritative set, so destroyed children are dropped and newly-created
3777
+ * children get their server-assigned ids + persisted state.
3778
+ * @param {Record<string, ?>} response - Command response payload.
3779
+ * @returns {void}
3780
+ */
3781
+ _reconcileNestedAttributesFromResponse(response) {
3782
+ const ModelClass = /**
3783
+ * Narrows the runtime value to the documented type.
3784
+ @type {typeof FrontendModelBase} */ (this.constructor)
3785
+ const resourceConfig = typeof ModelClass.resourceConfig === "function" ? ModelClass.resourceConfig() : null
3786
+ const nestedAttributesConfig = resourceConfig?.nestedAttributes
3787
+
3788
+ if (!nestedAttributesConfig) return
3789
+
3790
+ const modelData = ModelClass.modelDataFromResponse(response)
3791
+ const preloadedRelationships = modelData.preloadedRelationships
3792
+
3793
+ /**
3794
+ * Relevant preloads.
3795
+ @type {Record<string, ?>} */
3796
+ const relevantPreloads = {}
3797
+
3798
+ for (const relationshipName of Object.keys(nestedAttributesConfig)) {
3799
+ if (relationshipName in preloadedRelationships) {
3800
+ relevantPreloads[relationshipName] = preloadedRelationships[relationshipName]
3801
+ }
3802
+ }
3803
+
3804
+ if (Object.keys(relevantPreloads).length > 0) {
3805
+ ModelClass.applyPreloadedRelationships(this, relevantPreloads)
3806
+ }
3807
+ }
3808
+
3809
+ /**
3810
+ * Runs execute command.
3811
+ * @this {typeof FrontendModelBase}
3812
+ * @param {FrontendModelCommandType} commandType - Command type.
3813
+ * @param {Record<string, ?>} payload - Command payload.
3814
+ * @returns {Promise<Record<string, ?>>} - Parsed JSON response.
3815
+ */
3816
+ static async executeCommand(commandType, payload) {
3817
+ const commandName = this.commandName(commandType)
3818
+ const serializedPayload = /**
3819
+ * Narrows the runtime value to the documented type.
3820
+ @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(payload))
3821
+ const resourcePath = this.resourcePath()
3822
+ const containsAttachmentUpload = frontendModelPayloadContainsAttachmentUpload(serializedPayload)
3823
+ const useSharedTransport = !containsAttachmentUpload
3824
+ const url = useSharedTransport ? frontendModelApiUrl() : frontendModelCommandUrl(resourcePath || "", commandName)
3825
+
3826
+ if (useSharedTransport) {
3827
+ const batchResponse = await new Promise((resolve, reject) => {
3828
+ pendingSharedFrontendModelRequests.push({
3829
+ commandName,
3830
+ commandType,
3831
+ modelClass: this,
3832
+ payload: serializedPayload,
3833
+ reject,
3834
+ requestId: `${++sharedFrontendModelRequestId}`,
3835
+ resolve,
3836
+ resourcePath
3837
+ })
3838
+
3839
+ scheduleSharedFrontendModelRequestFlush()
3840
+ })
3841
+
3842
+ const decodedBatchResponse = /**
3843
+ * Narrows the runtime value to the documented type.
3844
+ @type {Record<string, ?>} */ (batchResponse)
3845
+
3846
+ this.throwOnErrorFrontendModelResponse({
3847
+ commandType,
3848
+ response: decodedBatchResponse
3849
+ })
3850
+
3851
+ return decodedBatchResponse
3852
+ }
3853
+
3854
+ return await trackFrontendModelTransportRequest(async () => {
3855
+ const directResponse = await fetch(url, {
3856
+ body: JSON.stringify(serializedPayload),
3857
+ credentials: "include",
3858
+ headers: frontendModelRequestHeaders(),
3859
+ method: "POST"
3860
+ })
3861
+
3862
+ const directResponseText = await directResponse.text()
3863
+
3864
+ if (!directResponse.ok) {
3865
+ throwFrontendModelHttpError({
3866
+ commandLabel: `${this.name}#${commandType}`,
3867
+ response: directResponse,
3868
+ responseText: directResponseText
3869
+ })
3870
+ }
3871
+
3872
+ const directJson = directResponseText.length > 0 ? JSON.parse(directResponseText) : {}
3873
+ const decodedDirectResponse = /**
3874
+ * Narrows the runtime value to the documented type.
3875
+ @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(directJson))
3876
+
3877
+ this.throwOnErrorFrontendModelResponse({
3878
+ commandType,
3879
+ response: decodedDirectResponse
3880
+ })
3881
+
3882
+ return decodedDirectResponse
3883
+ })
3884
+ }
3885
+
3886
+ /**
3887
+ * Runs execute custom command.
3888
+ * @this {typeof FrontendModelBase}
3889
+ * @param {object} args - Command arguments.
3890
+ * @param {string} args.commandName - Raw command path segment.
3891
+ * @param {FrontendModelRequestCommandType} args.commandType - Logical command type for error handling.
3892
+ * @param {string | number | null} [args.memberId] - Optional member id for member-scoped commands.
3893
+ * @param {Record<string, ?>} args.payload - Request payload.
3894
+ * @param {string} args.resourcePath - Direct resource path.
3895
+ * @returns {Promise<Record<string, ?>>} - Decoded response payload.
3896
+ */
3897
+ static async executeCustomCommand({commandName, commandType, memberId = null, payload, resourcePath}) {
3898
+ const serializedPayload = /**
3899
+ * Narrows the runtime value to the documented type.
3900
+ @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(payload))
3901
+ const customPath = frontendModelCustomCommandPath({
3902
+ commandName,
3903
+ memberId,
3904
+ modelName: this.getModelName(),
3905
+ resourcePath
3906
+ })
3907
+
3908
+ const batchResponse = await new Promise((resolve, reject) => {
3909
+ pendingSharedFrontendModelRequests.push({
3910
+ commandType,
3911
+ customPath,
3912
+ modelClass: this,
3913
+ payload: serializedPayload,
3914
+ reject,
3915
+ requestId: `${++sharedFrontendModelRequestId}`,
3916
+ resolve
3917
+ })
3918
+
3919
+ scheduleSharedFrontendModelRequestFlush()
3920
+ })
3921
+
3922
+ const decodedBatchResponse = /**
3923
+ * Narrows the runtime value to the documented type.
3924
+ @type {Record<string, ?>} */ (batchResponse)
3925
+
3926
+ this.throwOnErrorFrontendModelResponse({
3927
+ commandType,
3928
+ response: decodedBatchResponse
3929
+ })
3930
+
3931
+ return decodedBatchResponse
3932
+ }
3933
+
3934
+ /**
3935
+ * Runs throw on error frontend model response.
3936
+ * @this {typeof FrontendModelBase}
3937
+ * @param {object} args - Arguments.
3938
+ * @param {FrontendModelRequestCommandType} args.commandType - Command type.
3939
+ * @param {Record<string, ?>} args.response - Decoded response.
3940
+ * @returns {void}
3941
+ */
3942
+ static throwOnErrorFrontendModelResponse({commandType, response}) {
3943
+ if (response?.status !== "error") return
3944
+
3945
+ const responseKeys = Object.keys(response)
3946
+ const hasOnlyStatus = responseKeys.length === 1 && responseKeys[0] === "status"
3947
+ const hasErrorMessage = typeof response.errorMessage === "string" && response.errorMessage.length > 0
3948
+ const hasErrorEnvelopeKeys = Boolean(
3949
+ response.code !== undefined
3950
+ || response.error !== undefined
3951
+ || response.errors !== undefined
3952
+ || response.message !== undefined
3953
+ )
3954
+ const nonStatusKeys = responseKeys.filter((key) => key !== "status")
3955
+ const configuredAttributeNames = this.configuredFrontendModelAttributeNames()
3956
+ const looksLikeRawModelPayload = nonStatusKeys.length > 0
3957
+ && nonStatusKeys.every((key) => configuredAttributeNames.has(key))
3958
+
3959
+ if (!hasErrorMessage && !hasOnlyStatus && !hasErrorEnvelopeKeys && looksLikeRawModelPayload) return
3960
+
3961
+ const debugErrorMessage = typeof response.debugErrorMessage === "string" && response.debugErrorMessage.length > 0
3962
+ ? response.debugErrorMessage
3963
+ : null
3964
+ const errorMessage = debugErrorMessage || (hasErrorMessage
3965
+ ? response.errorMessage
3966
+ : `Request failed for ${this.name}#${commandType}`)
3967
+
3968
+ const error = /**
3969
+ * Narrows the runtime value to the documented type.
3970
+ @type {Error & {velocious?: Record<string, ?>, errorType?: string, validationErrors?: Record<string, ?>}} */ (new Error(errorMessage))
3971
+ if (response.velocious && typeof response.velocious === "object") {
3972
+ error.velocious = response.velocious
3973
+ }
3974
+ if (typeof response.errorType === "string") {
3975
+ error.errorType = response.errorType
3976
+ }
3977
+ if (response.validationErrors && typeof response.validationErrors === "object") {
3978
+ error.validationErrors = response.validationErrors
3979
+ }
3980
+ throw error
3981
+ }
3982
+
3983
+ /**
3984
+ * Runs configured frontend model attribute names.
3985
+ * @this {typeof FrontendModelBase}
3986
+ * @returns {Set<string>} - Configured frontend model attribute names.
3987
+ */
3988
+ static configuredFrontendModelAttributeNames() {
3989
+ const resourceConfig = /**
3990
+ * Narrows the runtime value to the documented type.
3991
+ @type {Record<string, ?>} */ (this.resourceConfig())
3992
+ const attributes = resourceConfig.attributes
3993
+
3994
+ if (Array.isArray(attributes)) {
3995
+ return new Set(attributes.filter((attributeName) => typeof attributeName === "string"))
3996
+ }
3997
+
3998
+ if (attributes && typeof attributes === "object") {
3999
+ return new Set(Object.keys(attributes))
4000
+ }
4001
+
4002
+ return new Set()
4003
+ }
4004
+ }