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
@@ -1,13 +1,20 @@
1
1
  // @ts-check
2
2
 
3
+ import {createHash} from "node:crypto"
3
4
  import UUID from "pure-uuid"
4
5
  import TableData from "../../table-data/index.js"
6
+ import TableIndex from "../../table-data/table-index.js"
7
+ import {modelPrimaryKeyCacheKey} from "../../../utils/model-primary-key.js"
5
8
  import normalizeRecordAttachmentInput from "./normalize-input.js"
6
9
 
7
10
  /**
8
11
  * AttachmentDriverConstructor type.
9
12
  * @typedef {import("../../../configuration-types.js").AttachmentDriverConstructor} AttachmentDriverConstructor */
10
13
  const ATTACHMENTS_TABLE = "velocious_attachments"
14
+ const ATTACHMENT_OWNER_INDEX_NAME = "index_velocious_attachments_on_record_type_and_record_id_digest"
15
+ const ATTACHMENT_RECORD_ID_DIGEST_LENGTH = 64
16
+ const ATTACHMENT_RECORD_ID_DIGEST_MIGRATION_BATCH_SIZE = 100
17
+ const ATTACHMENT_SCHEMA_LOCK_NAME = "velocious-attachments-schema"
11
18
 
12
19
  /**
13
20
  * Stores by configuration.
@@ -22,6 +29,45 @@ function generateUUID() {
22
29
  return new UUID(4).format()
23
30
  }
24
31
 
32
+ /**
33
+ * Returns the canonical stored owner identity for a model attachment.
34
+ * @param {import("../index.js").default} model - Attachment owner.
35
+ * @returns {string} - Canonical owner identity.
36
+ */
37
+ function attachmentRecordId(model) {
38
+ return modelPrimaryKeyCacheKey(model.getModelClass().primaryKey(), model.id())
39
+ }
40
+
41
+ /**
42
+ * Returns a bounded digest for indexed attachment owner lookups.
43
+ * @param {string} recordId - Canonical attachment owner identity.
44
+ * @returns {string} - SHA-256 digest.
45
+ */
46
+ function attachmentRecordIdDigest(recordId) {
47
+ return createHash("sha256").update(recordId).digest("hex")
48
+ }
49
+
50
+ /**
51
+ * Builds collision-safe attachment owner lookup conditions.
52
+ * @param {object} args - Owner lookup values.
53
+ * @param {string} [args.name] - Optional attachment name.
54
+ * @param {string} args.recordId - Canonical owner identity.
55
+ * @param {string} args.recordType - Owner model name.
56
+ * @returns {Record<string, string>} - Indexed digest and canonical identity conditions.
57
+ */
58
+ function attachmentOwnerConditions({name, recordId, recordType}) {
59
+ /** @type {Record<string, string>} */
60
+ const conditions = {
61
+ record_id: recordId,
62
+ record_id_digest: attachmentRecordIdDigest(recordId),
63
+ record_type: recordType
64
+ }
65
+
66
+ if (name !== undefined) conditions.name = name
67
+
68
+ return conditions
69
+ }
70
+
25
71
  /**
26
72
  * Runs store key for model.
27
73
  * @param {import("../index.js").default} model - Model instance.
@@ -36,12 +82,30 @@ function storeKeyForModel(model) {
36
82
  }
37
83
 
38
84
  /**
39
- * Runs the recordAttachmentsStoreForModel helper.
40
- * @param {import("../index.js").default} model - Model instance.
41
- * @returns {RecordAttachmentsStore} - Store instance.
85
+ * Returns the physical store key for an already-selected model connection.
86
+ * @param {typeof import("../index.js").default} modelClass - Model class.
87
+ * @param {import("../../drivers/base.js").default} connection - Selected physical connection.
88
+ * @returns {string} - Physical store key.
42
89
  */
43
- export function recordAttachmentsStoreForModel(model) {
44
- const configuration = model._getConfiguration()
90
+ function storeKeyForModelClass(modelClass, connection) {
91
+ const databaseIdentifier = modelClass.getDatabaseIdentifier()
92
+ const reuseKey = modelClass
93
+ ._getConfiguration()
94
+ .getDatabasePool(databaseIdentifier)
95
+ .getConnectionConfigurationReuseKey(connection)
96
+
97
+ return `${databaseIdentifier}:${reuseKey}`
98
+ }
99
+
100
+ /**
101
+ * Returns the shared attachment store for one configured database identity.
102
+ * @param {object} args - Store identity.
103
+ * @param {import("../../../configuration.js").default} args.configuration - Owning configuration.
104
+ * @param {string} args.databaseIdentifier - Logical database identifier.
105
+ * @param {string} args.storeKey - Physical store key.
106
+ * @returns {RecordAttachmentsStore} - Shared store instance.
107
+ */
108
+ function recordAttachmentsStore({configuration, databaseIdentifier, storeKey}) {
45
109
  let storesByDatabaseIdentifier = storesByConfiguration.get(configuration)
46
110
 
47
111
  if (!storesByDatabaseIdentifier) {
@@ -49,19 +113,46 @@ export function recordAttachmentsStoreForModel(model) {
49
113
  storesByConfiguration.set(configuration, storesByDatabaseIdentifier)
50
114
  }
51
115
 
52
- const key = storeKeyForModel(model)
53
- let store = storesByDatabaseIdentifier.get(key)
116
+ let store = storesByDatabaseIdentifier.get(storeKey)
54
117
 
55
118
  if (store) return store
56
119
 
57
- store = new RecordAttachmentsStore({
58
- configuration,
59
- databaseIdentifier: model.databaseOperation()?.databaseIdentifier() || model.getModelClass().getDatabaseIdentifier()
120
+ store = new RecordAttachmentsStore({configuration, databaseIdentifier})
121
+ storesByDatabaseIdentifier.set(storeKey, store)
122
+
123
+ return store
124
+ }
125
+
126
+ /**
127
+ * Returns the attachment store used before a model-level transaction starts.
128
+ * @param {typeof import("../index.js").default} modelClass - Model class opening the transaction.
129
+ * @param {import("../../drivers/base.js").default} connection - Selected physical connection.
130
+ * @returns {RecordAttachmentsStore} - Store instance.
131
+ */
132
+ export function recordAttachmentsStoreForModelClass(modelClass, connection) {
133
+ const databaseIdentifier = modelClass.getDatabaseIdentifier()
134
+
135
+ return recordAttachmentsStore({
136
+ configuration: modelClass._getConfiguration(),
137
+ databaseIdentifier,
138
+ storeKey: storeKeyForModelClass(modelClass, connection)
60
139
  })
140
+ }
61
141
 
62
- storesByDatabaseIdentifier.set(key, store)
142
+ /**
143
+ * Runs the recordAttachmentsStoreForModel helper.
144
+ * @param {import("../index.js").default} model - Model instance.
145
+ * @returns {RecordAttachmentsStore} - Store instance.
146
+ */
147
+ export function recordAttachmentsStoreForModel(model) {
148
+ const configuration = model._getConfiguration()
149
+ const databaseIdentifier = model.databaseOperation()?.databaseIdentifier() || model.getModelClass().getDatabaseIdentifier()
63
150
 
64
- return store
151
+ return recordAttachmentsStore({
152
+ configuration,
153
+ databaseIdentifier,
154
+ storeKey: storeKeyForModel(model)
155
+ })
65
156
  }
66
157
 
67
158
  /**
@@ -78,6 +169,9 @@ export default class RecordAttachmentsStore {
78
169
  this.configuration = configuration
79
170
  this.databaseIdentifier = databaseIdentifier
80
171
  this._readyPromise = null
172
+ this._schemaUpgradePromise = null
173
+ /** @type {number | undefined} */
174
+ this._schemaReadyGeneration = undefined
81
175
  this._driverColumnsAvailable = false
82
176
  this._contentBase64Nullable = true
83
177
  /**
@@ -120,8 +214,6 @@ export default class RecordAttachmentsStore {
120
214
  * @returns {Promise<void>} - Resolves when schema is ready.
121
215
  */
122
216
  async ensureSchema(db) {
123
- db.clearSchemaCache()
124
-
125
217
  if (await db.tableExists(ATTACHMENTS_TABLE)) {
126
218
  await this.ensureAttachmentStoreSchema({db})
127
219
  return
@@ -131,7 +223,8 @@ export default class RecordAttachmentsStore {
131
223
 
132
224
  table.string("id", {null: false, primaryKey: true})
133
225
  table.string("record_type", {null: false, index: true})
134
- table.string("record_id", {null: false, index: true})
226
+ table.text("record_id", {null: false})
227
+ table.string("record_id_digest", {maxLength: ATTACHMENT_RECORD_ID_DIGEST_LENGTH, null: false})
135
228
  table.string("name", {null: false, index: true})
136
229
  table.integer("position", {null: false})
137
230
  table.string("filename", {null: false})
@@ -142,10 +235,12 @@ export default class RecordAttachmentsStore {
142
235
  table.text("content_base64", {null: true})
143
236
  table.bigint("created_at_ms", {null: false})
144
237
  table.bigint("updated_at_ms", {null: false})
238
+ table.addIndex(new TableIndex(["record_type", "record_id_digest"], {name: ATTACHMENT_OWNER_INDEX_NAME}))
145
239
 
146
240
  await db.createTable(table)
147
241
  this._driverColumnsAvailable = true
148
242
  this._contentBase64Nullable = true
243
+ this._schemaReadyGeneration = this._schemaCacheGeneration(db)
149
244
  }
150
245
 
151
246
  /**
@@ -198,7 +293,7 @@ export default class RecordAttachmentsStore {
198
293
  if (persistenceFailed) {
199
294
  throw new AggregateError(
200
295
  [persistenceError, closeError],
201
- `Attachment persistence and path-source close both failed for ${model.getModelClass().getModelName()}#${String(model.id())} (${name})`,
296
+ `Attachment persistence and path-source close both failed for ${model.getModelClass().getModelName()}#${attachmentRecordId(model)} (${name})`,
202
297
  {cause: closeError}
203
298
  )
204
299
  }
@@ -242,7 +337,7 @@ export default class RecordAttachmentsStore {
242
337
  const attachmentDriverName = this._attachmentDriverNameFor({model, name})
243
338
  const now = Date.now()
244
339
  const recordType = model.getModelClass().getModelName()
245
- const recordId = String(model.id())
340
+ const recordId = attachmentRecordId(model)
246
341
  const attachmentId = generateUUID()
247
342
  /**
248
343
  * Written storage key.
@@ -270,7 +365,7 @@ export default class RecordAttachmentsStore {
270
365
  const existingRows = await db
271
366
  .newQuery()
272
367
  .from(ATTACHMENTS_TABLE)
273
- .where({name, record_id: recordId, record_type: recordType})
368
+ .where(attachmentOwnerConditions({name, recordId, recordType}))
274
369
  .results()
275
370
 
276
371
  for (const existingRow of existingRows) {
@@ -278,7 +373,7 @@ export default class RecordAttachmentsStore {
278
373
  }
279
374
 
280
375
  await db.delete({
281
- conditions: {name, record_id: recordId, record_type: recordType},
376
+ conditions: attachmentOwnerConditions({name, recordId, recordType}),
282
377
  tableName: ATTACHMENTS_TABLE
283
378
  })
284
379
  }
@@ -297,6 +392,7 @@ export default class RecordAttachmentsStore {
297
392
  name,
298
393
  position,
299
394
  record_id: recordId,
395
+ record_id_digest: attachmentRecordIdDigest(recordId),
300
396
  record_type: recordType,
301
397
  updated_at_ms: now
302
398
  }
@@ -354,14 +450,92 @@ export default class RecordAttachmentsStore {
354
450
  * @returns {Promise<void>} - Resolves when schema columns are ensured.
355
451
  */
356
452
  async ensureAttachmentStoreSchema({db}) {
453
+ if (this._schemaReadyGeneration === this._schemaCacheGeneration(db)) return
454
+ if (this._schemaUpgradePromise) return await this._schemaUpgradePromise
455
+
456
+ this._schemaUpgradePromise = (async () => {
457
+ const acquired = await db.acquireAdvisoryLock(ATTACHMENT_SCHEMA_LOCK_NAME)
458
+
459
+ if (!acquired) throw new Error(`Failed to acquire attachment schema lock ${ATTACHMENT_SCHEMA_LOCK_NAME}`)
460
+
461
+ try {
462
+ if (!await db.tableExists(ATTACHMENTS_TABLE)) return
463
+
464
+ await this._ensureAttachmentStoreSchema({db})
465
+ } finally {
466
+ await db.releaseAdvisoryLock(ATTACHMENT_SCHEMA_LOCK_NAME)
467
+ }
468
+ })()
469
+
470
+ try {
471
+ await this._schemaUpgradePromise
472
+ this._schemaReadyGeneration = this._schemaCacheGeneration(db)
473
+ } finally {
474
+ this._schemaUpgradePromise = null
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Returns the schema-cache generation for the connection's physical database.
480
+ * @param {import("../../../database/drivers/base.js").default} db - DB connection.
481
+ * @returns {number} - Current schema-cache generation.
482
+ */
483
+ _schemaCacheGeneration(db) {
484
+ const pool = this.configuration.getDatabasePool(this.databaseIdentifier)
485
+ const reuseKey = pool.getConnectionConfigurationReuseKey(db)
486
+
487
+ return this.configuration.schemaCacheGenerationForReuseKey(reuseKey)
488
+ }
489
+
490
+ /**
491
+ * Ensures attachment columns and indexes after schema-upgrade serialization is acquired.
492
+ * @param {object} args - Options.
493
+ * @param {import("../../../database/drivers/base.js").default} args.db - DB connection.
494
+ * @returns {Promise<void>} - Resolves when schema columns are ensured.
495
+ */
496
+ async _ensureAttachmentStoreSchema({db}) {
357
497
  const table = await db.getTableByNameOrFail(ATTACHMENTS_TABLE)
358
498
  const columns = await table.getColumns()
359
499
  const hasDriverColumn = columns.some((column) => column.getName() === "driver")
360
500
  const hasStorageKeyColumn = columns.some((column) => column.getName() === "storage_key")
361
501
  const contentBase64Column = columns.find((column) => column.getName() === "content_base64")
502
+ const recordIdColumn = columns.find((column) => column.getName() === "record_id")
503
+ const recordIdDigestColumn = columns.find((column) => column.getName() === "record_id_digest")
362
504
  const alterTable = new TableData(ATTACHMENTS_TABLE)
363
505
  let shouldAlter = false
364
506
 
507
+ if (!recordIdColumn) throw new Error(`${ATTACHMENTS_TABLE}.record_id is missing`)
508
+
509
+ const recordIdMaxLength = recordIdColumn.getMaxLength()
510
+
511
+ if (typeof recordIdMaxLength === "number" && recordIdMaxLength > 0) {
512
+ for (const index of await recordIdColumn.getIndexes()) {
513
+ if (index.isPrimaryKey()) continue
514
+
515
+ const indexName = index.getName()
516
+
517
+ if (!indexName) throw new Error(`Expected a name for ${ATTACHMENTS_TABLE}.record_id index`)
518
+
519
+ for (const sql of await db.removeIndexSQLs({name: indexName, tableName: ATTACHMENTS_TABLE})) {
520
+ await db.query(sql)
521
+ }
522
+ }
523
+
524
+ db.clearSchemaCache()
525
+ const recordIdAlterTable = new TableData(ATTACHMENTS_TABLE)
526
+
527
+ recordIdAlterTable.text("record_id", {
528
+ isNewColumn: false,
529
+ null: db.getType() === "pgsql" ? undefined : false
530
+ })
531
+
532
+ for (const sql of await db.alterTableSQLs(recordIdAlterTable)) {
533
+ await db.query(sql)
534
+ }
535
+
536
+ db.clearSchemaCache()
537
+ }
538
+
365
539
  if (!hasDriverColumn) {
366
540
  alterTable.string("driver", {null: true})
367
541
  shouldAlter = true
@@ -372,18 +546,113 @@ export default class RecordAttachmentsStore {
372
546
  shouldAlter = true
373
547
  }
374
548
 
549
+ if (!recordIdDigestColumn) {
550
+ alterTable.string("record_id_digest", {maxLength: ATTACHMENT_RECORD_ID_DIGEST_LENGTH, null: true})
551
+ shouldAlter = true
552
+ }
553
+
375
554
  if (shouldAlter) {
376
555
  const alterTableSQLs = await db.alterTableSQLs(alterTable)
377
556
 
378
557
  for (const sql of alterTableSQLs) {
379
558
  await db.query(sql)
380
559
  }
560
+
561
+ db.clearSchemaCache()
562
+ }
563
+
564
+ if (!recordIdDigestColumn || recordIdDigestColumn.getNull()) {
565
+ await this.backfillAttachmentRecordIdDigests(db)
566
+ await this.ensureAttachmentRecordIdDigestNotNull(db)
381
567
  }
382
568
 
569
+ await this.ensureAttachmentOwnerIndex(db)
570
+
383
571
  this._driverColumnsAvailable = true
384
572
  this._contentBase64Nullable = contentBase64Column ? contentBase64Column.getNull() : true
385
573
  }
386
574
 
575
+ /**
576
+ * Backfills bounded attachment owner digests in small batches.
577
+ * @param {import("../../../database/drivers/base.js").default} db - Database connection.
578
+ * @returns {Promise<void>} - Resolves when every existing row has a digest.
579
+ */
580
+ async backfillAttachmentRecordIdDigests(db) {
581
+ while (true) {
582
+ const rows = await db
583
+ .newQuery()
584
+ .from(ATTACHMENTS_TABLE)
585
+ .where({record_id_digest: null})
586
+ .limit(ATTACHMENT_RECORD_ID_DIGEST_MIGRATION_BATCH_SIZE)
587
+ .results()
588
+
589
+ for (const row of rows) {
590
+ if (typeof row.id !== "string" || typeof row.record_id !== "string") {
591
+ throw new Error(`Expected canonical attachment identity strings while backfilling ${ATTACHMENTS_TABLE}`)
592
+ }
593
+
594
+ await db.update({
595
+ conditions: {id: row.id},
596
+ data: {record_id_digest: attachmentRecordIdDigest(row.record_id)},
597
+ tableName: ATTACHMENTS_TABLE
598
+ })
599
+ }
600
+
601
+ if (rows.length < ATTACHMENT_RECORD_ID_DIGEST_MIGRATION_BATCH_SIZE) return
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Makes the backfilled attachment owner digest required.
607
+ * @param {import("../../../database/drivers/base.js").default} db - Database connection.
608
+ * @returns {Promise<void>} - Resolves when the digest column is non-nullable.
609
+ */
610
+ async ensureAttachmentRecordIdDigestNotNull(db) {
611
+ db.clearSchemaCache()
612
+ const table = await db.getTableByNameOrFail(ATTACHMENTS_TABLE)
613
+ const recordIdDigestColumn = await table.getColumnByNameOrFail("record_id_digest")
614
+
615
+ if (!recordIdDigestColumn.getNull()) return
616
+
617
+ const alterTable = new TableData(ATTACHMENTS_TABLE)
618
+
619
+ alterTable.string("record_id_digest", {
620
+ isNewColumn: false,
621
+ maxLength: ATTACHMENT_RECORD_ID_DIGEST_LENGTH,
622
+ null: false
623
+ })
624
+
625
+ for (const sql of await db.alterTableSQLs(alterTable)) await db.query(sql)
626
+
627
+ db.clearSchemaCache()
628
+ }
629
+
630
+ /**
631
+ * Ensures attachment owner queries retain a bounded composite index.
632
+ * @param {import("../../../database/drivers/base.js").default} db - Database connection.
633
+ * @returns {Promise<void>} - Resolves when the owner index exists.
634
+ */
635
+ async ensureAttachmentOwnerIndex(db) {
636
+ const table = await db.getTableByNameOrFail(ATTACHMENTS_TABLE)
637
+ const indexes = await table.getIndexes()
638
+ const ownerIndex = indexes.find((index) => {
639
+ const columnNames = index.getColumnNames()
640
+
641
+ return columnNames.length === 2 && columnNames[0] === "record_type" && columnNames[1] === "record_id_digest"
642
+ })
643
+
644
+ if (ownerIndex) return
645
+
646
+ for (const sql of await db.createIndexSQLs({
647
+ columns: ["record_type", "record_id_digest"],
648
+ ifNotExists: true,
649
+ name: ATTACHMENT_OWNER_INDEX_NAME,
650
+ tableName: ATTACHMENTS_TABLE
651
+ })) await db.query(sql)
652
+
653
+ db.clearSchemaCache()
654
+ }
655
+
387
656
  /**
388
657
  * Runs read attachment row.
389
658
  * @param {object} args - Options.
@@ -453,11 +722,11 @@ export default class RecordAttachmentsStore {
453
722
 
454
723
  return await this._withDb(async (db) => {
455
724
  const recordType = model.getModelClass().getModelName()
456
- const recordId = String(model.id())
725
+ const recordId = attachmentRecordId(model)
457
726
  let query = db
458
727
  .newQuery()
459
728
  .from(ATTACHMENTS_TABLE)
460
- .where({name, record_id: recordId, record_type: recordType})
729
+ .where(attachmentOwnerConditions({name, recordId, recordType}))
461
730
  .order("position ASC")
462
731
  .order("created_at_ms DESC")
463
732
  .limit(1)
@@ -484,11 +753,11 @@ export default class RecordAttachmentsStore {
484
753
 
485
754
  return await this._withDb(async (db) => {
486
755
  const recordType = model.getModelClass().getModelName()
487
- const recordId = String(model.id())
756
+ const recordId = attachmentRecordId(model)
488
757
  const query = db
489
758
  .newQuery()
490
759
  .from(ATTACHMENTS_TABLE)
491
- .where({name, record_id: recordId, record_type: recordType})
760
+ .where(attachmentOwnerConditions({name, recordId, recordType}))
492
761
  .order("position ASC")
493
762
  .order("created_at_ms ASC")
494
763
 
@@ -496,6 +765,47 @@ export default class RecordAttachmentsStore {
496
765
  }, model)
497
766
  }
498
767
 
768
+ /**
769
+ * Prepares attachment schema before a record transaction can migrate ownership.
770
+ * @param {object} args - Options.
771
+ * @param {import("../../drivers/base.js").default} args.connection - Record-owning database connection.
772
+ * @returns {Promise<void>} - Resolves when existing attachment schema is current.
773
+ */
774
+ async prepareRecordIdentityMigration({connection}) {
775
+ await this.ensureAttachmentStoreSchema({db: connection})
776
+ }
777
+
778
+ /**
779
+ * Moves every attachment row to a record's new primary-key identity.
780
+ * @param {object} args - Options.
781
+ * @param {import("../../drivers/base.js").default} args.connection - Transaction-owning database connection.
782
+ * @param {import("../index.js").default} args.model - Attachment owner after the key change.
783
+ * @param {import("../../../utils/model-primary-key.js").ModelPrimaryKeyValue} args.nextIdentity - New owner identity.
784
+ * @param {import("../../../utils/model-primary-key.js").ModelPrimaryKeyValue} args.previousIdentity - Persisted owner identity.
785
+ * @returns {Promise<void>} - Resolves after ownership is migrated.
786
+ */
787
+ async migrateRecordIdentity({connection, model, nextIdentity, previousIdentity}) {
788
+ const primaryKey = model.getModelClass().primaryKey()
789
+ const nextRecordId = modelPrimaryKeyCacheKey(primaryKey, nextIdentity)
790
+ const previousRecordId = modelPrimaryKeyCacheKey(primaryKey, previousIdentity)
791
+
792
+ if (nextRecordId === previousRecordId) return
793
+
794
+ if (!await connection.tableExists(ATTACHMENTS_TABLE)) return
795
+
796
+ await connection.update({
797
+ conditions: attachmentOwnerConditions({
798
+ recordId: previousRecordId,
799
+ recordType: model.getModelClass().getModelName()
800
+ }),
801
+ data: {
802
+ record_id: nextRecordId,
803
+ record_id_digest: attachmentRecordIdDigest(nextRecordId)
804
+ },
805
+ tableName: ATTACHMENTS_TABLE
806
+ })
807
+ }
808
+
499
809
  /**
500
810
  * Runs delete attachment row storage.
501
811
  * @param {object} args - Options.
@@ -535,12 +845,12 @@ export default class RecordAttachmentsStore {
535
845
 
536
846
  return await this._withDb(async (db) => {
537
847
  const recordType = model.getModelClass().getModelName()
538
- const recordId = String(model.id())
848
+ const recordId = attachmentRecordId(model)
539
849
  /** @type {Array<Record<string, ReturnType<typeof JSON.parse>>>} */
540
850
  const rows = await db
541
851
  .newQuery()
542
852
  .from(ATTACHMENTS_TABLE)
543
- .where({name, record_id: recordId, record_type: recordType})
853
+ .where(attachmentOwnerConditions({name, recordId, recordType}))
544
854
  .results()
545
855
 
546
856
  // Refuse to purge when any row's driver cannot delete its backing storage:
@@ -730,7 +1040,7 @@ export default class RecordAttachmentsStore {
730
1040
  const query = db
731
1041
  .newQuery()
732
1042
  .from(ATTACHMENTS_TABLE)
733
- .where({name, record_id: recordId, record_type: recordType})
1043
+ .where(attachmentOwnerConditions({name, recordId, recordType}))
734
1044
  .order("position DESC")
735
1045
  .limit(1)
736
1046
  const rows = await query.results()
@@ -2,6 +2,7 @@
2
2
 
3
3
  import UUID from "pure-uuid"
4
4
  import HasManyRelationship from "./relationships/has-many.js"
5
+ import {scalarModelPrimaryKey, scalarModelPrimaryKeyValue} from "../../utils/model-primary-key.js"
5
6
 
6
7
  /**
7
8
  * Global audit event bus matching ActiveRecordAuditable::Events.
@@ -648,6 +649,7 @@ async function createAuditWithCurrentConnection(record, args, modelClass) {
648
649
  const auditedChanges = args.auditedChanges === undefined ? null : args.auditedChanges
649
650
  const params = args.params === undefined ? null : args.params
650
651
  const currentDate = new Date()
652
+ const recordId = scalarModelPrimaryKeyValue(record.id(), `Auditing for ${modelClass.name}`)
651
653
 
652
654
  const auditActionId = await findOrCreateLookupId({
653
655
  columnName: "action",
@@ -667,7 +669,7 @@ async function createAuditWithCurrentConnection(record, args, modelClass) {
667
669
  tableName: tableData.tableName,
668
670
  data: {
669
671
  id: auditId,
670
- [`${modelKey}_id`]: record.id(),
672
+ [`${modelKey}_id`]: recordId,
671
673
  audit_action_id: auditActionId,
672
674
  audited_changes: auditedChanges,
673
675
  params,
@@ -691,7 +693,7 @@ async function createAuditWithCurrentConnection(record, args, modelClass) {
691
693
  id: auditId,
692
694
  audit_action_id: auditActionId,
693
695
  audit_auditable_type_id: auditAuditableTypeId,
694
- auditable_id: record.id(),
696
+ auditable_id: recordId,
695
697
  auditable_type: modelClass.getModelName(),
696
698
  audited_changes: auditedChanges,
697
699
  params,
@@ -836,7 +838,8 @@ function withoutAudit(modelClass, action) {
836
838
  const tableData = auditTableDataByModel.get(modelClass)?.get(databaseIdentity)
837
839
  const modelTableSql = db.quoteTable(modelClass.tableName())
838
840
  const auditActionsTableSql = db.quoteTable("audit_actions")
839
- const modelPrimaryKeySql = `${modelTableSql}.${db.quoteColumn(modelClass.primaryKey())}`
841
+ const primaryKey = scalarModelPrimaryKey(modelClass.primaryKey(), `withoutAudit for ${modelClass.name}`)
842
+ const modelPrimaryKeySql = `${modelTableSql}.${db.quoteColumn(primaryKey)}`
840
843
  const auditActionsIdSql = `${auditActionsTableSql}.${db.quoteColumn("id")}`
841
844
  const auditActionsActionSql = `${auditActionsTableSql}.${db.quoteColumn("action")}`
842
845