velocious 1.0.643 → 1.0.645

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 (279) hide show
  1. package/README.md +21 -10
  2. package/build/authorization/ability.js +36 -13
  3. package/build/background-jobs/adapter.js +9 -0
  4. package/build/background-jobs/main.js +35 -10
  5. package/build/background-jobs/store.js +217 -38
  6. package/build/background-jobs/types.js +15 -0
  7. package/build/configuration-types.js +1 -1
  8. package/build/configuration.js +45 -9
  9. package/build/database/drivers/base-column.js +8 -0
  10. package/build/database/drivers/mssql/sql/update.js +1 -3
  11. package/build/database/drivers/mysql/sql/update.js +1 -3
  12. package/build/database/drivers/pgsql/column.js +22 -0
  13. package/build/database/drivers/pgsql/sql/update.js +1 -3
  14. package/build/database/drivers/sqlite/sql/alter-table.js +11 -1
  15. package/build/database/drivers/sqlite/sql/update.js +1 -3
  16. package/build/database/query/index.js +2 -2
  17. package/build/database/query/model-class-query.js +26 -10
  18. package/build/database/query/preloader/has-many.js +4 -1
  19. package/build/database/query/query-data.js +2 -1
  20. package/build/database/query/update-base.js +14 -0
  21. package/build/database/query/with-count.js +3 -1
  22. package/build/database/record/acts-as-list.js +37 -13
  23. package/build/database/record/attachments/store.js +336 -26
  24. package/build/database/record/auditing.js +6 -3
  25. package/build/database/record/index.js +197 -42
  26. package/build/database/record/instance-relationships/belongs-to.js +2 -1
  27. package/build/database/record/validators/uniqueness.js +8 -5
  28. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
  29. package/build/frontend-model-controller.js +136 -53
  30. package/build/frontend-model-resource/base-resource.js +87 -37
  31. package/build/frontend-model-resource/velocious-attachment-resource.js +98 -31
  32. package/build/frontend-models/base.js +307 -59
  33. package/build/frontend-models/preloader.js +6 -3
  34. package/build/frontend-models/query.js +15 -6
  35. package/build/frontend-models/resource-definition.js +21 -2
  36. package/build/frontend-models/use-destroyed-event.js +1 -1
  37. package/build/frontend-models/use-model-class-event.js +2 -2
  38. package/build/frontend-models/use-updated-event.js +1 -1
  39. package/build/frontend-models/websocket-channel.js +256 -93
  40. package/build/frontend-models/websocket-publishers.js +218 -18
  41. package/build/http-server/client/websocket-session.js +13 -1
  42. package/build/http-server/websocket-channel.js +15 -2
  43. package/build/src/authorization/ability.d.ts +9 -3
  44. package/build/src/authorization/ability.d.ts.map +1 -1
  45. package/build/src/authorization/ability.js +33 -14
  46. package/build/src/background-jobs/adapter.d.ts +6 -0
  47. package/build/src/background-jobs/adapter.d.ts.map +1 -1
  48. package/build/src/background-jobs/adapter.js +9 -1
  49. package/build/src/background-jobs/main.d.ts +6 -0
  50. package/build/src/background-jobs/main.d.ts.map +1 -1
  51. package/build/src/background-jobs/main.js +34 -11
  52. package/build/src/background-jobs/store.d.ts +66 -12
  53. package/build/src/background-jobs/store.d.ts.map +1 -1
  54. package/build/src/background-jobs/store.js +191 -35
  55. package/build/src/background-jobs/types.d.ts +55 -0
  56. package/build/src/background-jobs/types.d.ts.map +1 -1
  57. package/build/src/background-jobs/types.js +16 -1
  58. package/build/src/configuration-types.d.ts +3 -3
  59. package/build/src/configuration-types.d.ts.map +1 -1
  60. package/build/src/configuration-types.js +2 -2
  61. package/build/src/configuration.d.ts +18 -8
  62. package/build/src/configuration.d.ts.map +1 -1
  63. package/build/src/configuration.js +38 -7
  64. package/build/src/database/drivers/base-column.d.ts +5 -0
  65. package/build/src/database/drivers/base-column.d.ts.map +1 -1
  66. package/build/src/database/drivers/base-column.js +8 -1
  67. package/build/src/database/drivers/mssql/sql/update.d.ts.map +1 -1
  68. package/build/src/database/drivers/mssql/sql/update.js +2 -4
  69. package/build/src/database/drivers/mysql/sql/update.d.ts.map +1 -1
  70. package/build/src/database/drivers/mysql/sql/update.js +2 -4
  71. package/build/src/database/drivers/pgsql/column.d.ts +5 -0
  72. package/build/src/database/drivers/pgsql/column.d.ts.map +1 -1
  73. package/build/src/database/drivers/pgsql/column.js +19 -1
  74. package/build/src/database/drivers/pgsql/sql/update.d.ts.map +1 -1
  75. package/build/src/database/drivers/pgsql/sql/update.js +2 -4
  76. package/build/src/database/drivers/sqlite/sql/alter-table.d.ts.map +1 -1
  77. package/build/src/database/drivers/sqlite/sql/alter-table.js +6 -2
  78. package/build/src/database/drivers/sqlite/sql/update.d.ts.map +1 -1
  79. package/build/src/database/drivers/sqlite/sql/update.js +2 -4
  80. package/build/src/database/query/index.d.ts +4 -4
  81. package/build/src/database/query/index.d.ts.map +1 -1
  82. package/build/src/database/query/index.js +3 -3
  83. package/build/src/database/query/model-class-query.d.ts +8 -2
  84. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  85. package/build/src/database/query/model-class-query.js +25 -11
  86. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  87. package/build/src/database/query/preloader/has-many.js +4 -2
  88. package/build/src/database/query/query-data.d.ts.map +1 -1
  89. package/build/src/database/query/query-data.js +3 -2
  90. package/build/src/database/query/update-base.d.ts +7 -0
  91. package/build/src/database/query/update-base.d.ts.map +1 -1
  92. package/build/src/database/query/update-base.js +13 -1
  93. package/build/src/database/query/with-count.d.ts.map +1 -1
  94. package/build/src/database/query/with-count.js +3 -2
  95. package/build/src/database/record/acts-as-list.d.ts.map +1 -1
  96. package/build/src/database/record/acts-as-list.js +35 -14
  97. package/build/src/database/record/attachments/store.d.ts +67 -0
  98. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  99. package/build/src/database/record/attachments/store.js +301 -25
  100. package/build/src/database/record/auditing.d.ts.map +1 -1
  101. package/build/src/database/record/auditing.js +7 -4
  102. package/build/src/database/record/index.d.ts +78 -26
  103. package/build/src/database/record/index.d.ts.map +1 -1
  104. package/build/src/database/record/index.js +171 -42
  105. package/build/src/database/record/instance-relationships/belongs-to.d.ts.map +1 -1
  106. package/build/src/database/record/instance-relationships/belongs-to.js +3 -2
  107. package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
  108. package/build/src/database/record/validators/uniqueness.js +8 -6
  109. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +4 -4
  110. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  111. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -12
  112. package/build/src/frontend-model-controller.d.ts +29 -8
  113. package/build/src/frontend-model-controller.d.ts.map +1 -1
  114. package/build/src/frontend-model-controller.js +122 -51
  115. package/build/src/frontend-model-resource/base-resource.d.ts +51 -24
  116. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  117. package/build/src/frontend-model-resource/base-resource.js +83 -37
  118. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +45 -26
  119. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
  120. package/build/src/frontend-model-resource/velocious-attachment-resource.js +89 -31
  121. package/build/src/frontend-models/base.d.ts +53 -20
  122. package/build/src/frontend-models/base.d.ts.map +1 -1
  123. package/build/src/frontend-models/base.js +275 -59
  124. package/build/src/frontend-models/preloader.d.ts.map +1 -1
  125. package/build/src/frontend-models/preloader.js +6 -4
  126. package/build/src/frontend-models/query.d.ts +2 -2
  127. package/build/src/frontend-models/query.d.ts.map +1 -1
  128. package/build/src/frontend-models/query.js +14 -7
  129. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  130. package/build/src/frontend-models/resource-definition.js +20 -3
  131. package/build/src/frontend-models/use-destroyed-event.d.ts +1 -1
  132. package/build/src/frontend-models/use-destroyed-event.d.ts.map +1 -1
  133. package/build/src/frontend-models/use-destroyed-event.js +2 -2
  134. package/build/src/frontend-models/use-model-class-event.d.ts +2 -2
  135. package/build/src/frontend-models/use-model-class-event.d.ts.map +1 -1
  136. package/build/src/frontend-models/use-model-class-event.js +3 -3
  137. package/build/src/frontend-models/use-updated-event.d.ts +1 -1
  138. package/build/src/frontend-models/use-updated-event.d.ts.map +1 -1
  139. package/build/src/frontend-models/use-updated-event.js +2 -2
  140. package/build/src/frontend-models/websocket-channel.d.ts +60 -41
  141. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  142. package/build/src/frontend-models/websocket-channel.js +225 -85
  143. package/build/src/frontend-models/websocket-publishers.d.ts +9 -0
  144. package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
  145. package/build/src/frontend-models/websocket-publishers.js +190 -19
  146. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  147. package/build/src/http-server/client/websocket-session.js +11 -2
  148. package/build/src/http-server/websocket-channel.d.ts +20 -8
  149. package/build/src/http-server/websocket-channel.d.ts.map +1 -1
  150. package/build/src/http-server/websocket-channel.js +15 -3
  151. package/build/src/sync/assets/cache.d.ts +288 -0
  152. package/build/src/sync/assets/cache.d.ts.map +1 -0
  153. package/build/src/sync/assets/cache.js +883 -0
  154. package/build/src/sync/assets/types.d.ts +199 -0
  155. package/build/src/sync/assets/types.d.ts.map +1 -0
  156. package/build/src/sync/assets/types.js +56 -0
  157. package/build/src/sync/sync-api-client.d.ts.map +1 -1
  158. package/build/src/sync/sync-api-client.js +4 -3
  159. package/build/src/sync/sync-client.d.ts.map +1 -1
  160. package/build/src/sync/sync-client.js +3 -2
  161. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  162. package/build/src/sync/sync-envelope-replay-service.js +6 -5
  163. package/build/src/sync/sync-publisher.d.ts.map +1 -1
  164. package/build/src/sync/sync-publisher.js +4 -3
  165. package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts +2 -2
  166. package/build/src/testing/browser-frontend-model-event-hook-scenarios.d.ts.map +1 -1
  167. package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +3 -3
  168. package/build/src/testing/test-profiler.d.ts +4 -4
  169. package/build/src/testing/test-profiler.d.ts.map +1 -1
  170. package/build/src/testing/test-profiler.js +3 -3
  171. package/build/src/testing/test-runner.d.ts +279 -117
  172. package/build/src/testing/test-runner.d.ts.map +1 -1
  173. package/build/src/testing/test-runner.js +503 -933
  174. package/build/src/testing/test.d.ts +11 -72
  175. package/build/src/testing/test.d.ts.map +1 -1
  176. package/build/src/testing/test.js +131 -284
  177. package/build/src/testing/velocious-attempt-executor.d.ts +54 -0
  178. package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -0
  179. package/build/src/testing/velocious-attempt-executor.js +408 -0
  180. package/build/src/testing/velocious-runner-reporter.d.ts +67 -0
  181. package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -0
  182. package/build/src/testing/velocious-runner-reporter.js +240 -0
  183. package/build/src/testing/velocious-suite-hook-executor.d.ts +19 -0
  184. package/build/src/testing/velocious-suite-hook-executor.d.ts.map +1 -0
  185. package/build/src/testing/velocious-suite-hook-executor.js +41 -0
  186. package/build/src/testing/velocious-test-arguments.d.ts +39 -0
  187. package/build/src/testing/velocious-test-arguments.d.ts.map +1 -0
  188. package/build/src/testing/velocious-test-arguments.js +55 -0
  189. package/build/src/utils/model-primary-key.d.ts +73 -0
  190. package/build/src/utils/model-primary-key.d.ts.map +1 -0
  191. package/build/src/utils/model-primary-key.js +144 -0
  192. package/build/src/utils/sha256-bytes-hex.d.ts +7 -0
  193. package/build/src/utils/sha256-bytes-hex.d.ts.map +1 -0
  194. package/build/src/utils/sha256-bytes-hex.js +117 -0
  195. package/build/src/utils/sha256-hex.d.ts +2 -4
  196. package/build/src/utils/sha256-hex.d.ts.map +1 -1
  197. package/build/src/utils/sha256-hex.js +6 -120
  198. package/build/sync/assets/cache.js +998 -0
  199. package/build/sync/assets/types.js +62 -0
  200. package/build/sync/sync-api-client.js +3 -2
  201. package/build/sync/sync-client.js +2 -1
  202. package/build/sync/sync-envelope-replay-service.js +5 -4
  203. package/build/sync/sync-publisher.js +3 -2
  204. package/build/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  205. package/build/testing/test-profiler.js +2 -2
  206. package/build/testing/test-runner.js +496 -990
  207. package/build/testing/test.js +164 -306
  208. package/build/testing/velocious-attempt-executor.js +441 -0
  209. package/build/testing/velocious-runner-reporter.js +264 -0
  210. package/build/testing/velocious-suite-hook-executor.js +43 -0
  211. package/build/testing/velocious-test-arguments.js +64 -0
  212. package/build/tsconfig.tsbuildinfo +1 -1
  213. package/build/utils/model-primary-key.js +171 -0
  214. package/build/utils/sha256-bytes-hex.js +132 -0
  215. package/build/utils/sha256-hex.js +7 -135
  216. package/package.json +5 -4
  217. package/src/authorization/ability.js +36 -13
  218. package/src/background-jobs/adapter.js +9 -0
  219. package/src/background-jobs/main.js +35 -10
  220. package/src/background-jobs/store.js +217 -38
  221. package/src/background-jobs/types.js +15 -0
  222. package/src/configuration-types.js +1 -1
  223. package/src/configuration.js +45 -9
  224. package/src/database/drivers/base-column.js +8 -0
  225. package/src/database/drivers/mssql/sql/update.js +1 -3
  226. package/src/database/drivers/mysql/sql/update.js +1 -3
  227. package/src/database/drivers/pgsql/column.js +22 -0
  228. package/src/database/drivers/pgsql/sql/update.js +1 -3
  229. package/src/database/drivers/sqlite/sql/alter-table.js +11 -1
  230. package/src/database/drivers/sqlite/sql/update.js +1 -3
  231. package/src/database/query/index.js +2 -2
  232. package/src/database/query/model-class-query.js +26 -10
  233. package/src/database/query/preloader/has-many.js +4 -1
  234. package/src/database/query/query-data.js +2 -1
  235. package/src/database/query/update-base.js +14 -0
  236. package/src/database/query/with-count.js +3 -1
  237. package/src/database/record/acts-as-list.js +37 -13
  238. package/src/database/record/attachments/store.js +336 -26
  239. package/src/database/record/auditing.js +6 -3
  240. package/src/database/record/index.js +197 -42
  241. package/src/database/record/instance-relationships/belongs-to.js +2 -1
  242. package/src/database/record/validators/uniqueness.js +8 -5
  243. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +24 -8
  244. package/src/frontend-model-controller.js +136 -53
  245. package/src/frontend-model-resource/base-resource.js +87 -37
  246. package/src/frontend-model-resource/velocious-attachment-resource.js +98 -31
  247. package/src/frontend-models/base.js +307 -59
  248. package/src/frontend-models/preloader.js +6 -3
  249. package/src/frontend-models/query.js +15 -6
  250. package/src/frontend-models/resource-definition.js +21 -2
  251. package/src/frontend-models/use-destroyed-event.js +1 -1
  252. package/src/frontend-models/use-model-class-event.js +2 -2
  253. package/src/frontend-models/use-updated-event.js +1 -1
  254. package/src/frontend-models/websocket-channel.js +256 -93
  255. package/src/frontend-models/websocket-publishers.js +218 -18
  256. package/src/http-server/client/websocket-session.js +13 -1
  257. package/src/http-server/websocket-channel.js +15 -2
  258. package/src/sync/assets/cache.js +998 -0
  259. package/src/sync/assets/types.js +62 -0
  260. package/src/sync/sync-api-client.js +3 -2
  261. package/src/sync/sync-client.js +2 -1
  262. package/src/sync/sync-envelope-replay-service.js +5 -4
  263. package/src/sync/sync-publisher.js +3 -2
  264. package/src/testing/browser-frontend-model-event-hook-scenarios.js +2 -2
  265. package/src/testing/test-profiler.js +2 -2
  266. package/src/testing/test-runner.js +496 -990
  267. package/src/testing/test.js +164 -306
  268. package/src/testing/velocious-attempt-executor.js +441 -0
  269. package/src/testing/velocious-runner-reporter.js +264 -0
  270. package/src/testing/velocious-suite-hook-executor.js +43 -0
  271. package/src/testing/velocious-test-arguments.js +64 -0
  272. package/src/utils/model-primary-key.js +171 -0
  273. package/src/utils/sha256-bytes-hex.js +132 -0
  274. package/src/utils/sha256-hex.js +7 -135
  275. package/build/src/testing/testing-package-adapter.d.ts +0 -9
  276. package/build/src/testing/testing-package-adapter.d.ts.map +0 -1
  277. package/build/src/testing/testing-package-adapter.js +0 -69
  278. package/build/testing/testing-package-adapter.js +0 -76
  279. package/src/testing/testing-package-adapter.js +0 -76
@@ -3,11 +3,12 @@
3
3
  import AuthorizationBaseResource from "../authorization/base-resource.js"
4
4
  import * as inflection from "inflection"
5
5
  import isPlainObject from "../utils/plain-object.js"
6
+ import {modelPrimaryKeyConditions, readModelPrimaryKeyValue, scalarModelPrimaryKeyValue} from "../utils/model-primary-key.js"
6
7
  import VelociousError from "../velocious-error.js"
7
8
 
8
9
  /**
9
10
  * Backend or frontend model class bound to a frontend-model resource.
10
- * @typedef {import("../authorization/base-resource.js").AuthorizationResourceModelClass & {attachmentDefinitions: () => Record<string, import("../configuration-types.js").FrontendModelAttachmentConfiguration>, primaryKey: () => string}} FrontendModelResourceModelClass
11
+ * @typedef {import("../authorization/base-resource.js").AuthorizationResourceModelClass & {attachmentDefinitions: () => Record<string, import("../configuration-types.js").FrontendModelAttachmentConfiguration>, primaryKey: () => import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition}} FrontendModelResourceModelClass
11
12
  */
12
13
 
13
14
  /**
@@ -15,6 +16,12 @@ import VelociousError from "../velocious-error.js"
15
16
  * @typedef {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} FrontendModelResourceAction
16
17
  */
17
18
 
19
+ /**
20
+ * Options for building an authorized frontend-model resource query.
21
+ * @typedef {object} FrontendModelResourceAuthorizedQueryOptions
22
+ * @property {() => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>} [ruleQueryFactory] - Query factory used when authorization must target a captured record snapshot.
23
+ */
24
+
18
25
  /**
19
26
  * Frontend-model controller methods used by resources.
20
27
  * @typedef {import("../controller.js").default & {
@@ -23,8 +30,8 @@ import VelociousError from "../velocious-error.js"
23
30
  * applyFrontendModelSearch: (args: {query: import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>, search: FrontendModelResourceSearch}) => void,
24
31
  * applyFrontendModelSort: (args: {query: import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>, sort: FrontendModelResourceSort}) => void,
25
32
  * frontendModelAbilityAction: (action: FrontendModelResourceAction) => string,
26
- * frontendModelAbilityAuthorizedQuery: (action: FrontendModelResourceAction) => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
27
- * frontendModelAuthorizedQuery: (action: FrontendModelResourceAction) => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
33
+ * frontendModelAbilityAuthorizedQuery: (action: FrontendModelResourceAction, options?: FrontendModelResourceAuthorizedQueryOptions) => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
34
+ * frontendModelAuthorizedQuery: (action: FrontendModelResourceAction, options?: FrontendModelResourceAuthorizedQueryOptions) => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
28
35
  * frontendModelIndexQuery: (options?: FrontendModelResourceIndexQueryOptions & {resource?: Pick<FrontendModelBaseResource<FrontendModelResourceModelClass>, "applyFrontendModelIndexPagination" | "applyFrontendModelIndexSearch" | "applyFrontendModelIndexSort">}) => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
29
36
  * frontendModelParams: () => import("../configuration-types.js").VelociousParams,
30
37
  * frontendModelPreload: () => import("../database/query/index.js").NestedPreloadRecord | null,
@@ -175,7 +182,7 @@ import VelociousError from "../velocious-error.js"
175
182
 
176
183
  /**
177
184
  * Normalized nested attributes entry.
178
- * @typedef {FrontendModelResourceAttributePayload & {id?: string | number, _destroy?: boolean, attributes?: FrontendModelResourceAttributePayload, attachments?: FrontendModelResourceAttributePayload, nestedAttributes?: FrontendModelResourceAttributePayload}} FrontendModelResourceNestedEntry
185
+ * @typedef {FrontendModelResourceAttributePayload & {id?: import("../utils/model-primary-key.js").ModelPrimaryKeyValue, _destroy?: boolean, attributes?: FrontendModelResourceAttributePayload, attachments?: FrontendModelResourceAttributePayload, nestedAttributes?: FrontendModelResourceAttributePayload}} FrontendModelResourceNestedEntry
179
186
  */
180
187
 
181
188
  /**
@@ -217,7 +224,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
217
224
  static relationships = undefined
218
225
  /** @type {string | undefined} */
219
226
  static modelName = undefined
220
- /** @type {string | undefined} */
227
+ /** @type {string | string[] | undefined} */
221
228
  static primaryKey = undefined
222
229
  /** @type {import("../configuration-types.js").FrontendModelResourceServerConfiguration | undefined} */
223
230
  static server = undefined
@@ -468,7 +475,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
468
475
  if (collectionCommands) config.collectionCommands = /** @type {string[]} */ (collectionCommands)
469
476
  if (memberCommands) config.memberCommands = /** @type {string[]} */ (memberCommands)
470
477
  if (modelName) config.modelName = /** @type {string} */ (modelName)
471
- if (primaryKey) config.primaryKey = /** @type {string} */ (primaryKey)
478
+ if (primaryKey) config.primaryKey = /** @type {string | string[]} */ (primaryKey)
472
479
  if (relationships) config.relationships = /** @type {string[]} */ (relationships)
473
480
  if (server) config.server = /** @type {import("../configuration-types.js").FrontendModelResourceServerConfiguration} */ (server)
474
481
  if (sync !== undefined) config.sync = /** @type {import("../configuration-types.js").FrontendModelResourceSyncConfiguration | boolean} */ (sync)
@@ -476,6 +483,23 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
476
483
  return config
477
484
  }
478
485
 
486
+ /**
487
+ * Resolves the client-facing resource primary key from explicit resource config or the backing model.
488
+ * @param {import("../configuration-types.js").FrontendModelResourceConfiguration | import("../configuration-types.js").NormalizedFrontendModelResourceConfiguration} resourceConfiguration - Resource configuration.
489
+ * @returns {import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition} - Client-facing primary key.
490
+ */
491
+ static resolvedPrimaryKey(resourceConfiguration) {
492
+ if (resourceConfiguration.primaryKey) return resourceConfiguration.primaryKey
493
+ if (!this.ModelClass) return "id"
494
+
495
+ const modelClass = /** @type {typeof import("../database/record/index.js").default} */ (this.modelClass())
496
+ const modelPrimaryKey = modelClass.primaryKey()
497
+
498
+ return Array.isArray(modelPrimaryKey)
499
+ ? modelPrimaryKey.map((columnName) => modelClass.resolveAttributeName(columnName) || columnName)
500
+ : modelClass.resolveAttributeName(modelPrimaryKey) || modelPrimaryKey
501
+ }
502
+
479
503
  /**
480
504
  * Runs controller instance.
481
505
  * @returns {import("../controller.js").default} - Controller instance.
@@ -671,12 +695,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
671
695
  */
672
696
  async findSyncRecord({ability = this.ability, forDelete = false, mutation}) {
673
697
  const ModelClass = this.databaseModelClass()
674
- const primaryKey = ModelClass.primaryKey()
675
698
  const query = ability
676
699
  ? ModelClass.accessibleFor(this.syncAbilityAction(forDelete ? "destroy" : "update"), ability)
677
700
  : ModelClass.where({})
678
701
 
679
- return await query.findBy({[primaryKey]: mutation.resourceId})
702
+ return await query.findBy(modelPrimaryKeyConditions(this.primaryKey(), mutation.resourceId))
680
703
  }
681
704
 
682
705
  /**
@@ -863,18 +886,23 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
863
886
 
864
887
  /**
865
888
  * Runs primary key.
866
- * @returns {string} - Primary key.
889
+ * @returns {import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition} - Primary key.
867
890
  */
868
- primaryKey() { return this.modelClass().primaryKey() }
891
+ primaryKey() {
892
+ const ResourceClass = /** @type {typeof FrontendModelBaseResource} */ (this.constructor)
893
+
894
+ return ResourceClass.resolvedPrimaryKey(this.resourceConfiguration())
895
+ }
869
896
 
870
897
  /**
871
898
  * Runs authorized query.
872
899
  * @param {FrontendModelResourceAction} action - Ability action.
900
+ * @param {FrontendModelResourceAuthorizedQueryOptions} [options] - Authorization query options.
873
901
  * @returns {import("../database/query/model-class-query.js").default<TDatabaseModelClass>} - Authorized query.
874
902
  */
875
- authorizedQuery(action) {
903
+ authorizedQuery(action, options) {
876
904
  // Narrows the controller query to this resource's model class.
877
- return /** @type {import("../database/query/model-class-query.js").default<TDatabaseModelClass>} */ (this.typedControllerInstance().frontendModelAbilityAuthorizedQuery(action))
905
+ return /** @type {import("../database/query/model-class-query.js").default<TDatabaseModelClass>} */ (this.typedControllerInstance().frontendModelAbilityAuthorizedQuery(action, options))
878
906
  }
879
907
 
880
908
  /**
@@ -988,7 +1016,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
988
1016
  /**
989
1017
  * Runs find.
990
1018
  * @param {"find" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} action - Action.
991
- * @param {string | number} id - Record id.
1019
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} id - Record id.
992
1020
  * @returns {Promise<import("../database/record/index.js").default | null>} - Located model.
993
1021
  */
994
1022
  async find(action, id) {
@@ -999,7 +1027,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
999
1027
  query = query.preload(preload)
1000
1028
  }
1001
1029
 
1002
- return await query.findBy({[this.primaryKey()]: id})
1030
+ return await query.findBy(modelPrimaryKeyConditions(this.primaryKey(), id))
1003
1031
  }
1004
1032
 
1005
1033
  /**
@@ -1282,7 +1310,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1282
1310
  * @param {FrontendModelResourcePayloadValue} args.rawEntries - Raw nested entries from the request payload.
1283
1311
  * @param {{attributes: string[], nested: Record<string, ReturnType<typeof JSON.parse>>}} args.childPermit - Parsed child permit.
1284
1312
  * @param {FrontendModelResourceController | null | undefined} args.controller - Controller instance for child resource lookup.
1285
- * @returns {{ability: import("../authorization/ability.js").default | undefined, childResource: FrontendModelBaseResource, childResourceConfig: FrontendModelResolvedResourceConfiguration, childWritableAttributes: string[], destroyPermitted: boolean, entries: Array<FrontendModelResourceNestedEntry>, relationship: import("../database/record/relationships/base.js").default, targetModelClass: typeof import("../database/record/index.js").default}} Nested relationship context.
1313
+ * @returns {{ability: import("../authorization/ability.js").default | undefined, childPrimaryKey: import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition, childResource: FrontendModelBaseResource, childResourceConfig: FrontendModelResolvedResourceConfiguration, childWritableAttributes: string[], destroyPermitted: boolean, entries: Array<FrontendModelResourceNestedEntry>, relationship: import("../database/record/relationships/base.js").default, targetModelClass: typeof import("../database/record/index.js").default}} Nested relationship context.
1286
1314
  */
1287
1315
  _nestedRelationshipContext({parent, relationshipName, rawEntries, childPermit, controller}) {
1288
1316
  if (!controller) {
@@ -1334,9 +1362,10 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1334
1362
  params: controller.frontendModelParams(),
1335
1363
  resourceConfiguration: childResourceConfig.resourceConfiguration
1336
1364
  })
1365
+ const childPrimaryKey = childResource.primaryKey()
1337
1366
  const childWritableAttributes = childPermit.attributes.filter((name) => name !== "_destroy")
1338
1367
  const entries = rawNormalizedEntries
1339
- .map((entry) => this._normalizeNestedRelationshipEntry({childPermit, entry, relationshipName, targetModelClass}))
1368
+ .map((entry) => this._normalizeNestedRelationshipEntry({childPermit, childPrimaryKey, entry, relationshipName, targetModelClass}))
1340
1369
  .filter((entry) => {
1341
1370
  if (typeof modelAcceptance.rejectIf !== "function") return true
1342
1371
 
@@ -1345,6 +1374,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1345
1374
 
1346
1375
  return {
1347
1376
  ability: controller.currentAbility() || this.ability,
1377
+ childPrimaryKey,
1348
1378
  childResource,
1349
1379
  childResourceConfig,
1350
1380
  childWritableAttributes,
@@ -1400,12 +1430,13 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1400
1430
  * fields (`{name, file, commentsAttributes}`).
1401
1431
  * @param {object} args - Normalization inputs.
1402
1432
  * @param {{attributes: string[], nested: Record<string, ReturnType<typeof JSON.parse>>}} args.childPermit - Parsed child permit spec.
1433
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition} args.childPrimaryKey - Resolved child resource primary key.
1403
1434
  * @param {FrontendModelResourceNestedEntry} args.entry - Raw nested entry.
1404
1435
  * @param {string} args.relationshipName - Relationship name for error messages.
1405
1436
  * @param {typeof import("../database/record/index.js").default} args.targetModelClass - Child model class.
1406
1437
  * @returns {FrontendModelResourceNestedEntry} Normalized nested entry.
1407
1438
  */
1408
- _normalizeNestedRelationshipEntry({childPermit, entry, relationshipName, targetModelClass}) {
1439
+ _normalizeNestedRelationshipEntry({childPermit, childPrimaryKey, entry, relationshipName, targetModelClass}) {
1409
1440
  /** @type {FrontendModelResourceAttributePayload} */
1410
1441
  const attributes = {}
1411
1442
  /** @type {FrontendModelResourceAttributePayload} */
@@ -1418,11 +1449,16 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1418
1449
 
1419
1450
  for (const [attributeName, value] of Object.entries(entry)) {
1420
1451
  if (attributeName === "id") {
1421
- if (typeof value !== "string" && typeof value !== "number") {
1422
- throw new Error(`nestedAttributes['${relationshipName}'] entry id must be a string or number.`)
1423
- }
1452
+ if (Array.isArray(childPrimaryKey)) {
1453
+ modelPrimaryKeyConditions(childPrimaryKey, value)
1454
+ normalized.id = /** @type {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} */ (value)
1455
+ } else {
1456
+ if (typeof value !== "string" && typeof value !== "number") {
1457
+ throw new Error(`nestedAttributes['${relationshipName}'] entry id must be a string or number.`)
1458
+ }
1424
1459
 
1425
- normalized.id = value
1460
+ normalized.id = value
1461
+ }
1426
1462
  continue
1427
1463
  }
1428
1464
 
@@ -1520,6 +1556,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1520
1556
  const existing = await this._findNestedRecord({
1521
1557
  ability: context.ability,
1522
1558
  action: "destroy",
1559
+ childPrimaryKey: context.childPrimaryKey,
1523
1560
  childResourceConfiguration: context.childResourceConfig.resourceConfiguration,
1524
1561
  id,
1525
1562
  relationshipName,
@@ -1536,6 +1573,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1536
1573
  ? await this._findNestedRecord({
1537
1574
  ability: context.ability,
1538
1575
  action: "update",
1576
+ childPrimaryKey: context.childPrimaryKey,
1539
1577
  childResourceConfiguration: context.childResourceConfig.resourceConfiguration,
1540
1578
  id,
1541
1579
  relationshipName,
@@ -1555,6 +1593,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1555
1593
  await this._authorizeCreatedChild({
1556
1594
  ability: context.ability,
1557
1595
  child,
1596
+ childPrimaryKey: context.childPrimaryKey,
1558
1597
  childResourceConfiguration: context.childResourceConfig.resourceConfiguration,
1559
1598
  relationshipName,
1560
1599
  targetModelClass: context.targetModelClass
@@ -1565,7 +1604,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1565
1604
  await context.childResource._applyNestedAttributes(child, entry.nestedAttributes, controller, childPermit)
1566
1605
  }
1567
1606
 
1568
- parent.setAttribute(foreignKey, child.id())
1607
+ parent.setAttribute(foreignKey, scalarModelPrimaryKeyValue(child.id(), `Nested belongs-to write for ${child.getModelClass().name}`))
1569
1608
  }
1570
1609
  }
1571
1610
  }
@@ -1645,6 +1684,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1645
1684
  const existing = await this._findScopedChild({
1646
1685
  ability: context.ability,
1647
1686
  action: "destroy",
1687
+ childPrimaryKey: context.childPrimaryKey,
1648
1688
  childResourceConfiguration: context.childResourceConfig.resourceConfiguration,
1649
1689
  id,
1650
1690
  parent,
@@ -1666,6 +1706,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1666
1706
  const existing = await this._findScopedChild({
1667
1707
  ability: context.ability,
1668
1708
  action: "update",
1709
+ childPrimaryKey: context.childPrimaryKey,
1669
1710
  childResourceConfiguration: context.childResourceConfig.resourceConfiguration,
1670
1711
  id,
1671
1712
  parent,
@@ -1702,6 +1743,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1702
1743
  await this._authorizeCreatedChild({
1703
1744
  ability: context.ability,
1704
1745
  child,
1746
+ childPrimaryKey: context.childPrimaryKey,
1705
1747
  childResourceConfiguration: context.childResourceConfig.resourceConfiguration,
1706
1748
  relationshipName,
1707
1749
  targetModelClass: context.targetModelClass
@@ -1760,7 +1802,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1760
1802
  _parentLinkAttributesForNestedChild({parent, relationship, targetModelClass}) {
1761
1803
  const foreignKey = this._foreignKeyAttributeForModel(relationship, targetModelClass)
1762
1804
  /** @type {Record<string, string | number>} */
1763
- const attributes = {[foreignKey]: /** @type {string | number} */ (parent.id())}
1805
+ const attributes = {[foreignKey]: scalarModelPrimaryKeyValue(parent.id(), `Nested child write for ${parent.getModelClass().name}`)}
1764
1806
 
1765
1807
  if (relationship.getPolymorphic()) {
1766
1808
  const typeAttribute = this._polymorphicTypeAttributeForModel(relationship, targetModelClass)
@@ -1788,18 +1830,18 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1788
1830
  * @param {object} args - Lookup inputs.
1789
1831
  * @param {import("../authorization/ability.js").default | undefined} args.ability - Current ability.
1790
1832
  * @param {"update" | "destroy"} args.action - Frontend action.
1833
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition} args.childPrimaryKey - Resolved child resource primary key.
1791
1834
  * @param {import("../configuration-types.js").NormalizedFrontendModelResourceConfiguration} args.childResourceConfiguration - Child resource configuration.
1792
- * @param {string | number} args.id - Child id from the payload.
1835
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} args.id - Child id from the payload.
1793
1836
  * @param {string} args.relationshipName - Parent's relationship name for error messages.
1794
1837
  * @param {typeof import("../database/record/index.js").default} args.targetModelClass - Child model class.
1795
1838
  * @returns {Promise<import("../database/record/index.js").default>} Authorized child model.
1796
1839
  */
1797
- async _findNestedRecord({ability, action, childResourceConfiguration, id, relationshipName, targetModelClass}) {
1798
- const primaryKey = targetModelClass.primaryKey()
1840
+ async _findNestedRecord({ability, action, childPrimaryKey, childResourceConfiguration, id, relationshipName, targetModelClass}) {
1799
1841
  const query = ability
1800
1842
  ? targetModelClass.accessibleFor(this._resolveChildAbilityAction(childResourceConfiguration, action), ability)
1801
1843
  : targetModelClass.where({})
1802
- const existing = await query.findBy({[primaryKey]: id})
1844
+ const existing = await query.findBy(modelPrimaryKeyConditions(childPrimaryKey, id))
1803
1845
 
1804
1846
  if (!existing) {
1805
1847
  throw new Error(`Cannot ${action} nested ${relationshipName}[id=${id}]: record not found or not authorized.`)
@@ -1842,17 +1884,25 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1842
1884
  * @param {object} args - Arguments.
1843
1885
  * @param {import("../authorization/ability.js").default | undefined} args.ability - Current ability.
1844
1886
  * @param {"update" | "destroy"} args.action - Frontend action.
1887
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition} args.childPrimaryKey - Resolved child resource primary key.
1845
1888
  * @param {import("../configuration-types.js").NormalizedFrontendModelResourceConfiguration} args.childResourceConfiguration - Child resource configuration.
1846
- * @param {string | number} args.id - Child id from the payload.
1889
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyValue} args.id - Child id from the payload.
1847
1890
  * @param {import("../database/record/index.js").default} args.parent - Parent model instance.
1848
1891
  * @param {Record<string, string | number>} args.parentLinkAttributes - Attributes that scope the child to the parent.
1849
1892
  * @param {string} args.relationshipName - Parent's relationship name (for error messages).
1850
1893
  * @param {typeof import("../database/record/index.js").default} args.targetModelClass - Child model class.
1851
1894
  * @returns {Promise<import("../database/record/index.js").default>} - Authorized, parent-linked child model.
1852
1895
  */
1853
- async _findScopedChild({ability, action, childResourceConfiguration, id, parent, parentLinkAttributes, relationshipName, targetModelClass}) {
1854
- const primaryKey = targetModelClass.primaryKey()
1855
- const lookup = {[primaryKey]: id, ...parentLinkAttributes}
1896
+ async _findScopedChild({ability, action, childPrimaryKey, childResourceConfiguration, id, parent, parentLinkAttributes, relationshipName, targetModelClass}) {
1897
+ const identityConditions = modelPrimaryKeyConditions(childPrimaryKey, id)
1898
+
1899
+ for (const [attributeName, parentValue] of Object.entries(parentLinkAttributes)) {
1900
+ if (Object.prototype.hasOwnProperty.call(identityConditions, attributeName) && identityConditions[attributeName] !== parentValue) {
1901
+ throw new Error(`Cannot ${action} nested ${relationshipName}[id=${id}]: identity field '${attributeName}' does not match parent ${parent.getModelClass().name}[id=${parent.id()}].`)
1902
+ }
1903
+ }
1904
+
1905
+ const lookup = {...identityConditions, ...parentLinkAttributes}
1856
1906
  const query = ability
1857
1907
  ? targetModelClass.accessibleFor(this._resolveChildAbilityAction(childResourceConfiguration, action), ability)
1858
1908
  : targetModelClass.where({})
@@ -1873,22 +1923,22 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
1873
1923
  * @param {object} args - Arguments.
1874
1924
  * @param {import("../authorization/ability.js").default | undefined} args.ability - Current ability.
1875
1925
  * @param {import("../database/record/index.js").default} args.child - Child model instance just created.
1926
+ * @param {import("../utils/model-primary-key.js").ModelPrimaryKeyDefinition} args.childPrimaryKey - Resolved child resource primary key.
1876
1927
  * @param {import("../configuration-types.js").NormalizedFrontendModelResourceConfiguration} args.childResourceConfiguration - Child resource configuration.
1877
1928
  * @param {string} args.relationshipName - Parent's relationship name (for error messages).
1878
1929
  * @param {typeof import("../database/record/index.js").default} args.targetModelClass - Child model class.
1879
1930
  * @returns {Promise<void>}
1880
1931
  */
1881
- async _authorizeCreatedChild({ability, child, childResourceConfiguration, relationshipName, targetModelClass}) {
1932
+ async _authorizeCreatedChild({ability, child, childPrimaryKey, childResourceConfiguration, relationshipName, targetModelClass}) {
1882
1933
  if (!ability) return
1883
1934
 
1884
1935
  const abilityAction = this._resolveChildAbilityAction(childResourceConfiguration, "create")
1885
- const primaryKey = targetModelClass.primaryKey()
1886
- const authorizedIds = await targetModelClass
1936
+ const identity = readModelPrimaryKeyValue(childPrimaryKey, (attributeName) => child.readAttribute(attributeName))
1937
+ const authorizedChild = await targetModelClass
1887
1938
  .accessibleFor(abilityAction, ability)
1888
- .where({[primaryKey]: child.readAttribute(primaryKey)})
1889
- .pluck(primaryKey)
1939
+ .findBy(modelPrimaryKeyConditions(childPrimaryKey, identity))
1890
1940
 
1891
- if (authorizedIds.length === 0) {
1941
+ if (!authorizedChild) {
1892
1942
  throw new Error(`Nested create on ${relationshipName}[${targetModelClass.name}] not authorized.`)
1893
1943
  }
1894
1944
  }
@@ -2,7 +2,12 @@
2
2
 
3
3
  import FrontendModelBaseResource from "./base-resource.js"
4
4
  import VelociousAttachment from "../database/record/attachments/attachment-record.js"
5
+ import {frontendModelResourcesForBackendProject} from "../frontend-models/resource-definition.js"
5
6
  import isPlainObject from "../utils/plain-object.js"
7
+ import {modelPrimaryKeyConditions, modelPrimaryKeyValueFromCacheKey} from "../utils/model-primary-key.js"
8
+
9
+ /** @typedef {{name: string, recordId: string, recordType: string, resourceName: string | null}} AttachmentOwnerScope */
10
+ /** @typedef {{backendProject: import("../configuration-types.js").BackendProjectConfiguration, modelName: string, resourceClass: import("../configuration-types.js").FrontendModelResourceClassType, resourceConfiguration: import("../configuration-types.js").NormalizedFrontendModelResourceConfiguration}} AttachmentOwnerResource */
6
11
 
7
12
  /**
8
13
  * Framework-owned frontend resource exposing safe attachment metadata while
@@ -59,6 +64,20 @@ export default class VelociousAttachmentResource extends FrontendModelBaseResour
59
64
  }
60
65
  }
61
66
 
67
+ /**
68
+ * Loads attachment metadata after removing the authorization-only resource name from database filters.
69
+ * @returns {Promise<VelociousAttachment[]>} - Attachment metadata rows.
70
+ */
71
+ async records() {
72
+ const controller = /** @type {import("../frontend-model-controller.js").default} */ (this.controllerInstance())
73
+ const params = controller.frontendModelParams()
74
+ const where = {...this.requiredWhereFromParams()}
75
+
76
+ delete where.resourceName
77
+
78
+ return await controller.withFrontendModelParams({...params, where}, async () => await this.indexQuery().toArray())
79
+ }
80
+
62
81
  /**
63
82
  * Runs find.
64
83
  * @param {"find" | "update" | "destroy" | "attach" | "download" | "url"} action - Action.
@@ -96,20 +115,32 @@ export default class VelociousAttachmentResource extends FrontendModelBaseResour
96
115
 
97
116
  /**
98
117
  * Returns a validated owner scope from frontend-model where params.
99
- * @returns {{name: string, recordId: string, recordType: string}} - Attachment owner scope.
118
+ * @returns {AttachmentOwnerScope} - Attachment owner scope.
100
119
  */
101
120
  requiredOwnerScopeFromParams() {
102
- const where = this.params().where
103
-
104
- if (!isPlainObject(where)) {
105
- throw new Error("VelociousAttachment index requires recordType, recordId, and name where filters")
106
- }
121
+ const where = this.requiredWhereFromParams()
122
+ const recordType = this.requiredSingleWhereValue({attributeName: "recordType", where})
107
123
 
108
124
  return {
109
125
  name: this.requiredSingleWhereValue({attributeName: "name", where}),
110
126
  recordId: this.requiredSingleWhereValue({attributeName: "recordId", where}),
111
- recordType: this.requiredSingleWhereValue({attributeName: "recordType", where})
127
+ recordType,
128
+ resourceName: this.requiredSingleWhereValue({attributeName: "resourceName", where})
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Returns the required attachment metadata where object.
134
+ * @returns {Record<string, ReturnType<typeof JSON.parse>>} - Attachment where filters.
135
+ */
136
+ requiredWhereFromParams() {
137
+ const where = this.params().where
138
+
139
+ if (!isPlainObject(where)) {
140
+ throw new Error("VelociousAttachment index requires resourceName, recordType, recordId, and name where filters")
112
141
  }
142
+
143
+ return where
113
144
  }
114
145
 
115
146
  /**
@@ -130,57 +161,91 @@ export default class VelociousAttachmentResource extends FrontendModelBaseResour
130
161
  /**
131
162
  * Builds owner scope from a stored attachment row.
132
163
  * @param {VelociousAttachment} attachment - Attachment row.
133
- * @returns {{name: string, recordId: string, recordType: string}} - Owner scope.
164
+ * @returns {AttachmentOwnerScope} - Owner scope.
134
165
  */
135
166
  ownerScopeFromAttachment(attachment) {
136
167
  return {
137
168
  name: attachment.name(),
138
169
  recordId: attachment.recordId(),
139
- recordType: attachment.recordType()
170
+ recordType: attachment.recordType(),
171
+ resourceName: null
140
172
  }
141
173
  }
142
174
 
143
175
  /**
144
176
  * Checks whether the current ability can read the attachment owner.
145
- * @param {{name: string, recordId: string, recordType: string}} ownerScope - Owner scope.
177
+ * @param {AttachmentOwnerScope} ownerScope - Owner scope.
146
178
  * @returns {Promise<boolean>} - Whether owner is readable.
147
179
  */
148
180
  async attachmentOwnerAuthorized(ownerScope) {
149
181
  const controller = /** @type {import("../frontend-model-controller.js").default} */ (this.controllerInstance())
150
- const ownerResource = this.attachmentOwnerResource({controller, ownerScope})
182
+ const ownerResources = this.attachmentOwnerResources({controller, ownerScope})
151
183
 
152
- if (!ownerResource) {
153
- throw new Error(`No frontend model resource configured for attachment owner ${ownerScope.recordType}`)
184
+ if (ownerResources.length < 1) {
185
+ throw new Error(`No frontend model resource configured for attachment owner ${ownerScope.resourceName ?? ownerScope.recordType}`)
154
186
  }
155
187
 
156
- const ownerModelClass = controller.frontendModelResourceModelClass(ownerResource)
188
+ for (const ownerResource of ownerResources) {
189
+ const ownerModelClass = controller.frontendModelResourceModelClass(ownerResource)
190
+
191
+ await controller.ensureFrontendModelRecordClassInitialized(ownerModelClass)
192
+
193
+ const abilityAction = ownerResource.resourceConfiguration.abilities.find || ownerResource.resourceConfiguration.abilities.index || "read"
194
+ const primaryKey = ownerModelClass.primaryKey()
195
+ const ownerIdentity = modelPrimaryKeyValueFromCacheKey(primaryKey, ownerScope.recordId)
196
+ const owner = await ownerModelClass
197
+ .accessibleFor(abilityAction, this.ability)
198
+ .findBy(modelPrimaryKeyConditions(primaryKey, ownerIdentity))
157
199
 
158
- if (!ownerModelClass) {
159
- throw new Error(`No model class configured for attachment owner ${ownerScope.recordType}`)
200
+ if (owner) return true
160
201
  }
161
202
 
162
- const attachmentDefinitions = ownerModelClass.getAttachmentsMap()
203
+ return false
204
+ }
163
205
 
164
- if (!attachmentDefinitions[ownerScope.name]) {
165
- throw new Error(`No attachment '${ownerScope.name}' configured for ${ownerScope.recordType}`)
206
+ /**
207
+ * Resolves the explicit owner resource for scoped queries or every matching alias for member lookups.
208
+ * @param {object} args - Options object.
209
+ * @param {import("../frontend-model-controller.js").default} args.controller - Frontend-model controller.
210
+ * @param {AttachmentOwnerScope} args.ownerScope - Owner scope.
211
+ * @returns {AttachmentOwnerResource[]} - Matching owner resources.
212
+ */
213
+ attachmentOwnerResources({controller, ownerScope}) {
214
+ if (ownerScope.resourceName) {
215
+ const ownerResource = this.attachmentOwnerResource({
216
+ controller,
217
+ ownerScope: {...ownerScope, resourceName: ownerScope.resourceName}
218
+ })
219
+
220
+ return ownerResource ? [ownerResource] : []
166
221
  }
167
222
 
168
- await controller.ensureFrontendModelRecordClassInitialized(ownerModelClass)
223
+ const ownerResources = []
224
+
225
+ for (const backendProject of controller.getConfiguration().getBackendProjects()) {
226
+ for (const resourceName of Object.keys(frontendModelResourcesForBackendProject(backendProject))) {
227
+ const ownerResource = controller.frontendModelResourceConfigurationForBackendProjectModelName({backendProject, modelName: resourceName})
228
+
229
+ if (!ownerResource) continue
230
+
231
+ const ownerModelClass = controller.frontendModelResourceModelClass(ownerResource)
169
232
 
170
- const abilityAction = ownerResource.resourceConfiguration.abilities.find || ownerResource.resourceConfiguration.abilities.index || "read"
171
- const owner = await ownerModelClass
172
- .accessibleFor(abilityAction, this.ability)
173
- .findBy({[ownerModelClass.primaryKey()]: ownerScope.recordId})
233
+ if (ownerModelClass.getModelName() !== ownerScope.recordType) continue
234
+ if (!ownerResource.resourceConfiguration.attachments?.[ownerScope.name]) continue
174
235
 
175
- return Boolean(owner)
236
+ ownerResources.push(ownerResource)
237
+ }
238
+ }
239
+
240
+ return ownerResources
176
241
  }
177
242
 
178
243
  /**
179
244
  * Finds the frontend-model resource that owns an attachment scope.
180
245
  * @param {object} args - Options object.
181
246
  * @param {import("../frontend-model-controller.js").default} args.controller - Frontend-model controller.
182
- * @param {{name: string, recordId: string, recordType: string}} args.ownerScope - Owner scope.
183
- * @returns {{backendProject: import("../configuration-types.js").BackendProjectConfiguration, modelName: string, resourceClass: import("../configuration-types.js").FrontendModelResourceClassType, resourceConfiguration: import("../configuration-types.js").NormalizedFrontendModelResourceConfiguration} | null} - Owner resource configuration.
247
+ * @param {{name: string, recordId: string, recordType: string, resourceName: string}} args.ownerScope - Owner scope.
248
+ * @returns {AttachmentOwnerResource | null} - Owner resource configuration.
184
249
  */
185
250
  attachmentOwnerResource({controller, ownerScope}) {
186
251
  const backendProjects = controller.getConfiguration().getBackendProjects()
@@ -191,7 +256,7 @@ export default class VelociousAttachmentResource extends FrontendModelBaseResour
191
256
  for (const backendProject of backendProjects) {
192
257
  const ownerResource = controller.frontendModelResourceConfigurationForBackendProjectModelName({
193
258
  backendProject,
194
- modelName: ownerScope.recordType
259
+ modelName: ownerScope.resourceName
195
260
  })
196
261
 
197
262
  if (!ownerResource) continue
@@ -199,10 +264,12 @@ export default class VelociousAttachmentResource extends FrontendModelBaseResour
199
264
  const ownerModelClass = controller.frontendModelResourceModelClass(ownerResource)
200
265
 
201
266
  if (!ownerModelClass) {
202
- throw new Error(`No model class configured for attachment owner ${ownerScope.recordType}`)
267
+ throw new Error(`No model class configured for attachment owner ${ownerScope.resourceName}`)
203
268
  }
204
269
 
205
- const attachmentDefinitions = ownerModelClass.getAttachmentsMap()
270
+ if (ownerModelClass.getModelName() !== ownerScope.recordType) continue
271
+
272
+ const attachmentDefinitions = ownerResource.resourceConfiguration.attachments || {}
206
273
 
207
274
  if (attachmentDefinitions[ownerScope.name]) return ownerResource
208
275
 
@@ -210,7 +277,7 @@ export default class VelociousAttachmentResource extends FrontendModelBaseResour
210
277
  }
211
278
 
212
279
  if (resourceWithoutAttachment) {
213
- throw new Error(`No attachment '${ownerScope.name}' configured for ${ownerScope.recordType}`)
280
+ throw new Error(`No attachment '${ownerScope.name}' configured for ${ownerScope.resourceName}`)
214
281
  }
215
282
 
216
283
  return null