velocious 1.0.567 → 1.0.569

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 (247) hide show
  1. package/README.md +49 -1
  2. package/build/configuration.js +36 -0
  3. package/build/database/drivers/base-table.js +4 -3
  4. package/build/database/drivers/base.js +195 -54
  5. package/build/database/drivers/index-metadata.js +28 -0
  6. package/build/database/drivers/mssql/column.js +2 -34
  7. package/build/database/drivers/mssql/columns-index.js +37 -0
  8. package/build/database/drivers/mssql/index.js +10 -7
  9. package/build/database/drivers/mssql/table.js +55 -11
  10. package/build/database/drivers/mysql/index.js +10 -5
  11. package/build/database/drivers/mysql/sql/alter-table.js +13 -0
  12. package/build/database/drivers/pgsql/column.js +2 -27
  13. package/build/database/drivers/pgsql/columns-index.js +37 -0
  14. package/build/database/drivers/pgsql/index.js +4 -4
  15. package/build/database/drivers/pgsql/table.js +61 -10
  16. package/build/database/drivers/sqlite/base.js +2 -2
  17. package/build/database/drivers/sqlite/connection-sql-js.js +62 -2
  18. package/build/database/drivers/sqlite/index.web.js +74 -0
  19. package/build/database/drivers/sqlite/sql/alter-table.js +18 -0
  20. package/build/database/migration/index.js +92 -3
  21. package/build/database/operation-connection.js +110 -0
  22. package/build/database/operation-lease.js +47 -0
  23. package/build/database/operation.js +140 -0
  24. package/build/database/pool/base.js +26 -1
  25. package/build/database/pool/single-multi-use.js +49 -0
  26. package/build/database/query/alter-table-base.js +20 -1
  27. package/build/database/query/model-class-query.js +38 -7
  28. package/build/database/query/preloader/belongs-to.js +3 -2
  29. package/build/database/query/preloader/has-many.js +4 -3
  30. package/build/database/query/preloader/has-one.js +2 -1
  31. package/build/database/query/preloader/query-for-model.js +17 -0
  32. package/build/database/query/query-data.js +12 -3
  33. package/build/database/query/with-count.js +5 -3
  34. package/build/database/record/acts-as-list.js +11 -8
  35. package/build/database/record/attachments/normalize-input.js +38 -10
  36. package/build/database/record/attachments/storage-drivers/filesystem.js +33 -2
  37. package/build/database/record/attachments/storage-drivers/native.js +17 -2
  38. package/build/database/record/attachments/storage-drivers/s3.js +27 -7
  39. package/build/database/record/attachments/store.js +173 -53
  40. package/build/database/record/auditing.js +12 -9
  41. package/build/database/record/counter-cache-magnitude.js +5 -3
  42. package/build/database/record/index.js +95 -14
  43. package/build/database/record/instance-relationships/belongs-to.js +4 -2
  44. package/build/database/record/instance-relationships/has-many.js +7 -5
  45. package/build/database/record/instance-relationships/has-one.js +4 -2
  46. package/build/database/record/validators/uniqueness.js +3 -2
  47. package/build/database/table-data/table-foreign-key.js +9 -1
  48. package/build/environment-handlers/base.js +1 -1
  49. package/build/environment-handlers/node/attachment-path-source.js +144 -0
  50. package/build/environment-handlers/node.js +28 -3
  51. package/build/frontend-models/websocket-publishers.js +2 -2
  52. package/build/src/configuration.d.ts +12 -0
  53. package/build/src/configuration.d.ts.map +1 -1
  54. package/build/src/configuration.js +35 -1
  55. package/build/src/database/drivers/base-table.d.ts.map +1 -1
  56. package/build/src/database/drivers/base-table.js +4 -3
  57. package/build/src/database/drivers/base.d.ts +73 -20
  58. package/build/src/database/drivers/base.d.ts.map +1 -1
  59. package/build/src/database/drivers/base.js +181 -54
  60. package/build/src/database/drivers/index-metadata.d.ts +41 -0
  61. package/build/src/database/drivers/index-metadata.d.ts.map +1 -0
  62. package/build/src/database/drivers/index-metadata.js +26 -0
  63. package/build/src/database/drivers/mssql/column.d.ts.map +1 -1
  64. package/build/src/database/drivers/mssql/column.js +3 -31
  65. package/build/src/database/drivers/mssql/columns-index.d.ts +41 -0
  66. package/build/src/database/drivers/mssql/columns-index.d.ts.map +1 -1
  67. package/build/src/database/drivers/mssql/columns-index.js +35 -1
  68. package/build/src/database/drivers/mssql/index.d.ts +4 -1
  69. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  70. package/build/src/database/drivers/mssql/index.js +11 -8
  71. package/build/src/database/drivers/mssql/table.d.ts +42 -0
  72. package/build/src/database/drivers/mssql/table.d.ts.map +1 -1
  73. package/build/src/database/drivers/mssql/table.js +49 -12
  74. package/build/src/database/drivers/mysql/index.d.ts +2 -1
  75. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  76. package/build/src/database/drivers/mysql/index.js +10 -6
  77. package/build/src/database/drivers/mysql/sql/alter-table.d.ts.map +1 -1
  78. package/build/src/database/drivers/mysql/sql/alter-table.js +12 -1
  79. package/build/src/database/drivers/pgsql/column.d.ts.map +1 -1
  80. package/build/src/database/drivers/pgsql/column.js +3 -24
  81. package/build/src/database/drivers/pgsql/columns-index.d.ts +41 -0
  82. package/build/src/database/drivers/pgsql/columns-index.d.ts.map +1 -1
  83. package/build/src/database/drivers/pgsql/columns-index.js +35 -1
  84. package/build/src/database/drivers/pgsql/index.d.ts +2 -1
  85. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  86. package/build/src/database/drivers/pgsql/index.js +5 -5
  87. package/build/src/database/drivers/pgsql/table.d.ts +42 -0
  88. package/build/src/database/drivers/pgsql/table.d.ts.map +1 -1
  89. package/build/src/database/drivers/pgsql/table.js +54 -11
  90. package/build/src/database/drivers/sqlite/base.d.ts +1 -1
  91. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  92. package/build/src/database/drivers/sqlite/base.js +3 -3
  93. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +20 -0
  94. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
  95. package/build/src/database/drivers/sqlite/connection-sql-js.js +56 -3
  96. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  97. package/build/src/database/drivers/sqlite/index.web.js +64 -1
  98. package/build/src/database/drivers/sqlite/sql/alter-table.d.ts.map +1 -1
  99. package/build/src/database/drivers/sqlite/sql/alter-table.js +15 -1
  100. package/build/src/database/migration/index.d.ts +35 -1
  101. package/build/src/database/migration/index.d.ts.map +1 -1
  102. package/build/src/database/migration/index.js +78 -4
  103. package/build/src/database/operation-connection.d.ts +61 -0
  104. package/build/src/database/operation-connection.d.ts.map +1 -0
  105. package/build/src/database/operation-connection.js +93 -0
  106. package/build/src/database/operation-lease.d.ts +27 -0
  107. package/build/src/database/operation-lease.d.ts.map +1 -0
  108. package/build/src/database/operation-lease.js +44 -0
  109. package/build/src/database/operation.d.ts +77 -0
  110. package/build/src/database/operation.d.ts.map +1 -0
  111. package/build/src/database/operation.js +123 -0
  112. package/build/src/database/pool/base.d.ts +14 -0
  113. package/build/src/database/pool/base.d.ts.map +1 -1
  114. package/build/src/database/pool/base.js +24 -2
  115. package/build/src/database/pool/single-multi-use.d.ts +1 -0
  116. package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
  117. package/build/src/database/pool/single-multi-use.js +49 -1
  118. package/build/src/database/query/alter-table-base.d.ts +6 -0
  119. package/build/src/database/query/alter-table-base.d.ts.map +1 -1
  120. package/build/src/database/query/alter-table-base.js +18 -2
  121. package/build/src/database/query/model-class-query.d.ts +14 -0
  122. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  123. package/build/src/database/query/model-class-query.js +32 -8
  124. package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
  125. package/build/src/database/query/preloader/belongs-to.js +4 -3
  126. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  127. package/build/src/database/query/preloader/has-many.js +5 -4
  128. package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
  129. package/build/src/database/query/preloader/has-one.js +3 -2
  130. package/build/src/database/query/preloader/query-for-model.d.ts +10 -0
  131. package/build/src/database/query/preloader/query-for-model.d.ts.map +1 -0
  132. package/build/src/database/query/preloader/query-for-model.js +16 -0
  133. package/build/src/database/query/query-data.d.ts.map +1 -1
  134. package/build/src/database/query/query-data.js +13 -4
  135. package/build/src/database/query/with-count.d.ts.map +1 -1
  136. package/build/src/database/query/with-count.js +6 -4
  137. package/build/src/database/record/acts-as-list.js +12 -9
  138. package/build/src/database/record/attachments/attachment-record.d.ts +9 -0
  139. package/build/src/database/record/attachments/attachment-record.d.ts.map +1 -1
  140. package/build/src/database/record/attachments/normalize-input.d.ts +33 -4
  141. package/build/src/database/record/attachments/normalize-input.d.ts.map +1 -1
  142. package/build/src/database/record/attachments/normalize-input.js +39 -11
  143. package/build/src/database/record/attachments/storage-drivers/filesystem.d.ts +2 -5
  144. package/build/src/database/record/attachments/storage-drivers/filesystem.d.ts.map +1 -1
  145. package/build/src/database/record/attachments/storage-drivers/filesystem.js +28 -3
  146. package/build/src/database/record/attachments/storage-drivers/native.d.ts +2 -6
  147. package/build/src/database/record/attachments/storage-drivers/native.d.ts.map +1 -1
  148. package/build/src/database/record/attachments/storage-drivers/native.js +14 -3
  149. package/build/src/database/record/attachments/storage-drivers/s3.d.ts +2 -6
  150. package/build/src/database/record/attachments/storage-drivers/s3.d.ts.map +1 -1
  151. package/build/src/database/record/attachments/storage-drivers/s3.js +29 -8
  152. package/build/src/database/record/attachments/store.d.ts +31 -2
  153. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  154. package/build/src/database/record/attachments/store.js +158 -54
  155. package/build/src/database/record/auditing.d.ts.map +1 -1
  156. package/build/src/database/record/auditing.js +13 -10
  157. package/build/src/database/record/counter-cache-magnitude.js +6 -4
  158. package/build/src/database/record/index.d.ts +42 -2
  159. package/build/src/database/record/index.d.ts.map +1 -1
  160. package/build/src/database/record/index.js +87 -15
  161. package/build/src/database/record/instance-relationships/belongs-to.d.ts.map +1 -1
  162. package/build/src/database/record/instance-relationships/belongs-to.js +4 -3
  163. package/build/src/database/record/instance-relationships/has-many.d.ts.map +1 -1
  164. package/build/src/database/record/instance-relationships/has-many.js +7 -5
  165. package/build/src/database/record/instance-relationships/has-one.d.ts.map +1 -1
  166. package/build/src/database/record/instance-relationships/has-one.js +4 -3
  167. package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
  168. package/build/src/database/record/validators/uniqueness.js +4 -3
  169. package/build/src/database/table-data/table-foreign-key.d.ts +9 -1
  170. package/build/src/database/table-data/table-foreign-key.d.ts.map +1 -1
  171. package/build/src/database/table-data/table-foreign-key.js +9 -2
  172. package/build/src/environment-handlers/base.d.ts +2 -5
  173. package/build/src/environment-handlers/base.d.ts.map +1 -1
  174. package/build/src/environment-handlers/base.js +2 -2
  175. package/build/src/environment-handlers/node/attachment-path-source.d.ts +64 -0
  176. package/build/src/environment-handlers/node/attachment-path-source.d.ts.map +1 -0
  177. package/build/src/environment-handlers/node/attachment-path-source.js +116 -0
  178. package/build/src/environment-handlers/node.d.ts +12 -0
  179. package/build/src/environment-handlers/node.d.ts.map +1 -1
  180. package/build/src/environment-handlers/node.js +24 -4
  181. package/build/src/frontend-models/websocket-publishers.js +3 -3
  182. package/build/src/sync/server-sequence-allocator.d.ts +16 -4
  183. package/build/src/sync/server-sequence-allocator.d.ts.map +1 -1
  184. package/build/src/sync/server-sequence-allocator.js +37 -11
  185. package/build/src/sync/sync-client.d.ts.map +1 -1
  186. package/build/src/sync/sync-client.js +7 -3
  187. package/build/src/sync/sync-publisher.d.ts +2 -1
  188. package/build/src/sync/sync-publisher.d.ts.map +1 -1
  189. package/build/src/sync/sync-publisher.js +11 -6
  190. package/build/sync/server-sequence-allocator.js +43 -10
  191. package/build/sync/sync-client.js +6 -2
  192. package/build/sync/sync-publisher.js +10 -5
  193. package/build/tsconfig.tsbuildinfo +1 -1
  194. package/package.json +1 -1
  195. package/src/configuration.js +36 -0
  196. package/src/database/drivers/base-table.js +4 -3
  197. package/src/database/drivers/base.js +195 -54
  198. package/src/database/drivers/index-metadata.js +28 -0
  199. package/src/database/drivers/mssql/column.js +2 -34
  200. package/src/database/drivers/mssql/columns-index.js +37 -0
  201. package/src/database/drivers/mssql/index.js +10 -7
  202. package/src/database/drivers/mssql/table.js +55 -11
  203. package/src/database/drivers/mysql/index.js +10 -5
  204. package/src/database/drivers/mysql/sql/alter-table.js +13 -0
  205. package/src/database/drivers/pgsql/column.js +2 -27
  206. package/src/database/drivers/pgsql/columns-index.js +37 -0
  207. package/src/database/drivers/pgsql/index.js +4 -4
  208. package/src/database/drivers/pgsql/table.js +61 -10
  209. package/src/database/drivers/sqlite/base.js +2 -2
  210. package/src/database/drivers/sqlite/connection-sql-js.js +62 -2
  211. package/src/database/drivers/sqlite/index.web.js +74 -0
  212. package/src/database/drivers/sqlite/sql/alter-table.js +18 -0
  213. package/src/database/migration/index.js +92 -3
  214. package/src/database/operation-connection.js +110 -0
  215. package/src/database/operation-lease.js +47 -0
  216. package/src/database/operation.js +140 -0
  217. package/src/database/pool/base.js +26 -1
  218. package/src/database/pool/single-multi-use.js +49 -0
  219. package/src/database/query/alter-table-base.js +20 -1
  220. package/src/database/query/model-class-query.js +38 -7
  221. package/src/database/query/preloader/belongs-to.js +3 -2
  222. package/src/database/query/preloader/has-many.js +4 -3
  223. package/src/database/query/preloader/has-one.js +2 -1
  224. package/src/database/query/preloader/query-for-model.js +17 -0
  225. package/src/database/query/query-data.js +12 -3
  226. package/src/database/query/with-count.js +5 -3
  227. package/src/database/record/acts-as-list.js +11 -8
  228. package/src/database/record/attachments/normalize-input.js +38 -10
  229. package/src/database/record/attachments/storage-drivers/filesystem.js +33 -2
  230. package/src/database/record/attachments/storage-drivers/native.js +17 -2
  231. package/src/database/record/attachments/storage-drivers/s3.js +27 -7
  232. package/src/database/record/attachments/store.js +173 -53
  233. package/src/database/record/auditing.js +12 -9
  234. package/src/database/record/counter-cache-magnitude.js +5 -3
  235. package/src/database/record/index.js +95 -14
  236. package/src/database/record/instance-relationships/belongs-to.js +4 -2
  237. package/src/database/record/instance-relationships/has-many.js +7 -5
  238. package/src/database/record/instance-relationships/has-one.js +4 -2
  239. package/src/database/record/validators/uniqueness.js +3 -2
  240. package/src/database/table-data/table-foreign-key.js +9 -1
  241. package/src/environment-handlers/base.js +1 -1
  242. package/src/environment-handlers/node/attachment-path-source.js +144 -0
  243. package/src/environment-handlers/node.js +28 -3
  244. package/src/frontend-models/websocket-publishers.js +2 -2
  245. package/src/sync/server-sequence-allocator.js +43 -10
  246. package/src/sync/sync-client.js +6 -2
  247. package/src/sync/sync-publisher.js +10 -5
@@ -29,8 +29,9 @@ export default class VelociousDatabaseRecordHasManyInstanceRelationship extends
29
29
 
30
30
  if (!targetModelClass) throw new Error("Can't build a new record without a taget model class")
31
31
 
32
- const newInstance = /** @type {InstanceType<TMC>} */ (new targetModelClass(data))
33
-
32
+ const newInstance = this.getModel().bindRelatedRecord(
33
+ /** @type {InstanceType<TMC>} */ (new targetModelClass(data))
34
+ )
34
35
 
35
36
  // Add it to the loaded models of this relationship
36
37
  if (this._loaded === undefined) {
@@ -196,13 +197,14 @@ export default class VelociousDatabaseRecordHasManyInstanceRelationship extends
196
197
  )
197
198
  const targetTable = TargetModelClass.tableName()
198
199
  const throughTable = throughModelClass.tableName()
199
- const driver = TargetModelClass.connection()
200
+ const baseQuery = this.getModel().queryForModel(TargetModelClass)
201
+ const driver = baseQuery.driver
200
202
  const parentPrimaryKey = this.getPrimaryKey()
201
203
  const parentId = /** @type {string | number} */ (this.getModel().readColumn(parentPrimaryKey))
202
204
  const joinSql = `LEFT JOIN ${driver.quoteTable(throughTable)} ON ${driver.quoteTable(throughTable)}.${driver.quoteColumn(throughPrimaryKey)} = ${driver.quoteTable(targetTable)}.${driver.quoteColumn(targetForeignKey)}`
203
205
  const whereSql = `${driver.quoteTable(throughTable)}.${driver.quoteColumn(throughForeignKey)} = ${driver.options().quote(parentId)}`
204
206
 
205
- const query = TargetModelClass.joins(joinSql).where(whereSql)
207
+ const query = baseQuery.joins(joinSql).where(whereSql)
206
208
 
207
209
  return this.applyScope(query)
208
210
  }
@@ -224,7 +226,7 @@ export default class VelociousDatabaseRecordHasManyInstanceRelationship extends
224
226
  whereArgs[typeColumn] = this.getModel().getModelClass().getModelName()
225
227
  }
226
228
 
227
- const query = TargetModelClass.where(whereArgs)
229
+ const query = this.getModel().queryForModel(TargetModelClass).where(whereArgs)
228
230
 
229
231
  return this.applyScope(query)
230
232
  }
@@ -32,7 +32,9 @@ export default class VelociousDatabaseRecordHasOneInstanceRelationship extends B
32
32
 
33
33
  if (!TargetModelClass) throw new Error("Can't build a new record without a target model class")
34
34
 
35
- const newInstance = /** @type {InstanceType<TMC>} */ (new TargetModelClass(data))
35
+ const newInstance = this.getModel().bindRelatedRecord(
36
+ /** @type {InstanceType<TMC>} */ (new TargetModelClass(data))
37
+ )
36
38
 
37
39
  this._loaded = newInstance
38
40
 
@@ -64,7 +66,7 @@ export default class VelociousDatabaseRecordHasOneInstanceRelationship extends B
64
66
 
65
67
  whereArgs[foreignKey] = primaryModelID
66
68
 
67
- let query = TargetModelClass.where(whereArgs)
69
+ let query = this.getModel().queryForModel(TargetModelClass).where(whereArgs)
68
70
 
69
71
  query = this.applyScope(query)
70
72
 
@@ -15,7 +15,7 @@ export default class VelociousDatabaseRecordValidatorsUniqueness extends Base {
15
15
  async validate({model, attributeName}) {
16
16
  const modelClass = /** @type {typeof import("../index.js").default} */ (model.constructor)
17
17
 
18
- const connection = modelClass.connection()
18
+ const connection = model.connection()
19
19
  const tableName = modelClass._getTable().getName()
20
20
  const attributeValue = /** @type {string | number} */ (model.readAttribute(attributeName))
21
21
  const attributeNameUnderscore = inflection.underscore(attributeName)
@@ -49,7 +49,8 @@ export default class VelociousDatabaseRecordValidatorsUniqueness extends Base {
49
49
  whereArgs[scopeUnderscore] = /** @type {string | number} */ (scopeValue)
50
50
  }
51
51
 
52
- let existingRecordQuery = modelClass
52
+ let existingRecordQuery = model
53
+ .queryForModel(modelClass)
53
54
  .select(modelClass.primaryKey())
54
55
  .where(whereArgs)
55
56
 
@@ -7,16 +7,18 @@ export default class TableForeignKey {
7
7
  * Runs constructor.
8
8
  * @param {object} args - Options object.
9
9
  * @param {string} args.columnName - Column name.
10
+ * @param {boolean} [args.dropForeignKey] - Whether to drop this foreign key.
10
11
  * @param {boolean} [args.isNewForeignKey] - Whether is new foreign key.
11
12
  * @param {string} [args.name] - Name.
12
13
  * @param {string} args.tableName - Table name.
13
14
  * @param {string} args.referencedColumnName - Referenced column name.
14
15
  * @param {string} args.referencedTableName - Referenced table name.
15
16
  */
16
- constructor({columnName, isNewForeignKey, name, tableName, referencedColumnName, referencedTableName, ...restArgs}) {
17
+ constructor({columnName, dropForeignKey, isNewForeignKey, name, tableName, referencedColumnName, referencedTableName, ...restArgs}) {
17
18
  restArgsError(restArgs)
18
19
 
19
20
  this._columnName = columnName
21
+ this._dropForeignKey = dropForeignKey
20
22
  this._isNewForeignKey = isNewForeignKey
21
23
  this._name = name
22
24
  this._tableName = tableName
@@ -30,6 +32,12 @@ export default class TableForeignKey {
30
32
  */
31
33
  getColumnName() { return this._columnName }
32
34
 
35
+ /**
36
+ * Runs get drop foreign key.
37
+ * @returns {boolean} - Whether this foreign key should be dropped.
38
+ */
39
+ getDropForeignKey() { return this._dropForeignKey || false }
40
+
33
41
  /**
34
42
  * Runs get is new foreign key.
35
43
  * @returns {boolean} - Whether is new foreign key.
@@ -559,7 +559,7 @@ export default class VelociousEnvironmentHandlerBase {
559
559
  * @param {object} _args - Args.
560
560
  * @param {string[]} _args.allowedPathPrefixes - Allowed path prefixes.
561
561
  * @param {string} _args.inputPath - Input path.
562
- * @returns {Promise<{buffer: Buffer, filePath: string}>} - Resolved path and file bytes.
562
+ * @returns {Promise<import("../database/record/attachments/normalize-input.js").AttachmentPathSource>} - Opened path source.
563
563
  */
564
564
  async resolveAttachmentInputPath(_args) {
565
565
  throw new Error("Attachment path input is not supported in this environment")
@@ -0,0 +1,144 @@
1
+ // @ts-check
2
+
3
+ import { Readable } from "node:stream"
4
+
5
+ const READ_CHUNK_SIZE = 64 * 1024
6
+
7
+ /**
8
+ * Opened Node attachment path source.
9
+ *
10
+ * The opened file handle fixes the source identity at normalization time. Reads
11
+ * are limited to the stat snapshot and fail if the file becomes shorter.
12
+ */
13
+ export default class AttachmentPathSource {
14
+ /**
15
+ * Creates an opened attachment path source.
16
+ * @param {object} args - Source args.
17
+ * @param {number} args.byteSize - Opened-handle stat size.
18
+ * @param {import("node:fs/promises").FileHandle} args.fileHandle - Open file handle.
19
+ * @param {string} args.filePath - Validated path used to open the handle.
20
+ */
21
+ constructor({byteSize, fileHandle, filePath}) {
22
+ this.byteSize = byteSize
23
+ this.fileHandle = fileHandle
24
+ this.filePath = filePath
25
+ /** @type {Set<Readable>} */
26
+ this.activeStreams = new Set()
27
+ /** @type {Promise<void> | null} */
28
+ this.closePromise = null
29
+ this.closed = false
30
+ }
31
+
32
+ /**
33
+ * Creates a bounded, backpressured stream over the opened file snapshot.
34
+ * @returns {Promise<Readable>} - Snapshot read stream.
35
+ */
36
+ async createReadStream() {
37
+ this.assertOpen()
38
+
39
+ const stream = Readable.from(this.readChunks())
40
+
41
+ this.activeStreams.add(stream)
42
+ stream.once("close", () => {
43
+ this.activeStreams.delete(stream)
44
+ })
45
+
46
+ return stream
47
+ }
48
+
49
+ /**
50
+ * Reads the opened file snapshot into memory for compatibility-only callers.
51
+ * @returns {Promise<Buffer>} - Exact snapshot bytes.
52
+ */
53
+ async readBuffer() {
54
+ this.assertOpen()
55
+
56
+ const contentBuffer = Buffer.allocUnsafe(this.byteSize)
57
+ let offset = 0
58
+
59
+ while (offset < this.byteSize) {
60
+ this.assertOpen()
61
+
62
+ const {bytesRead} = await this.fileHandle.read(
63
+ contentBuffer,
64
+ offset,
65
+ Math.min(READ_CHUNK_SIZE, this.byteSize - offset),
66
+ offset
67
+ )
68
+
69
+ if (bytesRead === 0) this.throwTruncated({bytesRead: offset})
70
+
71
+ offset += bytesRead
72
+ }
73
+
74
+ return contentBuffer
75
+ }
76
+
77
+ /**
78
+ * Closes all active streams and the owned file handle.
79
+ * @returns {Promise<void>} - Resolves after close.
80
+ */
81
+ async close() {
82
+ if (!this.closePromise) {
83
+ this.closed = true
84
+ this.closePromise = (async () => {
85
+ const streams = Array.from(this.activeStreams)
86
+ const streamClosePromises = streams.map(async (stream) => {
87
+ if (stream.closed) return
88
+
89
+ await new Promise((resolve) => {
90
+ stream.once("close", resolve)
91
+ stream.destroy()
92
+ })
93
+ })
94
+
95
+ await Promise.all(streamClosePromises)
96
+ await this.fileHandle.close()
97
+ })()
98
+ }
99
+
100
+ await this.closePromise
101
+ }
102
+
103
+ /**
104
+ * Produces bounded chunks from the opened file handle.
105
+ * @yields {Buffer} - Snapshot chunks.
106
+ */
107
+ async *readChunks() {
108
+ let offset = 0
109
+
110
+ while (offset < this.byteSize) {
111
+ this.assertOpen()
112
+
113
+ const chunkBuffer = Buffer.allocUnsafe(Math.min(READ_CHUNK_SIZE, this.byteSize - offset))
114
+ const {bytesRead} = await this.fileHandle.read(chunkBuffer, 0, chunkBuffer.length, offset)
115
+
116
+ if (bytesRead === 0) this.throwTruncated({bytesRead: offset})
117
+
118
+ offset += bytesRead
119
+ yield bytesRead === chunkBuffer.length ? chunkBuffer : chunkBuffer.subarray(0, bytesRead)
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Asserts that the source remains open.
125
+ * @returns {void} - Throws if closed.
126
+ */
127
+ assertOpen() {
128
+ if (this.closed) {
129
+ throw new Error(`Attachment path source is closed: ${this.filePath}`)
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Throws a truncation error.
135
+ * @param {object} args - Args.
136
+ * @param {number} args.bytesRead - Bytes read before EOF.
137
+ * @returns {never} - Always throws.
138
+ */
139
+ throwTruncated({bytesRead}) {
140
+ throw new Error(
141
+ `Attachment path source was truncated: expected ${this.byteSize} bytes but read ${bytesRead} from ${this.filePath}`
142
+ )
143
+ }
144
+ }
@@ -35,6 +35,7 @@ import requireContext from "require-context"
35
35
  import InitializerFromRequireContext from "../database/initializer-from-require-context.js"
36
36
  import toImportSpecifier from "../utils/to-import-specifier.js"
37
37
  import {validateTimeZone} from "../time-zone.js"
38
+ import AttachmentPathSource from "./node/attachment-path-source.js"
38
39
 
39
40
  /**
40
41
  * Defines this typedef.
@@ -227,7 +228,7 @@ export default class VelociousEnvironmentHandlerNode extends Base{
227
228
  * @param {object} args - Args.
228
229
  * @param {string[]} args.allowedPathPrefixes - Allowed path prefixes.
229
230
  * @param {string} args.inputPath - Input path.
230
- * @returns {Promise<{buffer: Buffer, filePath: string}>} - Resolved path and bytes.
231
+ * @returns {Promise<AttachmentPathSource>} - Opened regular-file path source.
231
232
  */
232
233
  async resolveAttachmentInputPath({allowedPathPrefixes, inputPath}) {
233
234
  const filePath = path.resolve(inputPath)
@@ -239,9 +240,33 @@ export default class VelociousEnvironmentHandlerNode extends Base{
239
240
  throw new Error("Attachment path is outside allowed directories")
240
241
  }
241
242
 
242
- const buffer = await this.readAttachmentInputFile(filePath)
243
+ const fileHandle = await fs.open(filePath, "r")
243
244
 
244
- return {buffer, filePath}
245
+ try {
246
+ const fileStats = await fileHandle.stat()
247
+
248
+ if (!fileStats.isFile()) {
249
+ throw new Error("Attachment path must reference a regular file")
250
+ }
251
+
252
+ return new AttachmentPathSource({
253
+ byteSize: fileStats.size,
254
+ fileHandle,
255
+ filePath
256
+ })
257
+ } catch (error) {
258
+ try {
259
+ await fileHandle.close()
260
+ } catch (closeError) {
261
+ throw new AggregateError(
262
+ [error, closeError],
263
+ `Attachment path validation and source close both failed for ${filePath}`,
264
+ {cause: closeError}
265
+ )
266
+ }
267
+
268
+ throw error
269
+ }
245
270
  }
246
271
 
247
272
  /**
@@ -145,7 +145,7 @@ export async function ensureFrontendModelWebsocketPublishersRegistered(configura
145
145
 
146
146
  if (action !== "create" && action !== "update") return
147
147
 
148
- void model.getModelClass().connection().afterCommit(async () => {
148
+ void model.connection().afterCommit(async () => {
149
149
  broadcastFrontendModelEvent(model._getConfiguration(), modelName, {
150
150
  action,
151
151
  id: model.id(),
@@ -156,7 +156,7 @@ export async function ensureFrontendModelWebsocketPublishersRegistered(configura
156
156
  })
157
157
 
158
158
  modelClass.afterDestroy((model) => {
159
- void model.getModelClass().connection().afterCommit(async () => {
159
+ void model.connection().afterCommit(async () => {
160
160
  broadcastFrontendModelEvent(model._getConfiguration(), modelName, {
161
161
  action: "destroy",
162
162
  id: model.id()
@@ -49,23 +49,52 @@ export default class ServerSequenceAllocator {
49
49
  * parallel `next()` calls - including calls from other allocator instances
50
50
  * sharing the same table and connection - cannot interleave their insert
51
51
  * and last-insert-id reads and hand out duplicate sequences.
52
+ * @param {{connection?: import("../database/drivers/base.js").default}} [options] - Explicit record-owned connection.
52
53
  * @returns {Promise<number>} Next sequence value.
53
54
  */
54
- async next() {
55
+ async next({connection} = {}) {
55
56
  const queueKey = `${this.databaseIdentifier}::${this.tableName}`
56
57
  const previousAllocation = allocationQueues.get(queueKey) ?? Promise.resolve()
57
- const allocation = previousAllocation.then(() => this._allocateNext())
58
+ const allocation = previousAllocation.then(() => this._allocateNext(connection))
58
59
 
59
60
  allocationQueues.set(queueKey, allocation.then(() => undefined, () => undefined))
60
61
 
61
62
  return await allocation
62
63
  }
63
64
 
65
+ /**
66
+ * Allocates for a record while preserving allocator routing and operation ownership.
67
+ * @param {import("../database/record/index.js").default} record - Record receiving the sequence.
68
+ * @returns {Promise<number>} Next sequence value.
69
+ */
70
+ async _nextForRecord(record) {
71
+ const operation = record.databaseOperation()
72
+
73
+ if (!operation) return await this.next()
74
+
75
+ const ModelClass = record.getModelClass()
76
+
77
+ if (this._getConfiguration() !== ModelClass._getConfiguration()) {
78
+ throw new Error("Server sequence allocator belongs to another Velocious configuration")
79
+ }
80
+
81
+ const modelDatabaseIdentifier = ModelClass.getDatabaseIdentifier()
82
+
83
+ if (this.databaseIdentifier !== modelDatabaseIdentifier) {
84
+ throw new Error(`Server sequence allocator uses database ${JSON.stringify(this.databaseIdentifier)}, not operation model database ${JSON.stringify(modelDatabaseIdentifier)}`)
85
+ }
86
+
87
+ const operationScope = operation.forModel(ModelClass)
88
+
89
+ return await this.next({connection: operationScope.driver})
90
+ }
91
+
64
92
  /**
65
93
  * Ensures the backing table exists.
94
+ * @param {import("../database/drivers/base.js").default} [connection] - Explicit record-owned connection.
66
95
  * @returns {Promise<void>} Resolves when ready.
67
96
  */
68
- async ensureReady() {
97
+ async ensureReady(connection) {
69
98
  if (this._isReady) return
70
99
 
71
100
  if (this._usesMemoryStorage()) {
@@ -85,7 +114,7 @@ export default class ServerSequenceAllocator {
85
114
  // cache readiness when the table was not created inside an active
86
115
  // transaction; otherwise the next allocation re-verifies the table.
87
116
  if (!created || !db.insideTransaction()) this._isReady = true
88
- })
117
+ }, connection)
89
118
 
90
119
  try {
91
120
  await this._readyPromise
@@ -96,10 +125,11 @@ export default class ServerSequenceAllocator {
96
125
 
97
126
  /**
98
127
  * Allocates one sequence value after queueing.
128
+ * @param {import("../database/drivers/base.js").default} [connection] - Explicit record-owned connection.
99
129
  * @returns {Promise<number>} Allocated sequence value.
100
130
  */
101
- async _allocateNext() {
102
- await this.ensureReady()
131
+ async _allocateNext(connection) {
132
+ await this.ensureReady(connection)
103
133
 
104
134
  if (this._usesMemoryStorage()) {
105
135
  return ++this._memorySequence
@@ -123,7 +153,7 @@ export default class ServerSequenceAllocator {
123
153
  if (insertedId !== undefined && insertedId !== null) return Number(insertedId)
124
154
 
125
155
  return Number(await db.lastInsertID())
126
- })
156
+ }, connection)
127
157
  }
128
158
 
129
159
  /**
@@ -158,9 +188,12 @@ export default class ServerSequenceAllocator {
158
188
  * Runs a callback with a database connection.
159
189
  * @template Result
160
190
  * @param {(db: import("../database/drivers/base.js").default) => Promise<Result>} callback - Database callback.
191
+ * @param {import("../database/drivers/base.js").default} [connection] - Explicit record-owned connection.
161
192
  * @returns {Promise<Result>} Callback result.
162
193
  */
163
- async _withDb(callback) {
194
+ async _withDb(callback, connection) {
195
+ if (connection) return await callback(connection)
196
+
164
197
  return await this._getConfiguration().ensureConnections({databaseIdentifiers: [this.databaseIdentifier], name: "Server sequence allocator"}, async (dbs) => {
165
198
  const db = dbs[this.databaseIdentifier]
166
199
 
@@ -225,11 +258,11 @@ export function withServerSequence(ModelClass, {allocator, column = "serverSeque
225
258
  if (typeof prototype[advanceMethodName] != "function") {
226
259
  /**
227
260
  * Assigns the next server-side sequence.
228
- * @this {Record<string, ?>}
261
+ * @this {import("../database/record/index.js").default & Record<string, ?>}
229
262
  * @returns {Promise<void>}
230
263
  */
231
264
  prototype[advanceMethodName] = async function advanceServerSequenceThroughAllocator() {
232
- this[setterMethodName](await allocator.next())
265
+ this[setterMethodName](await allocator._nextForRecord(this))
233
266
  }
234
267
  }
235
268
 
@@ -220,13 +220,17 @@ export default class SyncClient {
220
220
  resource: record
221
221
  })
222
222
  const syncType = this.defaultSyncType({operation, record, resourceConfig})
223
+ const databaseOperation = record.databaseOperation()
224
+ const operationScope = databaseOperation
225
+ ? databaseOperation.forModel(this.config.syncModel)
226
+ : this.config.syncModel
223
227
 
224
- await resourceConfig.modelClass.connection().afterCommit(async () => {
228
+ await record.connection().afterCommit(async () => {
225
229
  try {
226
230
  await SyncApiClient.queueLocalSync({
227
231
  data,
228
232
  resource: record,
229
- syncModel: this.config.syncModel,
233
+ syncModel: operationScope,
230
234
  syncType
231
235
  })
232
236
  } catch (error) {
@@ -203,10 +203,14 @@ export default class SyncPublisher {
203
203
  sync_type: syncType,
204
204
  ...scopeValues.columns
205
205
  }
206
+ const databaseOperation = record.databaseOperation()
207
+ const operationScope = databaseOperation
208
+ ? databaseOperation.forModel(this.config.syncModel)
209
+ : this.config.syncModel
206
210
 
207
- await resourceConfig.modelClass.connection().afterCommit(async () => {
211
+ await record.connection().afterCommit(async () => {
208
212
  try {
209
- const syncRow = await this.upsertPublishedSyncRow(attributes)
213
+ const syncRow = await this.upsertPublishedSyncRow(attributes, operationScope)
210
214
 
211
215
  await this.broadcaster()({
212
216
  body: {
@@ -273,10 +277,11 @@ export default class SyncPublisher {
273
277
  * change back to), so repeated server changes to one resource reuse and
274
278
  * re-sequence one feed row.
275
279
  * @param {Record<string, ?>} attributes - Snapshotted sync row attributes.
280
+ * @param {?} syncModel - Operation-bound or static Sync model interface.
276
281
  * @returns {Promise<?>} Upserted sync row.
277
282
  */
278
- async upsertPublishedSyncRow(attributes) {
279
- const existingSync = await this.config.syncModel
283
+ async upsertPublishedSyncRow(attributes, syncModel = this.config.syncModel) {
284
+ const existingSync = await syncModel
280
285
  .where({
281
286
  [this.config.actorForeignKeyColumn]: null,
282
287
  resource_id: attributes.resource_id,
@@ -284,7 +289,7 @@ export default class SyncPublisher {
284
289
  })
285
290
  .first()
286
291
 
287
- return await upsertSyncRow({attributes, existingSync, syncModel: this.config.syncModel})
292
+ return await upsertSyncRow({attributes, existingSync, syncModel})
288
293
  }
289
294
 
290
295
  /**