velocious 1.0.568 → 1.0.570

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 (152) hide show
  1. package/README.md +30 -0
  2. package/build/configuration.js +36 -0
  3. package/build/database/drivers/base.js +244 -93
  4. package/build/database/drivers/mssql/index.js +10 -7
  5. package/build/database/drivers/mysql/index.js +10 -5
  6. package/build/database/drivers/pgsql/index.js +4 -4
  7. package/build/database/drivers/sqlite/base.js +2 -2
  8. package/build/database/drivers/sqlite/connection-sql-js.js +62 -2
  9. package/build/database/drivers/sqlite/index.web.js +74 -0
  10. package/build/database/operation-connection.js +121 -0
  11. package/build/database/operation-lease.js +47 -0
  12. package/build/database/operation.js +150 -0
  13. package/build/database/pool/base.js +26 -1
  14. package/build/database/pool/single-multi-use.js +49 -0
  15. package/build/database/query/model-class-query.js +38 -7
  16. package/build/database/query/preloader/belongs-to.js +3 -2
  17. package/build/database/query/preloader/has-many.js +4 -3
  18. package/build/database/query/preloader/has-one.js +2 -1
  19. package/build/database/query/preloader/query-for-model.js +17 -0
  20. package/build/database/query/query-data.js +12 -3
  21. package/build/database/query/with-count.js +5 -3
  22. package/build/database/record/acts-as-list.js +11 -8
  23. package/build/database/record/attachments/store.js +15 -11
  24. package/build/database/record/auditing.js +12 -9
  25. package/build/database/record/counter-cache-magnitude.js +5 -3
  26. package/build/database/record/index.js +95 -14
  27. package/build/database/record/instance-relationships/belongs-to.js +4 -2
  28. package/build/database/record/instance-relationships/has-many.js +7 -5
  29. package/build/database/record/instance-relationships/has-one.js +4 -2
  30. package/build/database/record/validators/uniqueness.js +3 -2
  31. package/build/frontend-models/websocket-publishers.js +2 -2
  32. package/build/src/configuration.d.ts +12 -0
  33. package/build/src/configuration.d.ts.map +1 -1
  34. package/build/src/configuration.js +35 -1
  35. package/build/src/database/drivers/base.d.ts +96 -23
  36. package/build/src/database/drivers/base.d.ts.map +1 -1
  37. package/build/src/database/drivers/base.js +230 -90
  38. package/build/src/database/drivers/mssql/index.d.ts +4 -1
  39. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  40. package/build/src/database/drivers/mssql/index.js +11 -8
  41. package/build/src/database/drivers/mysql/index.d.ts +2 -1
  42. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  43. package/build/src/database/drivers/mysql/index.js +10 -6
  44. package/build/src/database/drivers/pgsql/index.d.ts +2 -1
  45. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  46. package/build/src/database/drivers/pgsql/index.js +5 -5
  47. package/build/src/database/drivers/sqlite/base.d.ts +1 -1
  48. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  49. package/build/src/database/drivers/sqlite/base.js +3 -3
  50. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +20 -0
  51. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
  52. package/build/src/database/drivers/sqlite/connection-sql-js.js +56 -3
  53. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  54. package/build/src/database/drivers/sqlite/index.web.js +64 -1
  55. package/build/src/database/operation-connection.d.ts +67 -0
  56. package/build/src/database/operation-connection.d.ts.map +1 -0
  57. package/build/src/database/operation-connection.js +102 -0
  58. package/build/src/database/operation-lease.d.ts +27 -0
  59. package/build/src/database/operation-lease.d.ts.map +1 -0
  60. package/build/src/database/operation-lease.js +44 -0
  61. package/build/src/database/operation.d.ts +85 -0
  62. package/build/src/database/operation.d.ts.map +1 -0
  63. package/build/src/database/operation.js +132 -0
  64. package/build/src/database/pool/base.d.ts +14 -0
  65. package/build/src/database/pool/base.d.ts.map +1 -1
  66. package/build/src/database/pool/base.js +24 -2
  67. package/build/src/database/pool/single-multi-use.d.ts +1 -0
  68. package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
  69. package/build/src/database/pool/single-multi-use.js +49 -1
  70. package/build/src/database/query/model-class-query.d.ts +14 -0
  71. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  72. package/build/src/database/query/model-class-query.js +32 -8
  73. package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
  74. package/build/src/database/query/preloader/belongs-to.js +4 -3
  75. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  76. package/build/src/database/query/preloader/has-many.js +5 -4
  77. package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
  78. package/build/src/database/query/preloader/has-one.js +3 -2
  79. package/build/src/database/query/preloader/query-for-model.d.ts +10 -0
  80. package/build/src/database/query/preloader/query-for-model.d.ts.map +1 -0
  81. package/build/src/database/query/preloader/query-for-model.js +16 -0
  82. package/build/src/database/query/query-data.d.ts.map +1 -1
  83. package/build/src/database/query/query-data.js +13 -4
  84. package/build/src/database/query/with-count.d.ts.map +1 -1
  85. package/build/src/database/query/with-count.js +6 -4
  86. package/build/src/database/record/acts-as-list.js +12 -9
  87. package/build/src/database/record/attachments/attachment-record.d.ts +9 -0
  88. package/build/src/database/record/attachments/attachment-record.d.ts.map +1 -1
  89. package/build/src/database/record/attachments/store.d.ts +4 -2
  90. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  91. package/build/src/database/record/attachments/store.js +16 -12
  92. package/build/src/database/record/auditing.d.ts.map +1 -1
  93. package/build/src/database/record/auditing.js +13 -10
  94. package/build/src/database/record/counter-cache-magnitude.js +6 -4
  95. package/build/src/database/record/index.d.ts +42 -2
  96. package/build/src/database/record/index.d.ts.map +1 -1
  97. package/build/src/database/record/index.js +87 -15
  98. package/build/src/database/record/instance-relationships/belongs-to.d.ts.map +1 -1
  99. package/build/src/database/record/instance-relationships/belongs-to.js +4 -3
  100. package/build/src/database/record/instance-relationships/has-many.d.ts.map +1 -1
  101. package/build/src/database/record/instance-relationships/has-many.js +7 -5
  102. package/build/src/database/record/instance-relationships/has-one.d.ts.map +1 -1
  103. package/build/src/database/record/instance-relationships/has-one.js +4 -3
  104. package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
  105. package/build/src/database/record/validators/uniqueness.js +4 -3
  106. package/build/src/frontend-models/websocket-publishers.js +3 -3
  107. package/build/src/sync/server-sequence-allocator.d.ts +16 -4
  108. package/build/src/sync/server-sequence-allocator.d.ts.map +1 -1
  109. package/build/src/sync/server-sequence-allocator.js +37 -11
  110. package/build/src/sync/sync-client.d.ts.map +1 -1
  111. package/build/src/sync/sync-client.js +7 -3
  112. package/build/src/sync/sync-publisher.d.ts +2 -1
  113. package/build/src/sync/sync-publisher.d.ts.map +1 -1
  114. package/build/src/sync/sync-publisher.js +11 -6
  115. package/build/sync/server-sequence-allocator.js +43 -10
  116. package/build/sync/sync-client.js +6 -2
  117. package/build/sync/sync-publisher.js +10 -5
  118. package/build/tsconfig.tsbuildinfo +1 -1
  119. package/package.json +1 -1
  120. package/src/configuration.js +36 -0
  121. package/src/database/drivers/base.js +244 -93
  122. package/src/database/drivers/mssql/index.js +10 -7
  123. package/src/database/drivers/mysql/index.js +10 -5
  124. package/src/database/drivers/pgsql/index.js +4 -4
  125. package/src/database/drivers/sqlite/base.js +2 -2
  126. package/src/database/drivers/sqlite/connection-sql-js.js +62 -2
  127. package/src/database/drivers/sqlite/index.web.js +74 -0
  128. package/src/database/operation-connection.js +121 -0
  129. package/src/database/operation-lease.js +47 -0
  130. package/src/database/operation.js +150 -0
  131. package/src/database/pool/base.js +26 -1
  132. package/src/database/pool/single-multi-use.js +49 -0
  133. package/src/database/query/model-class-query.js +38 -7
  134. package/src/database/query/preloader/belongs-to.js +3 -2
  135. package/src/database/query/preloader/has-many.js +4 -3
  136. package/src/database/query/preloader/has-one.js +2 -1
  137. package/src/database/query/preloader/query-for-model.js +17 -0
  138. package/src/database/query/query-data.js +12 -3
  139. package/src/database/query/with-count.js +5 -3
  140. package/src/database/record/acts-as-list.js +11 -8
  141. package/src/database/record/attachments/store.js +15 -11
  142. package/src/database/record/auditing.js +12 -9
  143. package/src/database/record/counter-cache-magnitude.js +5 -3
  144. package/src/database/record/index.js +95 -14
  145. package/src/database/record/instance-relationships/belongs-to.js +4 -2
  146. package/src/database/record/instance-relationships/has-many.js +7 -5
  147. package/src/database/record/instance-relationships/has-one.js +4 -2
  148. package/src/database/record/validators/uniqueness.js +3 -2
  149. package/src/frontend-models/websocket-publishers.js +2 -2
  150. package/src/sync/server-sequence-allocator.js +43 -10
  151. package/src/sync/sync-client.js +6 -2
  152. package/src/sync/sync-publisher.js +10 -5
@@ -444,6 +444,11 @@ class VelociousDatabaseRecord {
444
444
  * @type {import("../drivers/base.js").default | undefined} */
445
445
  __connection = undefined
446
446
 
447
+ /**
448
+ * Explicit operation owning this record's database work.
449
+ * @type {import("../operation.js").default | undefined} */
450
+ _databaseOperation = undefined
451
+
447
452
  /**
448
453
  * Instance relationships.
449
454
  * @type {Record<string, import("./instance-relationships/base.js").default>} */
@@ -863,9 +868,10 @@ class VelociousDatabaseRecord {
863
868
  * single UPDATE ... SET col = (SELECT COUNT(*)) so concurrent
864
869
  * creates/destroys cannot race into a stale count.
865
870
  * @param {number | string | null} parentId - Parent primary-key value.
871
+ * @param {VelociousDatabaseRecord} record - Child record owning the connection.
866
872
  * @returns {Promise<void>} - Resolves when the counter cache has been synced.
867
873
  */
868
- async function syncCounter(parentId) {
874
+ async function syncCounter(parentId, record) {
869
875
  if (!parentId) return
870
876
 
871
877
  const relationship = ChildModel.getRelationshipByName(relationshipName)
@@ -880,7 +886,9 @@ class VelociousDatabaseRecord {
880
886
  const parentTable = ParentModel.tableName()
881
887
  const childTable = ChildModel.tableName()
882
888
  const pkColumn = inflection.underscore(primaryKey)
883
- const connection = ParentModel.connection()
889
+ const connection = record
890
+ .queryForModel(ParentModel)
891
+ .driver
884
892
  const quoted = connection.quote(parentId)
885
893
 
886
894
  const sql = `UPDATE ${connection.quoteTable(parentTable)} SET ${connection.quoteColumn(counterColumn)} = (SELECT COUNT(*) FROM ${connection.quoteTable(childTable)} WHERE ${connection.quoteColumn(fk)} = ${quoted}) WHERE ${connection.quoteColumn(pkColumn)} = ${quoted}`
@@ -901,11 +909,11 @@ class VelociousDatabaseRecord {
901
909
  }
902
910
 
903
911
  ChildModel.afterCreate(async (record) => {
904
- await syncCounter(readFkAttribute(record))
912
+ await syncCounter(readFkAttribute(record), record)
905
913
  })
906
914
 
907
915
  ChildModel.afterDestroy(async (record) => {
908
- await syncCounter(readFkAttribute(record))
916
+ await syncCounter(readFkAttribute(record), record)
909
917
  })
910
918
 
911
919
  ChildModel.beforeSave(async (record) => {
@@ -932,8 +940,8 @@ class VelociousDatabaseRecord {
932
940
 
933
941
  if (previousParentId !== undefined) {
934
942
  delete model[prevKey]
935
- await syncCounter(previousParentId)
936
- await syncCounter(readFkAttribute(model))
943
+ await syncCounter(previousParentId, record)
944
+ await syncCounter(readFkAttribute(model), record)
937
945
  }
938
946
  })
939
947
  }
@@ -1452,15 +1460,16 @@ class VelociousDatabaseRecord {
1452
1460
  * Runs initialize record.
1453
1461
  * @param {object} args - Options object.
1454
1462
  * @param {import("../../configuration.js").default} args.configuration - Configuration instance.
1463
+ * @param {import("../drivers/base.js").default} [args.connection] - Explicit metadata connection.
1455
1464
  * @returns {Promise<void>} - Resolves when complete.
1456
1465
  */
1457
- static async initializeRecord({configuration, ...restArgs}) {
1466
+ static async initializeRecord({configuration, connection: explicitConnection, ...restArgs}) {
1458
1467
  restArgsError(restArgs)
1459
1468
 
1460
1469
  if (!configuration) throw new Error(`No configuration given for ${this.name}`)
1461
1470
 
1462
1471
  this.registerRecordClass({configuration})
1463
- const connection = this.connection({enforceTenantDatabaseScope: false})
1472
+ const connection = explicitConnection || this.connection({enforceTenantDatabaseScope: false})
1464
1473
 
1465
1474
  this._databaseType = connection.getType()
1466
1475
 
@@ -2427,7 +2436,7 @@ class VelociousDatabaseRecord {
2427
2436
  await this._runLifecycleCallbacks("beforeValidation")
2428
2437
  await this._runValidations()
2429
2438
 
2430
- await this.getModelClass().transaction(async () => {
2439
+ const saveInTransaction = async () => {
2431
2440
  await this._runLifecycleCallbacks("beforeSave")
2432
2441
 
2433
2442
  // If any belongs-to-relationships was saved, then updated-at should still be set on this record.
@@ -2454,7 +2463,13 @@ class VelociousDatabaseRecord {
2454
2463
  await this._autoSaveAttachments()
2455
2464
  await this._runLifecycleCallbacks("afterSave")
2456
2465
  await this._emitRecordChangeAfterCommit(isNewRecord ? "create" : "update")
2457
- })
2466
+ }
2467
+
2468
+ if (this._databaseOperation) {
2469
+ await this._databaseOperation.transaction(saveInTransaction)
2470
+ } else {
2471
+ await this.getModelClass().transaction(saveInTransaction)
2472
+ }
2458
2473
  })
2459
2474
 
2460
2475
  this._assignedAttributeNames = undefined
@@ -2481,6 +2496,7 @@ class VelociousDatabaseRecord {
2481
2496
  if (model) {
2482
2497
  if (model instanceof VelociousDatabaseRecord) {
2483
2498
  if (model.isChanged()) {
2499
+ this.bindRelatedRecord(model)
2484
2500
  await model.save()
2485
2501
 
2486
2502
  const foreignKey = this._relationshipForeignKeyAttribute(instanceRelationship)
@@ -2539,6 +2555,7 @@ class VelociousDatabaseRecord {
2539
2555
 
2540
2556
  if (loaded) {
2541
2557
  for (const model of loaded) {
2558
+ this.bindRelatedRecord(model)
2542
2559
  const foreignKey = model._relationshipForeignKeyAttribute(instanceRelationship)
2543
2560
 
2544
2561
  model.setAttribute(foreignKey, this.id())
@@ -2592,6 +2609,7 @@ class VelociousDatabaseRecord {
2592
2609
  }
2593
2610
 
2594
2611
  for (const model of loaded) {
2612
+ this.bindRelatedRecord(model)
2595
2613
  const foreignKey = model._relationshipForeignKeyAttribute(instanceRelationship)
2596
2614
 
2597
2615
  model.setAttribute(foreignKey, this.id())
@@ -3021,15 +3039,18 @@ class VelociousDatabaseRecord {
3021
3039
  * Runs new query.
3022
3040
  * @template {typeof VelociousDatabaseRecord} MC
3023
3041
  * @this {MC}
3042
+ * @param {{driver?: import("../drivers/base.js").default | (() => import("../drivers/base.js").default), operation?: import("../operation.js").default}} [args] - Explicit query ownership.
3024
3043
  * @returns {ModelClassQuery<MC>} - The new query.
3025
3044
  */
3026
- static _newQuery() {
3045
+ static _newQuery({driver = () => this.connection(), operation, ...restArgs} = {}) {
3046
+ restArgsError(restArgs)
3027
3047
  this._assertHasBeenInitialized()
3028
3048
  const handler = new Handler()
3029
3049
  const query = new ModelClassQuery({
3030
- driver: () => this.connection(),
3050
+ driver,
3031
3051
  handler,
3032
- modelClass: this
3052
+ modelClass: this,
3053
+ operation
3033
3054
  })
3034
3055
 
3035
3056
  return query.from(new FromTable(this.tableName()))
@@ -3416,6 +3437,53 @@ class VelociousDatabaseRecord {
3416
3437
  }
3417
3438
  }
3418
3439
 
3440
+ /**
3441
+ * Binds future query, lifecycle, relationship, and persistence work to an operation.
3442
+ * @param {import("../operation.js").default} operation - Owning operation.
3443
+ * @returns {this} - Bound record.
3444
+ */
3445
+ bindDatabaseOperation(operation) {
3446
+ if (this._databaseOperation && this._databaseOperation !== operation) {
3447
+ throw new Error("Record is already bound to another database operation")
3448
+ }
3449
+
3450
+ this._databaseOperation = operation
3451
+
3452
+ return this
3453
+ }
3454
+
3455
+ /**
3456
+ * Returns the explicit operation owning this record, if any.
3457
+ * @returns {import("../operation.js").default | undefined} - Owning operation.
3458
+ */
3459
+ databaseOperation() {
3460
+ return this._databaseOperation
3461
+ }
3462
+
3463
+ /**
3464
+ * Binds a related record to the same operation as this record.
3465
+ * @template {VelociousDatabaseRecord} Model
3466
+ * @param {Model} record - Related record.
3467
+ * @returns {Model} - Related record.
3468
+ */
3469
+ bindRelatedRecord(record) {
3470
+ if (this._databaseOperation) this._databaseOperation.bindRecord(record)
3471
+
3472
+ return record
3473
+ }
3474
+
3475
+ /**
3476
+ * Builds a model query preserving this record's operation ownership.
3477
+ * @template {typeof VelociousDatabaseRecord} MC
3478
+ * @param {MC} ModelClass - Target model class.
3479
+ * @returns {ModelClassQuery<MC>} - Target query.
3480
+ */
3481
+ queryForModel(ModelClass) {
3482
+ if (this._databaseOperation) return this._databaseOperation.forModel(ModelClass)
3483
+
3484
+ return ModelClass._newQuery()
3485
+ }
3486
+
3419
3487
  /**
3420
3488
  * Runs load existing record.
3421
3489
  * @param {object} attributes - Attributes.
@@ -3473,11 +3541,20 @@ class VelociousDatabaseRecord {
3473
3541
  * @returns {import("../drivers/base.js").default} - The connection.
3474
3542
  */
3475
3543
  _connection() {
3544
+ if (this._databaseOperation) return this._databaseOperation.connection()
3476
3545
  if (this.__connection) return this.__connection
3477
3546
 
3478
3547
  return this.getModelClass().connection()
3479
3548
  }
3480
3549
 
3550
+ /**
3551
+ * Returns the connection that owns this record's database work.
3552
+ * @returns {import("../drivers/base.js").default} - Connection.
3553
+ */
3554
+ connection() {
3555
+ return this._connection()
3556
+ }
3557
+
3481
3558
  /**
3482
3559
  * Counts dependent records for a `dependent: "restrict"` relationship.
3483
3560
  * @param {RestrictInstanceRelationship} instanceRelationship - Relationship instance to count.
@@ -4374,7 +4451,11 @@ class VelociousDatabaseRecord {
4374
4451
 
4375
4452
  whereObject[primaryKey] = id
4376
4453
 
4377
- const query = /** @type {import("../query/model-class-query.js").default<MC>} */ (this.getModelClass().where(whereObject))
4454
+ const query = /** @type {import("../query/model-class-query.js").default<MC>} */ (
4455
+ this
4456
+ .queryForModel(this.getModelClass())
4457
+ .where(whereObject)
4458
+ )
4378
4459
  const reloadedModel = await query.first()
4379
4460
 
4380
4461
  if (!reloadedModel) throw new Error(`${this.constructor.name}#${id} couldn't be reloaded - record didn't exist`)
@@ -27,7 +27,9 @@ export default class VelociousDatabaseRecordBelongsToInstanceRelationship extend
27
27
 
28
28
  if (!TargetModelClass) throw new Error("Can't build a new record without a target model")
29
29
 
30
- const newInstance = /** @type {InstanceType<TMC>} */ (new TargetModelClass(data))
30
+ const newInstance = this.getModel().bindRelatedRecord(
31
+ /** @type {InstanceType<TMC>} */ (new TargetModelClass(data))
32
+ )
31
33
 
32
34
  this._loaded = newInstance
33
35
 
@@ -85,7 +87,7 @@ export default class VelociousDatabaseRecordBelongsToInstanceRelationship extend
85
87
 
86
88
  whereArgs[primaryKey] = foreignModelID
87
89
 
88
- const query = this.applyScope(TargetModelClass.where(whereArgs))
90
+ const query = this.applyScope(this.getModel().queryForModel(TargetModelClass).where(whereArgs))
89
91
 
90
92
  const foreignModel = await query.first()
91
93
 
@@ -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
 
@@ -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
  /**