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
@@ -21,6 +21,7 @@ import { normalizeDateStringForWrite } from "./database/datetime-storage.js"
21
21
  import VelociousError from "./velocious-error.js"
22
22
  import isDate from "./utils/is-date.js"
23
23
  import isPlainObject from "./utils/plain-object.js"
24
+ import {compositeModelPrimaryKeyCohortSql, modelPrimaryKeyCacheKey, modelPrimaryKeyConditions, modelPrimaryKeyValueFromCacheKey, readModelPrimaryKeyValue, scalarModelPrimaryKey} from "./utils/model-primary-key.js"
24
25
  import {RansackQueryError, normalizeRansackGroup, parseRansackSort} from "./utils/ransack.js"
25
26
 
26
27
  /**
@@ -80,6 +81,8 @@ import {RansackQueryError, normalizeRansackGroup, parseRansackSort} from "./util
80
81
  * @returns {void}
81
82
  */
82
83
 
84
+ const ATTACHMENT_OWNER_KEY = "__attachmentOwner"
85
+
83
86
  /**
84
87
  * Runs normalize frontend model preload.
85
88
  * @param {import("./database/query/index.js").NestedPreloadRecord | string | string[] | boolean | undefined | null} preload - Preload shorthand.
@@ -821,6 +824,10 @@ export default class FrontendModelController extends Controller {
821
824
 
822
825
  if (!frontendModelResource) return null
823
826
 
827
+ if (this.frontendModelResourceModelClass(frontendModelResource) === modelClass) {
828
+ return frontendModelResource
829
+ }
830
+
824
831
  return this.frontendModelResourceConfigurationForBackendProjectModelName({
825
832
  backendProject: frontendModelResource.backendProject,
826
833
  modelName: modelClass.getModelName()
@@ -1047,10 +1054,10 @@ export default class FrontendModelController extends Controller {
1047
1054
 
1048
1055
  /**
1049
1056
  * Runs frontend model primary key.
1050
- * @returns {string} - Frontend model primary key.
1057
+ * @returns {import("./utils/model-primary-key.js").ModelPrimaryKeyDefinition} - Frontend model primary key.
1051
1058
  */
1052
1059
  frontendModelPrimaryKey() {
1053
- return this.frontendModelClass().primaryKey()
1060
+ return this.frontendModelResourceInstance().primaryKey()
1054
1061
  }
1055
1062
 
1056
1063
  /**
@@ -1086,41 +1093,95 @@ export default class FrontendModelController extends Controller {
1086
1093
  /**
1087
1094
  * Runs frontend model ability authorized query.
1088
1095
  * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} action - Frontend action.
1096
+ * @param {{ruleQueryFactory?: () => import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>}} [options] - Authorization query options.
1089
1097
  * @returns {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} - Authorized query for the action.
1090
1098
  */
1091
- frontendModelAbilityAuthorizedQuery(action) {
1099
+ frontendModelAbilityAuthorizedQuery(action, {ruleQueryFactory} = {}) {
1092
1100
  const abilityAction = this.frontendModelAbilityAction(action)
1101
+ const modelClass = this.frontendModelClass()
1102
+
1103
+ if (!ruleQueryFactory) return modelClass.accessibleFor(abilityAction, this.currentAbility())
1104
+
1105
+ const ability = this.currentAbility()
1093
1106
 
1094
- return this.frontendModelClass().accessibleFor(abilityAction, this.currentAbility())
1107
+ if (!ability) {
1108
+ throw new Error(`No ability in context for ${modelClass.name}. Configure an ability resolver on the request`)
1109
+ }
1110
+
1111
+ return ability.applyToQuery({
1112
+ action: abilityAction,
1113
+ modelClass,
1114
+ query: modelClass._newQuery(),
1115
+ ruleQueryFactory
1116
+ })
1095
1117
  }
1096
1118
 
1097
1119
  /**
1098
1120
  * Runs frontend model authorized query.
1099
1121
  * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} action - Frontend action.
1122
+ * @param {{ruleQueryFactory?: () => import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>}} [options] - Authorization query options.
1100
1123
  * @returns {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} - Authorized query for the action.
1101
1124
  */
1102
- frontendModelAuthorizedQuery(action) {
1125
+ frontendModelAuthorizedQuery(action, options = {}) {
1103
1126
  const resource = this.frontendModelResourceInstance()
1104
1127
 
1105
1128
  if (resource.authorizedQuery !== FrontendModelBaseResource.prototype.authorizedQuery) {
1106
- return resource.authorizedQuery(action)
1129
+ return resource.authorizedQuery(action, options)
1107
1130
  }
1108
1131
 
1109
- return this.frontendModelAbilityAuthorizedQuery(action)
1132
+ return this.frontendModelAbilityAuthorizedQuery(action, options)
1110
1133
  }
1111
1134
 
1112
1135
  /**
1113
1136
  * Runs frontend model primary key value.
1114
1137
  * @param {import("./database/record/index.js").default} model - Model instance.
1115
- * @returns {string} - Primary key value as string.
1138
+ * @returns {import("./utils/model-primary-key.js").ModelPrimaryKeyValue} - Primary key value.
1116
1139
  */
1117
1140
  frontendModelPrimaryKeyValue(model) {
1118
- const columnName = this.frontendModelPrimaryKey()
1119
- const attributeNameMap = model.getModelClass().getColumnNameToAttributeNameMap()
1120
- const attributeName = attributeNameMap[columnName] || columnName
1121
- const value = model.readAttribute(attributeName)
1141
+ const primaryKey = this.frontendModelPrimaryKey()
1142
+
1143
+ return readModelPrimaryKeyValue(primaryKey, (attributeName) => model.readAttribute(attributeName))
1144
+ }
1122
1145
 
1123
- return String(value)
1146
+ /**
1147
+ * Returns the authorized identities from a candidate cohort without per-record queries.
1148
+ * @param {object} args - Arguments.
1149
+ * @param {import("./utils/model-primary-key.js").ModelPrimaryKeyValue[]} args.identities - Candidate identities.
1150
+ * @param {typeof import("./database/record/index.js").default} args.modelClass - Model class owning the identity attributes.
1151
+ * @param {import("./utils/model-primary-key.js").ModelPrimaryKeyDefinition} args.primaryKey - Identity definition.
1152
+ * @param {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} args.query - Authorized query.
1153
+ * @returns {Promise<Set<string>>} - Canonical authorized identity keys.
1154
+ */
1155
+ async frontendModelAuthorizedIdentitySet({identities, modelClass, primaryKey, query}) {
1156
+ if (!Array.isArray(primaryKey)) {
1157
+ const authorizedIds = await query
1158
+ .where({[primaryKey]: identities})
1159
+ .pluck(primaryKey)
1160
+
1161
+ return new Set(authorizedIds.map((value) => modelPrimaryKeyCacheKey(primaryKey, value)))
1162
+ }
1163
+
1164
+ const maxCompositeCohortSize = Math.max(1, Math.floor(query.driver.maxInClauseValues() / primaryKey.length))
1165
+ const cohorts = query.driver.chunkValues(identities, (cohort) => query
1166
+ .clone()
1167
+ .where(compositeModelPrimaryKeyCohortSql({modelClass, primaryKey, query, values: cohort}))
1168
+ .toSql(), {maxCount: maxCompositeCohortSize})
1169
+ const authorizedIdentityKeys = new Set()
1170
+
1171
+ for (const cohort of cohorts) {
1172
+ const authorizedModels = await query
1173
+ .clone()
1174
+ .where(compositeModelPrimaryKeyCohortSql({modelClass, primaryKey, query, values: cohort}))
1175
+ .toArray()
1176
+
1177
+ for (const model of authorizedModels) {
1178
+ const identity = readModelPrimaryKeyValue(primaryKey, (attributeName) => model.readAttribute(attributeName))
1179
+
1180
+ authorizedIdentityKeys.add(modelPrimaryKeyCacheKey(primaryKey, identity))
1181
+ }
1182
+ }
1183
+
1184
+ return authorizedIdentityKeys
1124
1185
  }
1125
1186
 
1126
1187
  /**
@@ -1134,14 +1195,16 @@ export default class FrontendModelController extends Controller {
1134
1195
  if (models.length === 0) return models
1135
1196
 
1136
1197
  const primaryKey = this.frontendModelPrimaryKey()
1137
- const ids = models.map((model) => this.frontendModelPrimaryKeyValue(model))
1138
- const authorizedQuery = this.frontendModelAuthorizedQuery(action).where({[primaryKey]: ids})
1139
-
1140
- const authorizedIdsRaw = await authorizedQuery.pluck(primaryKey)
1141
1198
 
1142
- const authorizedIds = new Set(authorizedIdsRaw.map((id) => String(id)))
1199
+ const identities = models.map((model) => this.frontendModelPrimaryKeyValue(model))
1200
+ const authorizedIds = await this.frontendModelAuthorizedIdentitySet({
1201
+ identities,
1202
+ modelClass: this.frontendModelClass(),
1203
+ primaryKey,
1204
+ query: this.frontendModelAuthorizedQuery(action)
1205
+ })
1143
1206
 
1144
- return models.filter((model) => authorizedIds.has(this.frontendModelPrimaryKeyValue(model)))
1207
+ return models.filter((model) => authorizedIds.has(modelPrimaryKeyCacheKey(primaryKey, this.frontendModelPrimaryKeyValue(model))))
1145
1208
  }
1146
1209
 
1147
1210
  /**
@@ -1158,7 +1221,7 @@ export default class FrontendModelController extends Controller {
1158
1221
  /**
1159
1222
  * Runs frontend model find record.
1160
1223
  * @param {"find" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url"} action - Frontend action.
1161
- * @param {string | number} id - Record id.
1224
+ * @param {import("./utils/model-primary-key.js").ModelPrimaryKeyValue} id - Record id.
1162
1225
  * @returns {Promise<import("./database/record/index.js").default | null>} - Located model record.
1163
1226
  */
1164
1227
  async frontendModelFindRecord(action, id) {
@@ -1471,17 +1534,21 @@ export default class FrontendModelController extends Controller {
1471
1534
  if (candidates.length === 0) continue
1472
1535
 
1473
1536
  const primaryKey = modelClass.primaryKey()
1474
- const ids = candidates
1475
- .map((record) => record.readAttribute(primaryKey))
1476
- .filter((value) => value !== null && value !== undefined)
1477
- if (ids.length === 0) continue
1478
1537
 
1479
1538
  for (const action of entry.actions) {
1539
+ /** @type {Set<string>} */
1480
1540
  let allowedIds
1481
1541
  try {
1482
- const authorizedQuery = modelClass.accessibleFor(action, ability).where({[primaryKey]: ids})
1483
- const plucked = await authorizedQuery.pluck(primaryKey)
1484
- allowedIds = new Set(plucked.map((value) => String(value)))
1542
+ const authorizedQuery = modelClass.accessibleFor(action, ability)
1543
+
1544
+ const identities = candidates.map((record) => record.id())
1545
+
1546
+ allowedIds = await this.frontendModelAuthorizedIdentitySet({
1547
+ identities,
1548
+ modelClass,
1549
+ primaryKey,
1550
+ query: authorizedQuery
1551
+ })
1485
1552
  } catch (error) {
1486
1553
  // An ability with no allow rules for the action throws via
1487
1554
  // `accessibleFor`; treat as a universal deny so the frontend
@@ -1492,8 +1559,8 @@ export default class FrontendModelController extends Controller {
1492
1559
  }
1493
1560
 
1494
1561
  for (const record of candidates) {
1495
- const idValue = record.readAttribute(primaryKey)
1496
- const allowed = idValue !== null && idValue !== undefined && allowedIds.has(String(idValue))
1562
+ const idValue = record.id()
1563
+ const allowed = allowedIds.has(modelPrimaryKeyCacheKey(primaryKey, idValue))
1497
1564
  record._setComputedAbility(action, allowed)
1498
1565
  }
1499
1566
  }
@@ -1659,7 +1726,7 @@ export default class FrontendModelController extends Controller {
1659
1726
  */
1660
1727
  frontendModelMssqlDistinctByPrimaryKeyQuery({query}) {
1661
1728
  const modelClass = this.frontendModelClass()
1662
- const primaryKey = modelClass.primaryKey()
1729
+ const primaryKey = scalarModelPrimaryKey(modelClass.primaryKey(), "MSSQL distinct frontend-model queries")
1663
1730
  const rootTableSql = query.driver.quoteTable(modelClass.tableName())
1664
1731
  const primaryKeySql = `${rootTableSql}.${query.driver.quoteColumn(primaryKey)}`
1665
1732
  const distinctIdsQuery = query.clone()
@@ -2972,7 +3039,7 @@ export default class FrontendModelController extends Controller {
2972
3039
  const authorizedIdsByClass = new Map()
2973
3040
  /**
2974
3041
  * Primary keys by class.
2975
- * @type {Map<typeof import("./database/record/index.js").default, string>} */
3042
+ * @type {Map<typeof import("./database/record/index.js").default, import("./utils/model-primary-key.js").ModelPrimaryKeyDefinition>} */
2976
3043
  const primaryKeysByClass = new Map()
2977
3044
 
2978
3045
  for (const [relatedModelClass, relatedModels] of modelsByClass.entries()) {
@@ -2993,22 +3060,16 @@ export default class FrontendModelController extends Controller {
2993
3060
  }
2994
3061
 
2995
3062
  const primaryKey = relatedModelClass.primaryKey()
2996
- const ids = relatedModels
2997
- .map((model) => model.attributes()[primaryKey])
2998
- .filter((id) => id !== undefined && id !== null)
2999
-
3000
- if (ids.length < 1) {
3001
- authorizedIdsByClass.set(relatedModelClass, new Set())
3002
- continue
3003
- }
3004
-
3005
- const authorizedIdsRaw = await relatedModelClass
3006
- .accessibleFor(abilityAction)
3007
- .where({[primaryKey]: ids})
3008
- .pluck(primaryKey)
3063
+ const identities = relatedModels.map((model) => model.id())
3064
+ const authorizedIds = await this.frontendModelAuthorizedIdentitySet({
3065
+ identities,
3066
+ modelClass: relatedModelClass,
3067
+ primaryKey,
3068
+ query: relatedModelClass.accessibleFor(abilityAction)
3069
+ })
3009
3070
 
3010
3071
  primaryKeysByClass.set(relatedModelClass, primaryKey)
3011
- authorizedIdsByClass.set(relatedModelClass, new Set(authorizedIdsRaw.map((id) => String(id))))
3072
+ authorizedIdsByClass.set(relatedModelClass, authorizedIds)
3012
3073
  }
3013
3074
 
3014
3075
  return models.filter((model) => {
@@ -3018,11 +3079,7 @@ export default class FrontendModelController extends Controller {
3018
3079
 
3019
3080
  if (!authorizedIds || !primaryKey) return false
3020
3081
 
3021
- const primaryKeyValue = model.attributes()[primaryKey]
3022
-
3023
- if (primaryKeyValue === undefined || primaryKeyValue === null) return false
3024
-
3025
- return authorizedIds.has(String(primaryKeyValue))
3082
+ return authorizedIds.has(modelPrimaryKeyCacheKey(primaryKey, model.id()))
3026
3083
  })
3027
3084
  }
3028
3085
 
@@ -3141,8 +3198,10 @@ export default class FrontendModelController extends Controller {
3141
3198
  const hasQueryData = Object.keys(queryDataValues).length > 0
3142
3199
  const hasAbilities = Object.keys(computedAbilities).length > 0
3143
3200
  const hasPreloaded = Object.keys(preloadedRelationships).length > 0
3201
+ const resource = this._serializationResourceInstanceForModel(model)
3202
+ const hasAttachmentOwner = Object.keys(resource?.resourceConfiguration().attachments || {}).length > 0
3144
3203
 
3145
- if (!hasPreloaded && !hasCounts && !hasQueryData && !hasAbilities) {
3204
+ if (!hasPreloaded && !hasCounts && !hasQueryData && !hasAbilities && !hasAttachmentOwner) {
3146
3205
  serializedModels.push(serializedAttributes)
3147
3206
  continue
3148
3207
  }
@@ -3156,6 +3215,17 @@ export default class FrontendModelController extends Controller {
3156
3215
  if (hasCounts) serialized.__associationCounts = associationCounts
3157
3216
  if (hasQueryData) serialized.__queryData = queryDataValues
3158
3217
  if (hasAbilities) serialized.__abilities = computedAbilities
3218
+ if (hasAttachmentOwner) {
3219
+ if (!resource) throw new Error(`Missing frontend model resource for attachment owner ${model.getModelClass().getModelName()}`)
3220
+
3221
+ const modelClass = model.getModelClass()
3222
+
3223
+ serialized[ATTACHMENT_OWNER_KEY] = {
3224
+ recordId: modelPrimaryKeyCacheKey(modelClass.primaryKey(), model.id()),
3225
+ recordType: modelClass.getModelName(),
3226
+ resourceName: resource.modelName()
3227
+ }
3228
+ }
3159
3229
 
3160
3230
  serializedModels.push(serialized)
3161
3231
  }
@@ -3465,7 +3535,8 @@ export default class FrontendModelController extends Controller {
3465
3535
 
3466
3536
  const params = this.frontendModelParams()
3467
3537
  const modelClass = this.frontendModelClass()
3468
- const id = params.id
3538
+ const primaryKey = this.frontendModelPrimaryKey()
3539
+ let id = params.id
3469
3540
 
3470
3541
  if (action === "create") {
3471
3542
  const mutationAttributes = frontendModelMutationAttributes(params)
@@ -3486,10 +3557,22 @@ export default class FrontendModelController extends Controller {
3486
3557
  return frontendModelSerializedModelSuccess(serializedModel)
3487
3558
  }
3488
3559
 
3489
- if ((typeof id !== "string" && typeof id !== "number") || `${id}`.length < 1) {
3560
+ if (!Array.isArray(primaryKey) && ((typeof id !== "string" && typeof id !== "number") || `${id}`.length < 1)) {
3490
3561
  return this.frontendModelErrorPayload("Expected model id.", {errorType: "validation_error"})
3491
3562
  }
3492
3563
 
3564
+ try {
3565
+ if (Array.isArray(primaryKey) && typeof id === "string") {
3566
+ id = modelPrimaryKeyValueFromCacheKey(primaryKey, id)
3567
+ }
3568
+
3569
+ modelPrimaryKeyConditions(primaryKey, id)
3570
+ } catch (error) {
3571
+ if (!(error instanceof TypeError)) throw error
3572
+
3573
+ return this.frontendModelErrorPayload(error.message, {errorType: "validation_error"})
3574
+ }
3575
+
3493
3576
  if (action === "attach") {
3494
3577
  const attachmentName = params.attachmentName
3495
3578
  const attachmentInput = params.attachment