velocious 1.0.592 → 1.0.594

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 (197) hide show
  1. package/README.md +11 -2
  2. package/build/configuration-types.js +1 -1
  3. package/build/configuration.js +68 -13
  4. package/build/database/drivers/base.js +83 -34
  5. package/build/database/drivers/mssql/index.js +32 -0
  6. package/build/database/drivers/mysql/index.js +17 -0
  7. package/build/database/drivers/pgsql/index.js +11 -0
  8. package/build/database/drivers/sqlite/base.js +12 -0
  9. package/build/database/drivers/sqlite/connection-sql-js.js +3 -2
  10. package/build/database/drivers/sqlite/index.js +7 -1
  11. package/build/database/drivers/sqlite/index.native.js +9 -3
  12. package/build/database/drivers/sqlite/index.web.js +6 -3
  13. package/build/database/migrator.js +90 -24
  14. package/build/database/operation.js +55 -4
  15. package/build/database/query/join-object.js +7 -4
  16. package/build/database/query/model-class-query.js +38 -7
  17. package/build/database/query/preloader/belongs-to.js +9 -6
  18. package/build/database/query/preloader/has-many.js +25 -14
  19. package/build/database/query/preloader/has-one.js +7 -4
  20. package/build/database/query/preloader/query-for-model.js +16 -1
  21. package/build/database/query/preloader.js +6 -4
  22. package/build/database/query/query-data.js +3 -3
  23. package/build/database/query/where-model-class-hash.js +8 -4
  24. package/build/database/query/with-count.js +4 -3
  25. package/build/database/record/index.js +148 -5
  26. package/build/database/record/instance-relationships/base.js +13 -0
  27. package/build/database/record/instance-relationships/belongs-to.js +1 -1
  28. package/build/database/record/instance-relationships/has-many.js +5 -2
  29. package/build/database/record/instance-relationships/has-one.js +1 -1
  30. package/build/database/record/relationships/base.js +17 -0
  31. package/build/database/use-database.js +81 -28
  32. package/build/environment-handlers/base.js +18 -0
  33. package/build/environment-handlers/node.js +32 -0
  34. package/build/src/configuration-types.d.ts +2 -2
  35. package/build/src/configuration-types.js +2 -2
  36. package/build/src/configuration.d.ts +13 -4
  37. package/build/src/configuration.d.ts.map +1 -1
  38. package/build/src/configuration.js +56 -16
  39. package/build/src/database/drivers/base.d.ts +22 -0
  40. package/build/src/database/drivers/base.d.ts.map +1 -1
  41. package/build/src/database/drivers/base.js +81 -37
  42. package/build/src/database/drivers/mssql/index.d.ts +7 -0
  43. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  44. package/build/src/database/drivers/mssql/index.js +15 -1
  45. package/build/src/database/drivers/mysql/index.d.ts +7 -0
  46. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  47. package/build/src/database/drivers/mysql/index.js +15 -1
  48. package/build/src/database/drivers/pgsql/index.d.ts +6 -0
  49. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  50. package/build/src/database/drivers/pgsql/index.js +10 -1
  51. package/build/src/database/drivers/sqlite/base.d.ts +7 -0
  52. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  53. package/build/src/database/drivers/sqlite/base.js +11 -1
  54. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +4 -1
  55. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
  56. package/build/src/database/drivers/sqlite/connection-sql-js.js +4 -3
  57. package/build/src/database/drivers/sqlite/index.d.ts +2 -1
  58. package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
  59. package/build/src/database/drivers/sqlite/index.js +7 -2
  60. package/build/src/database/drivers/sqlite/index.native.d.ts +2 -1
  61. package/build/src/database/drivers/sqlite/index.native.d.ts.map +1 -1
  62. package/build/src/database/drivers/sqlite/index.native.js +9 -4
  63. package/build/src/database/drivers/sqlite/index.web.d.ts +2 -1
  64. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  65. package/build/src/database/drivers/sqlite/index.web.js +7 -4
  66. package/build/src/database/migrator.d.ts +34 -0
  67. package/build/src/database/migrator.d.ts.map +1 -1
  68. package/build/src/database/migrator.js +83 -23
  69. package/build/src/database/operation.d.ts +15 -1
  70. package/build/src/database/operation.d.ts.map +1 -1
  71. package/build/src/database/operation.js +52 -5
  72. package/build/src/database/query/join-object.d.ts.map +1 -1
  73. package/build/src/database/query/join-object.js +7 -5
  74. package/build/src/database/query/model-class-query.d.ts +6 -0
  75. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  76. package/build/src/database/query/model-class-query.js +33 -8
  77. package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
  78. package/build/src/database/query/preloader/belongs-to.js +9 -7
  79. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  80. package/build/src/database/query/preloader/has-many.js +23 -16
  81. package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
  82. package/build/src/database/query/preloader/has-one.js +7 -5
  83. package/build/src/database/query/preloader/query-for-model.d.ts +8 -0
  84. package/build/src/database/query/preloader/query-for-model.d.ts.map +1 -1
  85. package/build/src/database/query/preloader/query-for-model.js +15 -2
  86. package/build/src/database/query/preloader.d.ts.map +1 -1
  87. package/build/src/database/query/preloader.js +6 -5
  88. package/build/src/database/query/query-data.js +4 -4
  89. package/build/src/database/query/where-model-class-hash.d.ts.map +1 -1
  90. package/build/src/database/query/where-model-class-hash.js +7 -5
  91. package/build/src/database/query/with-count.js +5 -4
  92. package/build/src/database/record/index.d.ts +95 -0
  93. package/build/src/database/record/index.d.ts.map +1 -1
  94. package/build/src/database/record/index.js +126 -6
  95. package/build/src/database/record/instance-relationships/base.d.ts +6 -0
  96. package/build/src/database/record/instance-relationships/base.d.ts.map +1 -1
  97. package/build/src/database/record/instance-relationships/base.js +12 -1
  98. package/build/src/database/record/instance-relationships/belongs-to.js +2 -2
  99. package/build/src/database/record/instance-relationships/has-many.d.ts.map +1 -1
  100. package/build/src/database/record/instance-relationships/has-many.js +6 -3
  101. package/build/src/database/record/instance-relationships/has-one.js +2 -2
  102. package/build/src/database/record/relationships/base.d.ts +11 -0
  103. package/build/src/database/record/relationships/base.d.ts.map +1 -1
  104. package/build/src/database/record/relationships/base.js +15 -1
  105. package/build/src/database/use-database.d.ts +33 -5
  106. package/build/src/database/use-database.d.ts.map +1 -1
  107. package/build/src/database/use-database.js +81 -30
  108. package/build/src/environment-handlers/base.d.ts +16 -0
  109. package/build/src/environment-handlers/base.d.ts.map +1 -1
  110. package/build/src/environment-handlers/base.js +17 -1
  111. package/build/src/environment-handlers/node.d.ts +20 -0
  112. package/build/src/environment-handlers/node.d.ts.map +1 -1
  113. package/build/src/environment-handlers/node.js +29 -1
  114. package/build/src/sync/server-change-feed.d.ts +14 -2
  115. package/build/src/sync/server-change-feed.d.ts.map +1 -1
  116. package/build/src/sync/server-change-feed.js +50 -10
  117. package/build/src/sync/sync-scope-store.d.ts +13 -2
  118. package/build/src/sync/sync-scope-store.d.ts.map +1 -1
  119. package/build/src/sync/sync-scope-store.js +48 -7
  120. package/build/src/tenants/frontend-tenant-sqlite-lifecycle.d.ts +43 -1
  121. package/build/src/tenants/frontend-tenant-sqlite-lifecycle.d.ts.map +1 -1
  122. package/build/src/tenants/frontend-tenant-sqlite-lifecycle.js +141 -17
  123. package/build/src/tenants/tenant-handle.d.ts +36 -0
  124. package/build/src/tenants/tenant-handle.d.ts.map +1 -1
  125. package/build/src/tenants/tenant-handle.js +73 -3
  126. package/build/src/testing/browser-test-app.js +3 -1
  127. package/build/src/testing/browser-use-database-hook-scenarios.d.ts +16 -0
  128. package/build/src/testing/browser-use-database-hook-scenarios.d.ts.map +1 -0
  129. package/build/src/testing/browser-use-database-hook-scenarios.js +182 -0
  130. package/build/src/testing/shared-transaction-broker.d.ts +14 -0
  131. package/build/src/testing/shared-transaction-broker.d.ts.map +1 -1
  132. package/build/src/testing/shared-transaction-broker.js +44 -7
  133. package/build/src/testing/shared-transaction-connection-coordinator.d.ts +5 -4
  134. package/build/src/testing/shared-transaction-connection-coordinator.d.ts.map +1 -1
  135. package/build/src/testing/shared-transaction-connection-coordinator.js +21 -10
  136. package/build/src/utils/model-scope.d.ts +2 -2
  137. package/build/src/utils/model-scope.d.ts.map +1 -1
  138. package/build/src/utils/model-scope.js +5 -3
  139. package/build/src/utils/ransack.d.ts.map +1 -1
  140. package/build/src/utils/ransack.js +5 -3
  141. package/build/sync/server-change-feed.js +58 -8
  142. package/build/sync/sync-scope-store.js +55 -5
  143. package/build/tenants/frontend-tenant-sqlite-lifecycle.js +150 -15
  144. package/build/tenants/tenant-handle.js +90 -6
  145. package/build/testing/browser-test-app.js +2 -0
  146. package/build/testing/browser-use-database-hook-scenarios.js +204 -0
  147. package/build/testing/shared-transaction-broker.js +48 -7
  148. package/build/testing/shared-transaction-connection-coordinator.js +23 -9
  149. package/build/tsconfig.tsbuildinfo +1 -1
  150. package/build/utils/model-scope.js +5 -2
  151. package/build/utils/ransack.js +5 -2
  152. package/package.json +1 -1
  153. package/scripts/browser-test-session.js +489 -0
  154. package/scripts/prewarm-chromedriver.js +3 -22
  155. package/scripts/test-browser.js +52 -16
  156. package/src/configuration-types.js +1 -1
  157. package/src/configuration.js +68 -13
  158. package/src/database/drivers/base.js +83 -34
  159. package/src/database/drivers/mssql/index.js +32 -0
  160. package/src/database/drivers/mysql/index.js +17 -0
  161. package/src/database/drivers/pgsql/index.js +11 -0
  162. package/src/database/drivers/sqlite/base.js +12 -0
  163. package/src/database/drivers/sqlite/connection-sql-js.js +3 -2
  164. package/src/database/drivers/sqlite/index.js +7 -1
  165. package/src/database/drivers/sqlite/index.native.js +9 -3
  166. package/src/database/drivers/sqlite/index.web.js +6 -3
  167. package/src/database/migrator.js +90 -24
  168. package/src/database/operation.js +55 -4
  169. package/src/database/query/join-object.js +7 -4
  170. package/src/database/query/model-class-query.js +38 -7
  171. package/src/database/query/preloader/belongs-to.js +9 -6
  172. package/src/database/query/preloader/has-many.js +25 -14
  173. package/src/database/query/preloader/has-one.js +7 -4
  174. package/src/database/query/preloader/query-for-model.js +16 -1
  175. package/src/database/query/preloader.js +6 -4
  176. package/src/database/query/query-data.js +3 -3
  177. package/src/database/query/where-model-class-hash.js +8 -4
  178. package/src/database/query/with-count.js +4 -3
  179. package/src/database/record/index.js +148 -5
  180. package/src/database/record/instance-relationships/base.js +13 -0
  181. package/src/database/record/instance-relationships/belongs-to.js +1 -1
  182. package/src/database/record/instance-relationships/has-many.js +5 -2
  183. package/src/database/record/instance-relationships/has-one.js +1 -1
  184. package/src/database/record/relationships/base.js +17 -0
  185. package/src/database/use-database.js +81 -28
  186. package/src/environment-handlers/base.js +18 -0
  187. package/src/environment-handlers/node.js +32 -0
  188. package/src/sync/server-change-feed.js +58 -8
  189. package/src/sync/sync-scope-store.js +55 -5
  190. package/src/tenants/frontend-tenant-sqlite-lifecycle.js +150 -15
  191. package/src/tenants/tenant-handle.js +90 -6
  192. package/src/testing/browser-test-app.js +2 -0
  193. package/src/testing/browser-use-database-hook-scenarios.js +204 -0
  194. package/src/testing/shared-transaction-broker.js +48 -7
  195. package/src/testing/shared-transaction-connection-coordinator.js +23 -9
  196. package/src/utils/model-scope.js +5 -2
  197. package/src/utils/ransack.js +5 -2
@@ -155,13 +155,14 @@ function queryForEntry({entry, modelClass, sourceModel}) {
155
155
  throw new Error(`withCount currently supports only hasMany relationships; ${modelClass.name}#${entry.relationshipName} is ${relationship.type}`)
156
156
  }
157
157
 
158
- const targetModelClass = relationship.getTargetModelClass()
158
+ const rawTargetModelClass = relationship.getTargetModelClass()
159
159
 
160
- if (!targetModelClass) {
160
+ if (!rawTargetModelClass) {
161
161
  throw new Error(`withCount: could not resolve target model for ${modelClass.name}#${entry.relationshipName}`)
162
162
  }
163
163
 
164
- const foreignKey = relationship.getForeignKey()
164
+ const targetModelClass = modelClass.bindRecordMetadataModelClass(rawTargetModelClass)
165
+ const foreignKey = relationship.getForeignKeyForModelClasses({modelClass, targetModelClass})
165
166
  /**
166
167
  * Mandatory cohort conditions.
167
168
  * @type {Record<string, ReturnType<typeof JSON.parse>>} */
@@ -24,6 +24,11 @@
24
24
 
25
25
  /** @typedef {import("../../configuration-types.js").TenantDatabaseProviderType} TenantDatabaseProviderType */
26
26
 
27
+ /**
28
+ * Schema metadata cached for one record class and physical database generation.
29
+ * @typedef {boolean | null | string | undefined | Promise<void> | string[] | import("../drivers/base-column.js").default[] | import("../drivers/base-table.js").default | Record<string, string> | Record<string, string | undefined> | Record<string, import("../drivers/base-column.js").default>} RecordMetadataValue
30
+ */
31
+
27
32
  import AdvisoryLockRunner, {AdvisoryLockBusyError, AdvisoryLockHoldTimeoutError, AdvisoryLockTimeoutError} from "../advisory-lock-runner.js"
28
33
  import BelongsToInstanceRelationship from "./instance-relationships/belongs-to.js"
29
34
  import BelongsToRelationship from "./relationships/belongs-to.js"
@@ -73,6 +78,39 @@ const declaredBooleanTruthyValues = new Set([1, true, "1"])
73
78
  /** Stored values that a declared `"boolean"` cast reads back as `false`. */
74
79
  const declaredBooleanFalsyValues = new Set([0, false, "0"])
75
80
 
81
+ /** Static record metadata fields isolated per physical database/schema generation. */
82
+ const recordMetadataPropertyNames = new Set([
83
+ "_attributeNameToColumnName",
84
+ "_columnNameToAttributeName",
85
+ "_columnNames",
86
+ "_columns",
87
+ "_columnsAsHash",
88
+ "_columnTypeByName",
89
+ "_databaseType",
90
+ "_initialized",
91
+ "_initializeRecordPromise",
92
+ "_table"
93
+ ])
94
+
95
+ /** @type {WeakMap<typeof import("./index.js").default, Map<string, Map<string, RecordMetadataValue>>>} */
96
+ const recordMetadataValuesByModel = new WeakMap()
97
+
98
+ /**
99
+ * Returns the generation-keyed metadata store owned by one canonical model.
100
+ * @param {typeof import("./index.js").default} modelClass - Canonical model class.
101
+ * @returns {Map<string, Map<string, RecordMetadataValue>>} - Metadata store.
102
+ */
103
+ function recordMetadataValuesFor(modelClass) {
104
+ let values = recordMetadataValuesByModel.get(modelClass)
105
+
106
+ if (!values) {
107
+ values = new Map()
108
+ recordMetadataValuesByModel.set(modelClass, values)
109
+ }
110
+
111
+ return values
112
+ }
113
+
76
114
  class ValidationError extends Error {
77
115
  /**
78
116
  * Narrows the runtime value to the documented type.
@@ -236,6 +274,15 @@ class VelociousDatabaseRecord {
236
274
  * @type {Promise<void> | null | undefined} */
237
275
  static _initializeRecordPromise
238
276
 
277
+ /** @type {typeof VelociousDatabaseRecord | undefined} Canonical model class exposed only by an operation-bound metadata proxy. */
278
+ static _recordMetadataModelClass
279
+
280
+ /** @type {((modelClass: typeof VelociousDatabaseRecord) => typeof VelociousDatabaseRecord) | undefined} Binds related generated model classes to the same operation metadata generation. */
281
+ static _recordMetadataBinder
282
+
283
+ /** @type {import("../operation.js").default | undefined} Operation exposed only by a constructing metadata proxy. */
284
+ static _recordMetadataOperation
285
+
239
286
  /**
240
287
  * Narrows the runtime value to the documented type.
241
288
  * @type {boolean | undefined} */
@@ -360,10 +407,32 @@ class VelociousDatabaseRecord {
360
407
  return defineModelScope({
361
408
  callback,
362
409
  modelClass: this,
363
- startQuery: () => this._newQuery()
410
+ startQuery: (modelClass = this) => {
411
+ // This backend scope factory can only be invoked through a DatabaseRecord class.
412
+ const BackendModelClass = /** @type {typeof VelociousDatabaseRecord} */ (modelClass)
413
+
414
+ return BackendModelClass._newQuery()
415
+ }
364
416
  })
365
417
  }
366
418
 
419
+ /**
420
+ * Returns the application model class behind an operation-bound metadata view.
421
+ * @returns {typeof VelociousDatabaseRecord} - Canonical model class.
422
+ */
423
+ static canonicalRecordMetadataModelClass() {
424
+ return this._recordMetadataModelClass || this
425
+ }
426
+
427
+ /**
428
+ * Binds a relationship target to this model class's metadata generation.
429
+ * @param {typeof VelociousDatabaseRecord} modelClass - Relationship target.
430
+ * @returns {typeof VelociousDatabaseRecord} - Generation-bound target, or the unchanged target for legacy queries.
431
+ */
432
+ static bindRecordMetadataModelClass(modelClass) {
433
+ return this._recordMetadataBinder ? this._recordMetadataBinder(modelClass) : modelClass
434
+ }
435
+
367
436
  static getColumnNameToAttributeNameMap() {
368
437
  if (!this._columnNameToAttributeName) {
369
438
  /**
@@ -1470,6 +1539,68 @@ class VelociousDatabaseRecord {
1470
1539
  this._columnTypeByName = undefined
1471
1540
  this._attributeNameToColumnName = undefined
1472
1541
  this._columnNameToAttributeName = undefined
1542
+
1543
+ if (!this._recordMetadataModelClass) this.clearRecordMetadataValues()
1544
+ }
1545
+
1546
+ /**
1547
+ * Static fields that belong to one physical database/schema generation.
1548
+ * @returns {Set<string>} - Metadata property names.
1549
+ */
1550
+ static recordMetadataPropertyNames() {
1551
+ return recordMetadataPropertyNames
1552
+ }
1553
+
1554
+ /**
1555
+ * Reads one operation-bound metadata field.
1556
+ * @param {string} metadataKey - Physical database and schema generation key.
1557
+ * @param {string} property - Static metadata property.
1558
+ * @returns {RecordMetadataValue} - Stored metadata value.
1559
+ */
1560
+ static recordMetadataValue(metadataKey, property) {
1561
+ return recordMetadataValuesFor(this).get(metadataKey)?.get(property)
1562
+ }
1563
+
1564
+ /**
1565
+ * Writes one operation-bound metadata field.
1566
+ * @param {string} metadataKey - Physical database and schema generation key.
1567
+ * @param {string} property - Static metadata property.
1568
+ * @param {RecordMetadataValue} value - Metadata value.
1569
+ * @returns {void}
1570
+ */
1571
+ static setRecordMetadataValue(metadataKey, property, value) {
1572
+ let values = recordMetadataValuesFor(this).get(metadataKey)
1573
+
1574
+ if (!values) {
1575
+ values = new Map()
1576
+ recordMetadataValuesFor(this).set(metadataKey, values)
1577
+ }
1578
+
1579
+ values.set(property, value)
1580
+ }
1581
+
1582
+ /** Clears every tenant/generation metadata snapshot for this model. */
1583
+ static clearRecordMetadataValues() {
1584
+ recordMetadataValuesByModel.delete(this)
1585
+ }
1586
+
1587
+ /**
1588
+ * Clears snapshots whose key belongs to one physical database identity.
1589
+ * @param {string} databaseIdentity - Logical identifier plus pool reuse key.
1590
+ * @returns {void}
1591
+ */
1592
+ static clearRecordMetadataValuesForDatabaseIdentity(databaseIdentity) {
1593
+ const values = recordMetadataValuesByModel.get(this)
1594
+
1595
+ if (!values) return
1596
+
1597
+ const metadataPrefix = `${databaseIdentity.length}:${databaseIdentity}:`
1598
+
1599
+ for (const metadataKey of values.keys()) {
1600
+ if (metadataKey.startsWith(metadataPrefix)) values.delete(metadataKey)
1601
+ }
1602
+
1603
+ if (values.size === 0) recordMetadataValuesByModel.delete(this)
1473
1604
  }
1474
1605
 
1475
1606
  /**
@@ -1484,8 +1615,10 @@ class VelociousDatabaseRecord {
1484
1615
  if (!configuration) throw new Error(`No configuration given for ${this.name}`)
1485
1616
 
1486
1617
  this.resetRecordMetadata()
1487
- this._configuration = configuration
1488
- this._configuration.registerModelClass(this)
1618
+ const modelClass = this._recordMetadataModelClass || this
1619
+
1620
+ modelClass._configuration = configuration
1621
+ configuration.registerModelClass(modelClass)
1489
1622
  }
1490
1623
 
1491
1624
  /**
@@ -1637,7 +1770,10 @@ class VelociousDatabaseRecord {
1637
1770
 
1638
1771
  if (!locales) throw new Error("Locales hasn't been set in the configuration")
1639
1772
 
1640
- await this.getTranslationClass().initializeRecord({
1773
+ const TranslationClass = this.getTranslationClass()
1774
+ const BoundTranslationClass = this._recordMetadataBinder ? this._recordMetadataBinder(TranslationClass) : TranslationClass
1775
+
1776
+ await BoundTranslationClass.initializeRecord({
1641
1777
  configuration: this._getConfiguration(),
1642
1778
  connection
1643
1779
  })
@@ -1825,6 +1961,8 @@ class VelociousDatabaseRecord {
1825
1961
  getModelClass() {
1826
1962
  const modelClass = /** @type {typeof VelociousDatabaseRecord} */ (this.constructor)
1827
1963
 
1964
+ if (this._databaseOperation) return this._databaseOperation.modelClass(modelClass)
1965
+
1828
1966
  return modelClass
1829
1967
  }
1830
1968
 
@@ -3092,8 +3230,10 @@ class VelociousDatabaseRecord {
3092
3230
  * @returns {ModelClassQuery<MC>} - The new query.
3093
3231
  */
3094
3232
  static _newQuery(args = {}) {
3095
- const {driver = () => this.connection(), operation, ...restArgs} = args
3233
+ const {driver: givenDriver, operation: givenOperation, ...restArgs} = args
3096
3234
  restArgsError(restArgs)
3235
+ const operation = givenOperation || this._recordMetadataOperation
3236
+ const driver = givenDriver || (operation ? operation.connection() : () => this.connection())
3097
3237
  this._assertHasBeenInitialized()
3098
3238
  const handler = new Handler()
3099
3239
  const query = new ModelClassQuery({
@@ -3450,6 +3590,9 @@ class VelociousDatabaseRecord {
3450
3590
  * @param {WriteAttributes} changes - Changes.
3451
3591
  */
3452
3592
  constructor(changes = /** @type {WriteAttributes} */ ({})) {
3593
+ const ModelClass = /** @type {typeof VelociousDatabaseRecord} */ (new.target)
3594
+
3595
+ this._databaseOperation = ModelClass._recordMetadataOperation
3453
3596
  this.getModelClass()._assertHasBeenInitialized()
3454
3597
  this._attributes = {}
3455
3598
  this._changes = {}
@@ -62,6 +62,19 @@ export default class VelociousDatabaseRecordBaseInstanceRelationship {
62
62
  throw new Error("'build' not implemented")
63
63
  }
64
64
 
65
+ /**
66
+ * Resolves a relationship target through the source record's operation and
67
+ * metadata generation before construction.
68
+ * @returns {TMC | undefined} - Bound target model class.
69
+ */
70
+ getBoundTargetModelClass() {
71
+ const targetModelClass = this.getTargetModelClass()
72
+
73
+ if (!targetModelClass) return undefined
74
+
75
+ return /** @type {TMC} */ (this.getModel().getModelClass().bindRecordMetadataModelClass(targetModelClass))
76
+ }
77
+
65
78
  /**
66
79
  * Runs get auto save.
67
80
  * @returns {boolean | undefined} Whether the relationship should be auto-saved before saving the parent model
@@ -23,7 +23,7 @@ export default class VelociousDatabaseRecordBelongsToInstanceRelationship extend
23
23
  * @returns {InstanceType<TMC>} - The build.
24
24
  */
25
25
  build(data) {
26
- const TargetModelClass = /** @type {TMC} */ (this.getTargetModelClass())
26
+ const TargetModelClass = this.getBoundTargetModelClass()
27
27
 
28
28
  if (!TargetModelClass) throw new Error("Can't build a new record without a target model")
29
29
 
@@ -25,7 +25,7 @@ export default class VelociousDatabaseRecordHasManyInstanceRelationship extends
25
25
  */
26
26
  build(data) {
27
27
  // Spawn new model of the targeted class
28
- const targetModelClass = this.getTargetModelClass()
28
+ const targetModelClass = this.getBoundTargetModelClass()
29
29
 
30
30
  if (!targetModelClass) throw new Error("Can't build a new record without a taget model class")
31
31
 
@@ -58,7 +58,10 @@ export default class VelociousDatabaseRecordHasManyInstanceRelationship extends
58
58
  const parentModel = this.getModel()
59
59
 
60
60
  if (parentModel.isPersisted()) {
61
- const foreignKeyName = this.getForeignKey()
61
+ const foreignKeyName = this.getRelationship().getForeignKeyForModelClasses({
62
+ modelClass: parentModel.getModelClass(),
63
+ targetModelClass
64
+ })
62
65
  const columnNameMap = targetModelClass.getColumnNameToAttributeNameMap()
63
66
  const foreignKeyAttributeName = columnNameMap[foreignKeyName]
64
67
  if (!foreignKeyAttributeName) throw new Error(`Unknown foreign key attribute name for ${foreignKeyName}`)
@@ -28,7 +28,7 @@ export default class VelociousDatabaseRecordHasOneInstanceRelationship extends B
28
28
  * @returns {InstanceType<TMC>} - The build.
29
29
  */
30
30
  build(data) {
31
- const TargetModelClass = /** @type {TMC} */ (this.getTargetModelClass())
31
+ const TargetModelClass = this.getBoundTargetModelClass()
32
32
 
33
33
  if (!TargetModelClass) throw new Error("Can't build a new record without a target model class")
34
34
 
@@ -117,6 +117,23 @@ export default class VelociousDatabaseRecordBaseRelationship {
117
117
  throw new Error("getForeignKey not implemented")
118
118
  }
119
119
 
120
+ /**
121
+ * Resolves an attribute-form foreign key through the model class that owns the physical column.
122
+ * @param {object} args - Bound relationship model classes.
123
+ * @param {typeof import("../index.js").default} args.modelClass - Relationship source model class.
124
+ * @param {typeof import("../index.js").default} args.targetModelClass - Relationship target model class.
125
+ * @returns {string} - Physical foreign-key column name.
126
+ */
127
+ getForeignKeyForModelClasses({modelClass, targetModelClass}) {
128
+ this.getForeignKey()
129
+
130
+ if (!this.foreignKey) throw new Error(`Relationship ${this.modelClass.name}#${this.relationshipName} did not resolve a foreign key`)
131
+
132
+ const foreignKeyModelClass = this.getType() === "belongsTo" ? modelClass : targetModelClass
133
+
134
+ return foreignKeyModelClass.getAttributeNameToColumnNameMap()[this.foreignKey] || this.foreignKey
135
+ }
136
+
120
137
  /**
121
138
  * Runs get inverse of.
122
139
  * @abstract
@@ -8,41 +8,94 @@ import Migrator from "./migrator.js"
8
8
  import restArgsError from "../utils/rest-args-error.js"
9
9
 
10
10
  /**
11
- * Runs load migrations.
12
- * @param {object} args - Options object.
13
- * @param {() => Promise<import("./migrator/types.js").RequireMigrationContextType>} args.migrationsRequireContextCallback - Migrations require context callback.
14
- * @returns {{loaded: boolean}} - Whether the database was loaded by this call.
11
+ * Initializes either the legacy ambient frontend database or one immutable
12
+ * tenant-bound physical database generation.
13
+ * @param {object} args - Initialization options.
14
+ * @param {import("../configuration.js").default} args.configuration - Current frontend configuration.
15
+ * @param {string} [args.databaseIdentifier] - Tenant-only logical database identifier.
16
+ * @param {() => Promise<import("./migrator/types.js").RequireMigrationContextType>} args.migrationsRequireContextCallback - Migrations loader.
17
+ * @param {string} [args.schemaGeneration] - Tenant schema generation.
18
+ * @param {import("../tenants/tenant-handle.js").default} [args.tenantHandle] - Immutable tenant handle.
19
+ * @returns {Promise<void>} - Resolves when the selected database is ready.
15
20
  */
16
- export default function loadMigrations({migrationsRequireContextCallback, ...restArgs}) {
17
- const instance = React.useMemo(() => ({running: false}), [])
21
+ export async function initializeFrontendDatabase({configuration, databaseIdentifier, migrationsRequireContextCallback, schemaGeneration, tenantHandle}) {
22
+ if (tenantHandle) {
23
+ tenantHandle.assertConfiguration(configuration)
24
+ if (!databaseIdentifier) throw new Error("Tenant frontend database initialization requires databaseIdentifier")
25
+ if (!schemaGeneration) throw new Error("Tenant frontend database initialization requires schemaGeneration")
26
+
27
+ await tenantHandle.initialize({
28
+ databaseIdentifier,
29
+ migrations: await migrationsRequireContextCallback(),
30
+ schemaGeneration
31
+ })
32
+ await configuration.initialize()
33
+ return
34
+ }
35
+ if (databaseIdentifier || schemaGeneration) {
36
+ throw new Error("Tenant frontend database initialization requires tenantHandle")
37
+ }
38
+
39
+ await configuration.ensureConnections({name: "React database migration loader"}, async () => {
40
+ const migrator = new Migrator({configuration})
41
+
42
+ await migrator.prepare()
43
+ await migrator.migrateFilesFromRequireContext(await migrationsRequireContextCallback())
44
+ })
45
+
46
+ await configuration.initialize()
47
+ }
48
+
49
+ /**
50
+ * React lifecycle hook for frontend database readiness. With `tenantHandle`,
51
+ * readiness follows that immutable physical tenant plus `schemaGeneration`;
52
+ * changing either cancels the stale render result without cancelling shared
53
+ * lifecycle work needed by another caller.
54
+ * @param {object} args - Hook options.
55
+ * @param {string} [args.databaseIdentifier] - Tenant-only logical database identifier.
56
+ * @param {() => Promise<import("./migrator/types.js").RequireMigrationContextType>} args.migrationsRequireContextCallback - Migrations loader.
57
+ * @param {string} [args.schemaGeneration] - Tenant schema generation.
58
+ * @param {import("../tenants/tenant-handle.js").default} [args.tenantHandle] - Immutable tenant handle.
59
+ * @returns {{error: Error | null, loaded: boolean}} - Selected database readiness.
60
+ */
61
+ export default function useDatabase({databaseIdentifier, migrationsRequireContextCallback, schemaGeneration, tenantHandle, ...restArgs}) {
62
+ restArgsError(restArgs)
63
+
18
64
  const {isServer} = useEnvSense()
19
- const [loaded, setLoaded] = React.useState(false)
65
+ const migrationsRequireContextCallbackRef = React.useRef(migrationsRequireContextCallback)
20
66
 
21
- const loadDatabase = React.useCallback(async () => {
22
- instance.running = true
67
+ migrationsRequireContextCallbackRef.current = migrationsRequireContextCallback
23
68
 
24
- try {
25
- await Configuration.current().ensureConnections({name: "React database migration loader"}, async () => {
26
- const migrator = new Migrator({configuration: Configuration.current()})
69
+ const selection = React.useMemo(() => {
70
+ if (isServer) return undefined
27
71
 
28
- await migrator.prepare()
29
- await migrator.migrateFilesFromRequireContext(await migrationsRequireContextCallback())
30
- })
72
+ return {configuration: Configuration.current(), databaseIdentifier, schemaGeneration, tenantHandle}
73
+ }, [isServer, databaseIdentifier, schemaGeneration, tenantHandle])
74
+ const [state, setState] = React.useState({error: /** @type {Error | null} */ (null), loaded: false, selection})
31
75
 
32
- await Configuration.current().initialize()
33
- setLoaded(true)
34
- } finally {
35
- instance.running = false
36
- }
37
- }, [])
76
+ React.useEffect(() => {
77
+ if (!selection) return undefined
38
78
 
39
- React.useMemo(() => {
40
- if (!loaded && !isServer && !instance.running) {
41
- loadDatabase()
42
- }
43
- }, [loaded])
79
+ let current = true
80
+ const migrationsLoader = migrationsRequireContextCallbackRef.current
44
81
 
45
- restArgsError(restArgs)
82
+ setState({error: null, loaded: false, selection})
83
+ void initializeFrontendDatabase({
84
+ configuration: selection.configuration,
85
+ databaseIdentifier: selection.databaseIdentifier,
86
+ migrationsRequireContextCallback: migrationsLoader,
87
+ schemaGeneration: selection.schemaGeneration,
88
+ tenantHandle: selection.tenantHandle
89
+ }).then(() => {
90
+ if (current) setState({error: null, loaded: true, selection})
91
+ }, (error) => {
92
+ if (current) setState({error: error instanceof Error ? error : new Error(String(error)), loaded: false, selection})
93
+ })
94
+
95
+ return () => { current = false }
96
+ }, [selection])
97
+
98
+ if (state.selection !== selection) return {error: null, loaded: false}
46
99
 
47
- return {loaded}
100
+ return {error: state.error, loaded: state.loaded}
48
101
  }
@@ -27,6 +27,24 @@ export default class VelociousEnvironmentHandlerBase {
27
27
  */
28
28
  async createTestSharedTransactionConnection(_args) { return undefined }
29
29
 
30
+ /**
31
+ * Gets the active shared-transaction coordinator owner for a connection.
32
+ * Browser runtimes never install the Node test broker.
33
+ * @param {object} _connection - Parent physical connection.
34
+ * @returns {symbol | undefined} - Active coordinator owner.
35
+ */
36
+ getSharedTransactionCoordinatorOwner(_connection) { return undefined }
37
+
38
+ /**
39
+ * Runs work as the current shared-transaction coordinator owner.
40
+ * @template T
41
+ * @param {object} _connection - Parent physical connection.
42
+ * @param {symbol} _owner - Coordinator owner.
43
+ * @param {() => T} callback - Owned work.
44
+ * @returns {T} - Callback result.
45
+ */
46
+ runWithSharedTransactionCoordinatorOwner(_connection, _owner, callback) { return callback() }
47
+
30
48
  /**
31
49
  * Mutable ambient tenant used by runtimes without async-context storage.
32
50
  * @type {ReturnType<typeof JSON.parse> | undefined}
@@ -80,6 +80,38 @@ export default class VelociousEnvironmentHandlerNode extends Base{
80
80
  * @type {import("node:async_hooks").AsyncLocalStorage<TimezoneStore> | undefined} */
81
81
  _timezoneAsyncLocalStorage = NodeAsyncLocalStorage ? new NodeAsyncLocalStorage() : undefined
82
82
 
83
+ /**
84
+ * Shared-transaction coordinator ownership by physical connection.
85
+ * @type {import("node:async_hooks").AsyncLocalStorage<Map<object, symbol>> | undefined}
86
+ */
87
+ _sharedTransactionCoordinatorAsyncLocalStorage = NodeAsyncLocalStorage ? new NodeAsyncLocalStorage() : undefined
88
+
89
+ /**
90
+ * Gets the active shared-transaction coordinator owner for a connection.
91
+ * @param {object} connection - Parent physical connection.
92
+ * @returns {symbol | undefined} - Active coordinator owner.
93
+ */
94
+ getSharedTransactionCoordinatorOwner(connection) {
95
+ return this._sharedTransactionCoordinatorAsyncLocalStorage?.getStore()?.get(connection)
96
+ }
97
+
98
+ /**
99
+ * Runs work as the current shared-transaction coordinator owner.
100
+ * @template T
101
+ * @param {object} connection - Parent physical connection.
102
+ * @param {symbol} owner - Coordinator owner.
103
+ * @param {() => T} callback - Owned work.
104
+ * @returns {T} - Callback result.
105
+ */
106
+ runWithSharedTransactionCoordinatorOwner(connection, owner, callback) {
107
+ if (!this._sharedTransactionCoordinatorAsyncLocalStorage) return callback()
108
+
109
+ const owners = new Map(this._sharedTransactionCoordinatorAsyncLocalStorage.getStore())
110
+
111
+ owners.set(connection, owner)
112
+ return this._sharedTransactionCoordinatorAsyncLocalStorage.run(owners, callback)
113
+ }
114
+
83
115
  /**
84
116
  * Find commands result.
85
117
  * @type {import("./base.js").CommandFileObjectType[] | undefined} */
@@ -154,7 +154,7 @@ export type DatabaseConfigurationType = {
154
154
  */
155
155
  migrations?: boolean;
156
156
  /**
157
- * - (MySQL) Opt in to multi-statement queries so a whole structure SQL dump loads in one round-trip via `StructureSqlLoader`. Off by default; ordinary queries otherwise reject stacked statements.
157
+ * - (MySQL) Opt in to multi-statement queries so structure SQL loads and all-table cleanup can batch into one round-trip. Off by default; ordinary queries otherwise reject stacked statements.
158
158
  */
159
159
  multipleStatements?: boolean;
160
160
  /**
@@ -1660,7 +1660,7 @@ export type ConfigurationArgsType = {
1660
1660
  * @property {() => ReturnType<typeof JSON.parse>} [getConnection] - Custom connection factory override.
1661
1661
  * @property {string} [host] - Database host.
1662
1662
  * @property {boolean} [migrations] - Whether migrations are enabled for this database.
1663
- * @property {boolean} [multipleStatements] - (MySQL) Opt in to multi-statement queries so a whole structure SQL dump loads in one round-trip via `StructureSqlLoader`. Off by default; ordinary queries otherwise reject stacked statements.
1663
+ * @property {boolean} [multipleStatements] - (MySQL) Opt in to multi-statement queries so structure SQL loads and all-table cleanup can batch into one round-trip. Off by default; ordinary queries otherwise reject stacked statements.
1664
1664
  * @property {number} [maxRowsPerInsert] - Maximum rows per `INSERT ... VALUES (...), (...), ...` statement generated by `Record.insertMultiple`. Defaults to 500.
1665
1665
  * @property {number} [maxInsertSqlBytes] - Maximum serialized SQL size, in bytes, for a single `INSERT ... VALUES (...), (...), ...` statement. Defaults to 1 MiB (1048576).
1666
1666
  * @property {number} [maxInClauseValues] - Maximum values in a single `IN (...)` cohort used by preloads, association counts, and queryData aggregates. Defaults to 999.