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
@@ -24,7 +24,7 @@ import WhereParser from "../query-parser/where-parser.js"
24
24
  /**
25
25
  * Defines this typedef.
26
26
  * @template {typeof import("../record/index.js").default} [MC=typeof import("../record/index.js").default]
27
- * @typedef {import("./index.js").QueryArgsType & {modelClass: MC, joinBasePath?: string[], joinTracker?: import("./join-tracker.js").default, forceQualifyBaseTable?: boolean, withCount?: import("./with-count.js").WithCountEntry[], queryData?: import("./query-data.js").QueryDataEntry[]}} ModelClassQueryArgsType
27
+ * @typedef {import("./index.js").QueryArgsType & {modelClass: MC, joinBasePath?: string[], joinTracker?: import("./join-tracker.js").default, forceQualifyBaseTable?: boolean, withCount?: import("./with-count.js").WithCountEntry[], queryData?: import("./query-data.js").QueryDataEntry[], operation?: import("../operation.js").default}} ModelClassQueryArgsType
28
28
  */
29
29
  /**
30
30
  * Runs unquote sql identifier.
@@ -200,6 +200,7 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
200
200
  this._joinBasePath = args.joinBasePath || []
201
201
  this._joinTracker = args.joinTracker || new JoinTracker({modelClass: this.modelClass})
202
202
  this._forceQualifyBaseTable = Boolean(args.forceQualifyBaseTable)
203
+ this._operation = args.operation
203
204
 
204
205
  /**
205
206
  * Narrows the runtime value to the documented type.
@@ -240,7 +241,8 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
240
241
  joinTracker: this._joinTracker.clone(),
241
242
  forceQualifyBaseTable: this._forceQualifyBaseTable,
242
243
  withCount: [...this._withCount],
243
- queryData: [...this._queryData]
244
+ queryData: [...this._queryData],
245
+ operation: this._operation
244
246
  }))
245
247
 
246
248
  // @ts-expect-error
@@ -723,7 +725,11 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
723
725
  * @returns {VelociousDatabaseQueryModelClassQuery<MC>} - The scoped join query.
724
726
  */
725
727
  buildJoinScopeQuery(targetModelClass, joinPath) {
726
- const scopedQuery = /** @type {VelociousDatabaseQueryModelClassQuery<MC>} */ (targetModelClass._newQuery())
728
+ const scopedQuery = /** @type {VelociousDatabaseQueryModelClassQuery<MC>} */ (
729
+ this._operation
730
+ ? this._operation.forModel(targetModelClass)
731
+ : targetModelClass._newQuery()
732
+ )
727
733
 
728
734
  scopedQuery._joinTracker = this._joinTracker
729
735
  scopedQuery._joinBasePath = joinPath
@@ -864,8 +870,7 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
864
870
 
865
871
  if (record) return record
866
872
 
867
- const ModelClass = this.getModelClass()
868
- const newRecord = /** @type {InstanceType<MC>} */ (new ModelClass(conditions))
873
+ const newRecord = this.build(conditions)
869
874
 
870
875
  if (callback) {
871
876
  callback(newRecord)
@@ -874,6 +879,33 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
874
879
  return newRecord
875
880
  }
876
881
 
882
+ /**
883
+ * Builds a record owned by the query's operation, when present.
884
+ * @param {Record<string, ?>} [attributes] - Initial attributes.
885
+ * @returns {InstanceType<MC>} - Built record.
886
+ */
887
+ build(attributes = {}) {
888
+ const ModelClass = this.getModelClass()
889
+ const record = /** @type {InstanceType<MC>} */ (new ModelClass(attributes))
890
+
891
+ if (this._operation) this._operation.bindRecord(record)
892
+
893
+ return record
894
+ }
895
+
896
+ /**
897
+ * Creates a record owned by the query's operation, when present.
898
+ * @param {Record<string, ?>} [attributes] - Initial attributes.
899
+ * @returns {Promise<InstanceType<MC>>} - Created record.
900
+ */
901
+ async create(attributes = {}) {
902
+ const record = this.build(attributes)
903
+
904
+ await record.save()
905
+
906
+ return record
907
+ }
908
+
877
909
  /**
878
910
  * Runs first.
879
911
  * @returns {Promise<InstanceType<MC> | null>} - Resolves with the first.
@@ -917,8 +949,7 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
917
949
  const results = await this.results()
918
950
 
919
951
  for (const result of results) {
920
- const ModelClass = this.getModelClass()
921
- const model = /** @type {InstanceType<MC>} */ (new ModelClass())
952
+ const model = this.build()
922
953
 
923
954
  model.loadExistingRecord(result)
924
955
  models.push(model)
@@ -2,6 +2,7 @@
2
2
 
3
3
  import ensureModelClassInitialized from "./ensure-model-class-initialized.js"
4
4
  import PreloaderSelection from "./selection.js"
5
+ import preloadQueryForModel from "./query-for-model.js"
5
6
  import restArgsError from "../../../utils/rest-args-error.js"
6
7
 
7
8
  export default class VelociousDatabaseQueryPreloaderBelongsTo {
@@ -94,7 +95,7 @@ export default class VelociousDatabaseQueryPreloaderBelongsTo {
94
95
  whereArgs[primaryKey] = [...foreignKeyValues]
95
96
 
96
97
  // Load target models to be preloaded on the given models
97
- let query = targetModelClass.where(whereArgs)
98
+ let query = preloadQueryForModel(modelsToLoad, targetModelClass).where(whereArgs)
98
99
 
99
100
  query = this.relationship.applyScope(query)
100
101
  query = this.selection.applyToQuery({query, targetModelClass, mappingColumns: [primaryKey]})
@@ -211,7 +212,7 @@ export default class VelociousDatabaseQueryPreloaderBelongsTo {
211
212
 
212
213
  whereArgs[primaryKey] = [...foreignKeyValuesByType[targetType]]
213
214
 
214
- let query = targetModelClass.where(whereArgs)
215
+ let query = preloadQueryForModel(this.models, targetModelClass).where(whereArgs)
215
216
 
216
217
  query = this.relationship.applyScope(query)
217
218
  query = this.selection.applyToQuery({query, targetModelClass, mappingColumns: [primaryKey]})
@@ -2,6 +2,7 @@
2
2
 
3
3
  import ensureModelClassInitialized from "./ensure-model-class-initialized.js"
4
4
  import PreloaderSelection from "./selection.js"
5
+ import preloadQueryForModel from "./query-for-model.js"
5
6
  import restArgsError from "../../../utils/rest-args-error.js"
6
7
 
7
8
  /**
@@ -155,7 +156,7 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
155
156
  }
156
157
 
157
158
  // Step 1: Query the through table to build parent→target ID mapping
158
- const throughModels = await throughModelClass
159
+ const throughModels = await preloadQueryForModel(modelsToLoad, throughModelClass)
159
160
  .where({[throughForeignKey]: [...modelsPrimaryKeyValues]})
160
161
  .toArray()
161
162
 
@@ -186,7 +187,7 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
186
187
  let targetModels = []
187
188
 
188
189
  if (allTargetIds.size > 0) {
189
- let query = targetModelClass.where({[targetForeignKey]: [...allTargetIds]})
190
+ let query = preloadQueryForModel(modelsToLoad, targetModelClass).where({[targetForeignKey]: [...allTargetIds]})
190
191
 
191
192
  query = this.relationship.applyScope(query)
192
193
  query = this.selection.applyToQuery({query, targetModelClass, mappingColumns: [targetForeignKey]})
@@ -299,7 +300,7 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
299
300
 
300
301
  await ensureModelClassInitialized(targetModelClass, this.relationship.getConfiguration())
301
302
 
302
- let query = targetModelClass.where(whereArgs)
303
+ let query = preloadQueryForModel(modelsToLoad, targetModelClass).where(whereArgs)
303
304
 
304
305
  query = this.relationship.applyScope(query)
305
306
  query = this.selection.applyToQuery({query, targetModelClass, mappingColumns: [foreignKey]})
@@ -2,6 +2,7 @@
2
2
 
3
3
  import ensureModelClassInitialized from "./ensure-model-class-initialized.js"
4
4
  import PreloaderSelection from "./selection.js"
5
+ import preloadQueryForModel from "./query-for-model.js"
5
6
  import restArgsError from "../../../utils/rest-args-error.js"
6
7
 
7
8
  export default class VelociousDatabaseQueryPreloaderHasOne {
@@ -87,7 +88,7 @@ export default class VelociousDatabaseQueryPreloaderHasOne {
87
88
  await ensureModelClassInitialized(targetModelClass, this.relationship.getConfiguration())
88
89
 
89
90
  // Load target models to be preloaded on the given models
90
- let query = targetModelClass.where(whereArgs)
91
+ let query = preloadQueryForModel(this.models, targetModelClass).where(whereArgs)
91
92
 
92
93
  query = this.relationship.applyScope(query)
93
94
  query = this.selection.applyToQuery({query, targetModelClass, mappingColumns: [foreignKey]})
@@ -0,0 +1,17 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Builds a target query preserving the explicit database operation owned by
5
+ * the source records.
6
+ * @template {typeof import("../../record/index.js").default} MC
7
+ * @param {import("../../record/index.js").default[]} models - Source records.
8
+ * @param {MC} ModelClass - Target model class.
9
+ * @returns {import("../model-class-query.js").default<MC>} - Target query.
10
+ */
11
+ export default function preloadQueryForModel(models, ModelClass) {
12
+ const sourceModel = models[0]
13
+
14
+ if (!sourceModel) throw new Error("Cannot build a preload query without source records")
15
+
16
+ return sourceModel.queryForModel(ModelClass)
17
+ }
@@ -190,7 +190,15 @@ export async function runQueryData({rootModelClass, rootModels, entries}) {
190
190
 
191
191
  const primaryKey = rootModelClass.primaryKey()
192
192
  const rootIds = rootModels.map((model) => /** @type {string | number} */ (model.readColumn(primaryKey)))
193
- const preparedEntries = entries.map((entry, entryIndex) => prepareEntry({entry, entryIndex, primaryKey, rootIds, rootModelClass}))
193
+ const sourceModel = rootModels[0]
194
+ const preparedEntries = entries.map((entry, entryIndex) => prepareEntry({
195
+ entry,
196
+ entryIndex,
197
+ primaryKey,
198
+ rootIds,
199
+ rootModelClass,
200
+ sourceModel
201
+ }))
194
202
  /**
195
203
  * Compatible query groups.
196
204
  * @type {Array<{aliases: Set<string>, query: import("./model-class-query.js").default, signature: string}>} */
@@ -231,9 +239,10 @@ export async function runQueryData({rootModelClass, rootModels, entries}) {
231
239
  * @param {string} args.primaryKey - Root model primary key column.
232
240
  * @param {Array<string | number>} args.rootIds - Root primary-key values.
233
241
  * @param {typeof import("../record/index.js").default} args.rootModelClass - Root model class.
242
+ * @param {import("../record/index.js").default} args.sourceModel - Loaded operation owner.
234
243
  * @returns {{aliases: string[], query: import("./model-class-query.js").default, signature: string}} - Prepared entry.
235
244
  */
236
- function prepareEntry({entry, entryIndex, primaryKey, rootIds, rootModelClass}) {
245
+ function prepareEntry({entry, entryIndex, primaryKey, rootIds, rootModelClass, sourceModel}) {
237
246
  const targetModelClass = resolveTargetModelClass(rootModelClass, entry.chain)
238
247
  const fn = targetModelClass.getQueryDataByName(entry.fnName)
239
248
 
@@ -242,7 +251,7 @@ function prepareEntry({entry, entryIndex, primaryKey, rootIds, rootModelClass})
242
251
  `Declare it with ${targetModelClass.name}.queryData(${JSON.stringify(entry.fnName)}, ({query, tableName}) => query.select(...))`)
243
252
  }
244
253
 
245
- const query = rootModelClass._newQuery()
254
+ const query = sourceModel.queryForModel(rootModelClass)
246
255
 
247
256
  // Empty out any defaults the query factory added — queryData runs
248
257
  // a bare aggregate, not a full model load.
@@ -111,10 +111,11 @@ export async function runWithCount({models, modelClass, entries}) {
111
111
 
112
112
  const primaryKey = modelClass.primaryKey()
113
113
  const parentIds = models.map((model) => /** @type {string | number} */ (model.readColumn(primaryKey)))
114
+ const sourceModel = models[0]
114
115
  const queryGroups = new Map()
115
116
 
116
117
  for (const entry of entries) {
117
- const countQuery = queryForEntry({entry, modelClass, parentIds})
118
+ const countQuery = queryForEntry({entry, modelClass, parentIds, sourceModel})
118
119
  const sql = countQuery.toSql()
119
120
  const existingGroup = queryGroups.get(sql)
120
121
 
@@ -138,9 +139,10 @@ export async function runWithCount({models, modelClass, entries}) {
138
139
  * @param {WithCountEntry} args.entry - Entry being evaluated.
139
140
  * @param {typeof import("../record/index.js").default} args.modelClass - Parent model class.
140
141
  * @param {Array<string | number>} args.parentIds - Primary keys of the loaded parents.
142
+ * @param {import("../record/index.js").default} args.sourceModel - Loaded operation owner.
141
143
  * @returns {import("./model-class-query.js").default} - Prepared count query.
142
144
  */
143
- function queryForEntry({entry, modelClass, parentIds}) {
145
+ function queryForEntry({entry, modelClass, parentIds, sourceModel}) {
144
146
  const relationship = modelClass.getRelationshipByName(entry.relationshipName)
145
147
 
146
148
  if (!relationship) {
@@ -172,7 +174,7 @@ function queryForEntry({entry, modelClass, parentIds}) {
172
174
  Object.assign(whereConditions, entry.where)
173
175
  }
174
176
 
175
- const baseQuery = targetModelClass._newQuery()
177
+ const baseQuery = sourceModel.queryForModel(targetModelClass)
176
178
  baseQuery._forceQualifyBaseTable = true
177
179
  baseQuery.where(whereConditions)
178
180
 
@@ -173,7 +173,7 @@ function assertPositivePosition({position, positionColumn, persisted = false}) {
173
173
  */
174
174
  async function placeMovedRecord({record, positionColumn, scope, scopeValue, position}) {
175
175
  const modelClass = /** @type {typeof import("./index.js").default} */ (record.constructor)
176
- const connection = modelClass.connection()
176
+ const connection = record.connection()
177
177
  const tableSql = connection.quoteTable(modelClass._getTable().getName())
178
178
  const scopeCol = modelClass.getColumnNameForAttributeName(scope)
179
179
  const posCol = modelClass.getColumnNameForAttributeName(positionColumn)
@@ -224,7 +224,7 @@ function clearBelongsToChangeForScope(record) {
224
224
  */
225
225
  async function shiftPositionsUp({record, positionColumn, scope, fromPosition, toPosition, scopeValue, excludeRecordId}) {
226
226
  const modelClass = /** @type {typeof import("./index.js").default} */ (record.constructor)
227
- const connection = modelClass.connection()
227
+ const connection = record.connection()
228
228
  const tableName = modelClass._getTable().getName()
229
229
  const resolvedScopeValue = scopeValue != null ? scopeValue : resolveScopeValue(record, scope)
230
230
  const scopeColumnName = modelClass.getColumnNameForAttributeName(scope)
@@ -239,7 +239,8 @@ async function shiftPositionsUp({record, positionColumn, scope, fromPosition, to
239
239
  const quotedScope = connection.quote(resolvedScopeValue)
240
240
 
241
241
  // Load rows in descending order so we bump the highest first
242
- let query = modelClass
242
+ let query = record
243
+ .queryForModel(modelClass)
243
244
  .select(modelClass.primaryKey())
244
245
  .select(positionColumn)
245
246
  .where({[scopeColumnName]: resolvedScopeValue})
@@ -289,7 +290,7 @@ async function shiftPositionsUp({record, positionColumn, scope, fromPosition, to
289
290
  */
290
291
  async function shiftPositionsDown({record, positionColumn, scope, fromPosition, toPosition, scopeValue}) {
291
292
  const modelClass = /** @type {typeof import("./index.js").default} */ (record.constructor)
292
- const connection = modelClass.connection()
293
+ const connection = record.connection()
293
294
  const tableName = modelClass._getTable().getName()
294
295
  const resolvedScopeValue = scopeValue != null ? scopeValue : resolveScopeValue(record, scope)
295
296
  const scopeColumnName = modelClass.getColumnNameForAttributeName(scope)
@@ -304,7 +305,8 @@ async function shiftPositionsDown({record, positionColumn, scope, fromPosition,
304
305
  const quotedScope = connection.quote(resolvedScopeValue)
305
306
 
306
307
  // Load rows in ascending order so we shift the lowest gap first
307
- let query = modelClass
308
+ let query = record
309
+ .queryForModel(modelClass)
308
310
  .select(modelClass.primaryKey())
309
311
  .select(positionColumn)
310
312
  .where({[scopeColumnName]: resolvedScopeValue})
@@ -345,7 +347,7 @@ async function shiftPositionsDown({record, positionColumn, scope, fromPosition,
345
347
  */
346
348
  async function highestPositionInScope({record, positionColumn, scope, scopeValue}) {
347
349
  const modelClass = /** @type {typeof import("./index.js").default} */ (record.constructor)
348
- const connection = modelClass.connection()
350
+ const connection = record.connection()
349
351
  const scopeColumnName = modelClass.getColumnNameForAttributeName(scope)
350
352
  const positionColumnName = modelClass.getColumnNameForAttributeName(positionColumn)
351
353
  const positionColumnSql = connection.quoteColumn(positionColumnName)
@@ -353,7 +355,8 @@ async function highestPositionInScope({record, positionColumn, scope, scopeValue
353
355
 
354
356
  if (resolvedScopeValue == null) return 0
355
357
 
356
- const rows = await modelClass
358
+ const rows = await record
359
+ .queryForModel(modelClass)
357
360
  .select(positionColumn)
358
361
  .where({[scopeColumnName]: resolvedScopeValue})
359
362
  .order(`${positionColumnSql} DESC`)
@@ -415,7 +418,7 @@ function resolveScopeValue(record, scope) {
415
418
  */
416
419
  async function moveOutOfWay({record, positionColumn, scope, scopeValue}) {
417
420
  const modelClass = /** @type {typeof import("./index.js").default} */ (record.constructor)
418
- const connection = modelClass.connection()
421
+ const connection = record.connection()
419
422
  const tableName = modelClass._getTable().getName()
420
423
  const resolvedScopeValue = scopeValue != null ? scopeValue : resolveScopeValue(record, scope)
421
424
 
@@ -2,14 +2,24 @@
2
2
 
3
3
  import UploadedFile from "../../../http-server/client/uploaded-file/uploaded-file.js"
4
4
 
5
+ /**
6
+ * AttachmentPathSource type.
7
+ * @typedef {object} AttachmentPathSource
8
+ * @property {number} byteSize - Opened file snapshot size.
9
+ * @property {string} filePath - Validated source path for metadata only.
10
+ * @property {() => Promise<import("node:stream").Readable>} createReadStream - Creates a bounded snapshot stream.
11
+ * @property {() => Promise<Buffer>} readBuffer - Reads snapshot bytes for compatibility callers.
12
+ * @property {() => Promise<void>} close - Closes the owned source.
13
+ */
5
14
  /**
6
15
  * NormalizedAttachmentInput type.
7
16
  * @typedef {object} NormalizedAttachmentInput
8
17
  * @property {number} byteSize - File size in bytes.
9
- * @property {Buffer} contentBuffer - Raw content bytes.
10
- * @property {string} contentBase64 - Base64 encoded content.
18
+ * @property {Buffer | null} contentBuffer - Raw in-memory content bytes.
19
+ * @property {string | null} contentBase64 - Base64 encoded in-memory content.
11
20
  * @property {string | null} contentType - Content type.
12
21
  * @property {string} filename - Filename.
22
+ * @property {AttachmentPathSource | null} pathSource - Environment-owned opened path source.
13
23
  */
14
24
  /**
15
25
  * Runs base name.
@@ -120,8 +130,16 @@ export default async function normalizeRecordAttachmentInput(input, args = {}) {
120
130
  const environmentHandler = args.environmentHandler
121
131
  /**
122
132
  * Defines buffer.
123
- * @type {Buffer} */
124
- let buffer
133
+ * @type {Buffer | null} */
134
+ let buffer = null
135
+ /**
136
+ * Defines byte size.
137
+ * @type {number | null} */
138
+ let byteSize = null
139
+ /**
140
+ * Defines path source.
141
+ * @type {AttachmentPathSource | null} */
142
+ let pathSource = null
125
143
  /**
126
144
  * Content type.
127
145
  * @type {string | null} */
@@ -147,13 +165,15 @@ export default async function normalizeRecordAttachmentInput(input, args = {}) {
147
165
  const allowedPathPrefixes = Array.isArray(args.allowedPathPrefixes)
148
166
  ? args.allowedPathPrefixes.filter((entry) => typeof entry === "string" && entry.length > 0)
149
167
  : []
150
- const {buffer: fileBuffer, filePath} = await environmentHandler.resolveAttachmentInputPath({
168
+ pathSource = await environmentHandler.resolveAttachmentInputPath({
151
169
  allowedPathPrefixes,
152
170
  inputPath: input.path
153
171
  })
154
172
 
155
- buffer = fileBuffer
156
- filename = typeof input.filename === "string" && input.filename.length > 0 ? input.filename : baseName(filePath)
173
+ byteSize = pathSource.byteSize
174
+ filename = typeof input.filename === "string" && input.filename.length > 0
175
+ ? input.filename
176
+ : baseName(pathSource.filePath)
157
177
  contentType = typeof input.contentType === "string" && input.contentType.length > 0 ? input.contentType : null
158
178
  } else if (isPlainObject(input) && typeof input.contentBase64 === "string") {
159
179
  buffer = Buffer.from(input.contentBase64, "base64")
@@ -184,11 +204,19 @@ export default async function normalizeRecordAttachmentInput(input, args = {}) {
184
204
  ? baseName(filename)
185
205
  : ""
186
206
 
207
+ if (!buffer && !pathSource) {
208
+ throw new Error("Attachment input normalization produced no content")
209
+ }
210
+
211
+ if (buffer) byteSize = buffer.length
212
+ if (byteSize === null) throw new Error("Attachment input normalization produced no byte size")
213
+
187
214
  return {
188
- byteSize: buffer.length,
215
+ byteSize,
189
216
  contentBuffer: buffer,
190
- contentBase64: buffer.toString("base64"),
217
+ contentBase64: buffer ? buffer.toString("base64") : null,
191
218
  contentType,
192
- filename: normalizedFilename || defaultFilename
219
+ filename: normalizedFilename || defaultFilename,
220
+ pathSource
193
221
  }
194
222
  }
@@ -1,7 +1,9 @@
1
1
  // @ts-check
2
2
 
3
+ import { createWriteStream } from "node:fs"
3
4
  import fs from "fs/promises"
4
5
  import path from "path"
6
+ import { pipeline } from "node:stream/promises"
5
7
 
6
8
  /**
7
9
  * Runs normalize base url.
@@ -52,16 +54,45 @@ export default class FilesystemAttachmentStorageDriver {
52
54
  * Runs write.
53
55
  * @param {object} args - Options.
54
56
  * @param {string} args.attachmentId - Attachment id.
55
- * @param {{contentBuffer: Buffer, filename: string}} args.input - Normalized attachment input.
57
+ * @param {import("../normalize-input.js").NormalizedAttachmentInput} args.input - Normalized attachment input.
56
58
  * @returns {Promise<{storageKey: string}>} - Storage key result.
57
59
  */
58
60
  async write({attachmentId, input}) {
59
61
  const normalizedFilename = path.basename(input.filename || "attachment.bin")
60
62
  const storageKey = `${attachmentId}-${normalizedFilename}`
61
63
  const filePath = path.resolve(this.directory(), storageKey)
64
+ const temporaryFilePath = `${filePath}.tmp`
62
65
 
63
66
  await fs.mkdir(path.dirname(filePath), {recursive: true})
64
- await fs.writeFile(filePath, input.contentBuffer)
67
+
68
+ try {
69
+ if (input.pathSource) {
70
+ await pipeline(
71
+ await input.pathSource.createReadStream(),
72
+ createWriteStream(temporaryFilePath)
73
+ )
74
+ } else if (input.contentBuffer) {
75
+ await fs.writeFile(temporaryFilePath, input.contentBuffer)
76
+ } else {
77
+ throw new Error("Filesystem attachment input has no content")
78
+ }
79
+
80
+ await fs.rename(temporaryFilePath, filePath)
81
+ } catch (error) {
82
+ try {
83
+ await fs.unlink(temporaryFilePath)
84
+ } catch (cleanupError) {
85
+ if (!(cleanupError instanceof Error) || !("code" in cleanupError) || cleanupError.code !== "ENOENT") {
86
+ throw new AggregateError(
87
+ [error, cleanupError],
88
+ `Filesystem attachment write and partial-file cleanup both failed for ${storageKey}`,
89
+ {cause: cleanupError}
90
+ )
91
+ }
92
+ }
93
+
94
+ throw error
95
+ }
65
96
 
66
97
  return {storageKey}
67
98
  }
@@ -47,7 +47,7 @@ export default class NativeAttachmentStorageDriver {
47
47
  * Runs write.
48
48
  * @param {object} args - Write args.
49
49
  * @param {string} args.attachmentId - Attachment id.
50
- * @param {{contentBase64: string, contentType: string | null, filename: string}} args.input - Normalized attachment input.
50
+ * @param {import("../normalize-input.js").NormalizedAttachmentInput} args.input - Normalized attachment input.
51
51
  * @param {import("../../index.js").default} args.model - Model instance.
52
52
  * @param {string} args.name - Attachment name.
53
53
  * @returns {Promise<{storageKey: string}>} - Storage key result.
@@ -57,10 +57,25 @@ export default class NativeAttachmentStorageDriver {
57
57
  throwMissingMethod({driverName: this.name, methodName: "write"})
58
58
  }
59
59
 
60
+ let contentBase64 = input.contentBase64
61
+
62
+ if (input.pathSource) {
63
+ // Native write callbacks have always received Base64. Path input is
64
+ // intentionally buffered here, after driver selection, to preserve that
65
+ // public callback contract without forcing filesystem/S3 to buffer.
66
+ const contentBuffer = await input.pathSource.readBuffer()
67
+
68
+ contentBase64 = contentBuffer.toString("base64")
69
+ }
70
+
71
+ if (contentBase64 === null) {
72
+ throw new Error(`Attachment storage driver "${this.name}" input has no content`)
73
+ }
74
+
60
75
  const result = await this.options.write({
61
76
  attachmentId,
62
77
  attachmentName: name,
63
- contentBase64: input.contentBase64,
78
+ contentBase64,
64
79
  contentType: input.contentType,
65
80
  filename: input.filename,
66
81
  model
@@ -167,20 +167,40 @@ export default class S3AttachmentStorageDriver {
167
167
  * Runs write.
168
168
  * @param {object} args - Write args.
169
169
  * @param {string} args.attachmentId - Attachment id.
170
- * @param {{contentBuffer: Buffer, contentType: string | null, filename: string}} args.input - Normalized attachment input.
170
+ * @param {import("../normalize-input.js").NormalizedAttachmentInput} args.input - Normalized attachment input.
171
171
  * @returns {Promise<{storageKey: string}>} - Storage key.
172
172
  */
173
173
  async write({attachmentId, input}) {
174
174
  const {PutObjectCommand} = await this.s3Runtime()
175
175
  const client = await this.client()
176
176
  const storageKey = `${attachmentId}-${input.filename}`
177
+ /**
178
+ * S3 request body.
179
+ * @type {Buffer | import("node:stream").Readable | null} */
180
+ let body = input.contentBuffer
181
+ /**
182
+ * Path input stream.
183
+ * @type {import("node:stream").Readable | null} */
184
+ let pathInputStream = null
185
+
186
+ if (input.pathSource) {
187
+ pathInputStream = await input.pathSource.createReadStream()
188
+ body = pathInputStream
189
+ }
177
190
 
178
- await client.send(new PutObjectCommand({
179
- Body: input.contentBuffer,
180
- Bucket: this.bucket(),
181
- ContentType: input.contentType || undefined,
182
- Key: storageKey
183
- }))
191
+ if (!body) throw new Error("S3 attachment input has no content")
192
+
193
+ try {
194
+ await client.send(new PutObjectCommand({
195
+ Body: body,
196
+ Bucket: this.bucket(),
197
+ ContentLength: input.byteSize,
198
+ ContentType: input.contentType || undefined,
199
+ Key: storageKey
200
+ }))
201
+ } finally {
202
+ if (pathInputStream) pathInputStream.destroy()
203
+ }
184
204
 
185
205
  return {storageKey}
186
206
  }