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
@@ -145,18 +145,75 @@ export default class VelociousDatabaseMigrator {
145
145
  * @returns {Promise<void>} - Resolves when complete.
146
146
  */
147
147
  async migrateFilesFromRequireContext(requireContext) {
148
- /**
149
- * Files.
150
- * @type {import("./migrator/types.js").MigrationObjectType[]} */
151
- let files = []
148
+ const files = this.migrationsFromRequireContext(requireContext)
149
+
150
+ await this.configuration.ensureConnections({databaseIdentifiers: this.databaseIdentifiers, name: "Database migrator: migrate require-context files"}, async () => {
151
+ for (const migration of files) {
152
+ await this.runMigrationFile({
153
+ migration,
154
+ requireMigration: async () => requireContext(migration.file).default
155
+ })
156
+ }
157
+
158
+ await this._afterMigrations()
159
+ })
160
+ }
161
+
162
+ /**
163
+ * Migrates exactly one already-captured physical database. This is the
164
+ * frontend/tenant counterpart to the ambient multi-database entrypoints:
165
+ * callers own the captured connection and no configuration fallback is read.
166
+ * @param {object} args - Captured migration arguments.
167
+ * @param {import("../configuration-types.js").DatabaseConfigurationType} args.databaseConfiguration - Captured physical database configuration.
168
+ * @param {string} args.databaseIdentifier - Logical database identifier.
169
+ * @param {import("./drivers/base.js").default} args.db - Captured physical connection.
170
+ * @param {import("./migrator/types.js").RequireMigrationContextType} args.requireContext - Frontend migration require context.
171
+ * @returns {Promise<number>} - Number of newly applied migrations.
172
+ */
173
+ async migrateRequireContextForDatabase({databaseConfiguration, databaseIdentifier, db, requireContext}) {
174
+ if (!databaseConfiguration.migrations) return 0
175
+
176
+ await MigrationsLedger.ensureTable(db)
177
+ const appliedVersions = new Set(await MigrationsLedger.appliedVersions(db))
178
+ const migrations = this.migrationsFromRequireContext(requireContext)
179
+ let appliedCount = 0
180
+
181
+ for (const migration of migrations) {
182
+ const version = `${migration.date}`
183
+
184
+ if (appliedVersions.has(version)) continue
185
+
186
+ const MigrationClass = requireContext(migration.file).default
187
+
188
+ if (!MigrationClass || typeof MigrationClass !== "function") {
189
+ throw new Error(`Migration ${migration.file} must export a default migration class. Type: ${typeof MigrationClass}`)
190
+ }
191
+ if (!(MigrationClass.getDatabaseIdentifiers() || ["default"]).includes(databaseIdentifier)) continue
192
+
193
+ const migrationInstance = new MigrationClass({configuration: this.configuration, databaseIdentifier, db})
194
+
195
+ await this.runMigrationUp({migration, migrationInstance})
196
+ await MigrationsLedger.recordVersion(db, version)
197
+ appliedVersions.add(version)
198
+ appliedCount++
199
+ }
200
+
201
+ return appliedCount
202
+ }
203
+
204
+ /**
205
+ * Parses and orders migrations from a browser/native require context.
206
+ * @param {import("./migrator/types.js").RequireMigrationContextType} requireContext - Migration require context.
207
+ * @returns {import("./migrator/types.js").MigrationObjectType[]} - Ordered migrations.
208
+ */
209
+ migrationsFromRequireContext(requireContext) {
210
+ const migrations = []
152
211
 
153
212
  for (const file of requireContext.keys()) {
154
- // "13,14" because somes "require-context"-npm-module deletes first character!?
155
213
  const match = file.match(/(\d{13,14})-(.+)\.js$/)
156
214
 
157
215
  if (!match) continue
158
216
 
159
- // Fix require-context-npm-module deletes first character
160
217
  let fileName = file
161
218
  let dateNumber = match[1]
162
219
 
@@ -165,30 +222,39 @@ export default class VelociousDatabaseMigrator {
165
222
  fileName = `2${fileName}`
166
223
  }
167
224
 
168
- // Parse regex
169
- const date = parseInt(dateNumber)
170
- const migrationName = match[2]
171
- const migrationClassName = inflection.camelize(migrationName.replaceAll("-", "_"))
172
-
173
- files.push({
225
+ migrations.push({
226
+ date: parseInt(dateNumber),
174
227
  file: fileName,
175
- date,
176
- migrationClassName
228
+ migrationClassName: inflection.camelize(match[2].replaceAll("-", "_"))
177
229
  })
178
230
  }
179
231
 
180
- files = files.sort((migration1, migration2) => migration1.date - migration2.date)
232
+ return migrations.sort((migration1, migration2) => migration1.date - migration2.date)
233
+ }
181
234
 
182
- await this.configuration.ensureConnections({databaseIdentifiers: this.databaseIdentifiers, name: "Database migrator: migrate require-context files"}, async () => {
183
- for (const migration of files) {
184
- await this.runMigrationFile({
185
- migration,
186
- requireMigration: async () => requireContext(migration.file).default
187
- })
188
- }
235
+ /**
236
+ * Runs one migration's upward implementation.
237
+ * @param {object} args - Migration arguments.
238
+ * @param {import("./migrator/types.js").MigrationObjectType} args.migration - Migration descriptor.
239
+ * @param {import("./migration/index.js").default} args.migrationInstance - Migration instance.
240
+ * @returns {Promise<void>} - Resolves after the migration succeeds.
241
+ */
242
+ async runMigrationUp({migration, migrationInstance}) {
243
+ try {
244
+ await migrationInstance.change()
245
+ } catch (changeError) {
246
+ if (!(changeError instanceof NotImplementedError)) throw changeError
247
+
248
+ try {
249
+ await migrationInstance.up()
250
+ } catch (upError) {
251
+ if (upError instanceof NotImplementedError) {
252
+ throw new Error(`'change' or 'up' didn't exist on migration: ${migration.file}`, {cause: upError})
253
+ }
189
254
 
190
- await this._afterMigrations()
191
- })
255
+ throw upError
256
+ }
257
+ }
192
258
  }
193
259
 
194
260
  /**
@@ -17,9 +17,10 @@ export default class VelociousDatabaseOperation {
17
17
  * @param {import("./drivers/base.js").default} args.connection - Pinned physical connection.
18
18
  * @param {string} args.databaseIdentifier - Singular database identifier.
19
19
  * @param {symbol} args.owner - Opaque pool lease owner.
20
+ * @param {string} [args.schemaGeneration] - Tenant schema generation owning record metadata.
20
21
  * @param {object | undefined} args.tenant - Tenant descriptor captured by the owning handle.
21
22
  */
22
- constructor({configuration, databaseConfiguration, configurationReuseKey, connection, databaseIdentifier, enforceCurrentTenantReuseKey = true, owner, tenant}) {
23
+ constructor({configuration, databaseConfiguration, configurationReuseKey, connection, databaseIdentifier, enforceCurrentTenantReuseKey = true, owner, schemaGeneration, tenant}) {
23
24
  this._active = true
24
25
  this._configuration = configuration
25
26
  this._databaseConfiguration = databaseConfiguration || configuration.resolveDatabaseConfiguration(databaseIdentifier, tenant)
@@ -27,7 +28,10 @@ export default class VelociousDatabaseOperation {
27
28
  this._databaseIdentifier = databaseIdentifier
28
29
  this._enforceCurrentTenantReuseKey = enforceCurrentTenantReuseKey
29
30
  this._physicalConnection = connection
31
+ this._schemaGeneration = schemaGeneration
30
32
  this._tenant = tenant
33
+ /** @type {WeakMap<typeof import("./record/index.js").default, typeof import("./record/index.js").default>} */
34
+ this._boundModelClasses = new WeakMap()
31
35
  this._connection = new OperationConnection({
32
36
  connection,
33
37
  operation: this,
@@ -45,12 +49,57 @@ export default class VelociousDatabaseOperation {
45
49
  this.assertActive()
46
50
  this.assertModel(ModelClass)
47
51
 
48
- return ModelClass._newQuery({
52
+ return this.modelClass(ModelClass)._newQuery({
49
53
  driver: this.connection(),
50
54
  operation: this
51
55
  })
52
56
  }
53
57
 
58
+ /**
59
+ * Returns a model-class view whose schema metadata is bound to this physical
60
+ * database generation. Construction still produces the application's original
61
+ * model class, so lifecycle callbacks and model registries retain class identity.
62
+ * @template {typeof import("./record/index.js").default} MC
63
+ * @param {MC} ModelClass - Canonical model class.
64
+ * @returns {MC} - Operation-bound model class.
65
+ */
66
+ modelClass(ModelClass) {
67
+ if (!this._schemaGeneration) return ModelClass
68
+
69
+ const canonicalModelClass = /** @type {MC} */ (ModelClass._recordMetadataModelClass || ModelClass)
70
+
71
+ const existing = this._boundModelClasses.get(canonicalModelClass)
72
+
73
+ if (existing) return /** @type {MC} */ (existing)
74
+
75
+ const databaseIdentity = this.databaseIdentity()
76
+ const metadataKey = `${databaseIdentity.length}:${databaseIdentity}:${this._schemaGeneration}`
77
+ const metadataProperties = canonicalModelClass.recordMetadataPropertyNames()
78
+ const boundModelClass = new Proxy(canonicalModelClass, {
79
+ construct: (target, args, newTarget) => Reflect.construct(target, args, newTarget),
80
+ get: (target, property, receiver) => {
81
+ if (property === "_recordMetadataModelClass") return target
82
+ if (property === "_recordMetadataBinder") return (/** @type {typeof import("./record/index.js").default} */ targetModelClass) => this.modelClass(targetModelClass)
83
+ if (property === "_recordMetadataOperation") return this
84
+ if (typeof property === "string" && metadataProperties.has(property)) return target.recordMetadataValue(metadataKey, property)
85
+
86
+ return Reflect.get(target, property, receiver)
87
+ },
88
+ set: (target, property, value, receiver) => {
89
+ if (typeof property === "string" && metadataProperties.has(property)) {
90
+ target.setRecordMetadataValue(metadataKey, property, value)
91
+ return true
92
+ }
93
+
94
+ return Reflect.set(target, property, value, receiver)
95
+ }
96
+ })
97
+
98
+ this._boundModelClasses.set(canonicalModelClass, boundModelClass)
99
+
100
+ return /** @type {MC} */ (boundModelClass)
101
+ }
102
+
54
103
  /**
55
104
  * Verifies that a model belongs to this operation's configuration and database.
56
105
  * @param {typeof import("./record/index.js").default} ModelClass - Model class to verify.
@@ -162,9 +211,11 @@ export default class VelociousDatabaseOperation {
162
211
  async ensureModelInitialized(ModelClass) {
163
212
  this.assertActive()
164
213
 
165
- if (ModelClass.isInitialized()) this.assertModel(ModelClass)
214
+ const boundModelClass = this.modelClass(ModelClass)
215
+
216
+ if (boundModelClass.isInitialized()) this.assertModel(ModelClass)
166
217
 
167
- await ModelClass.ensureInitialized({
218
+ await boundModelClass.ensureInitialized({
168
219
  configuration: this._configuration,
169
220
  connection: this.connection()
170
221
  })
@@ -54,12 +54,15 @@ export default class VelociousDatabaseQueryJoinObject extends JoinBase {
54
54
  for (const joinKey in join) {
55
55
  const joinValue = join[joinKey]
56
56
  const relationship = modelClass.getRelationshipByName(joinKey)
57
- const targetModelClass = relationship.getTargetModelClass()
57
+ const rawTargetModelClass = relationship.getTargetModelClass()
58
58
 
59
- if (!targetModelClass) {
59
+ if (!rawTargetModelClass) {
60
60
  throw new Error(`Relationship ${modelClass.name}#${joinKey} has no target model class`)
61
61
  }
62
62
 
63
+ const targetModelClass = query.bindModelClass(rawTargetModelClass)
64
+ const foreignKey = relationship.getForeignKeyForModelClasses({modelClass, targetModelClass})
65
+
63
66
  const joinPath = path.concat([joinKey])
64
67
  const parentTableRef = query.getJoinTableReference(path)
65
68
  const targetEntry = query._registerJoinPath(joinPath)
@@ -80,9 +83,9 @@ export default class VelociousDatabaseQueryJoinObject extends JoinBase {
80
83
 
81
84
  if (relationship.getType() == "belongsTo") {
82
85
  sql += `${conn.quoteTable(targetTableRef)}.${conn.quoteColumn(relationship.getPrimaryKey())} = `
83
- sql += `${conn.quoteTable(parentTableRef)}.${conn.quoteColumn(relationship.getForeignKey())}`
86
+ sql += `${conn.quoteTable(parentTableRef)}.${conn.quoteColumn(foreignKey)}`
84
87
  } else if (relationship.getType() == "hasMany" || relationship.getType() == "hasOne") {
85
- sql += `${conn.quoteTable(targetTableRef)}.${conn.quoteColumn(relationship.getForeignKey())} = `
88
+ sql += `${conn.quoteTable(targetTableRef)}.${conn.quoteColumn(foreignKey)} = `
86
89
  sql += `${conn.quoteTable(parentTableRef)}.${conn.quoteColumn(relationship.getPrimaryKey())}`
87
90
  } else {
88
91
  throw new Error(`Unknown relationship type: ${relationship.getType()}`)
@@ -87,6 +87,22 @@ function normalizeScopePath(path) {
87
87
  return [...path]
88
88
  }
89
89
 
90
+ /**
91
+ * Narrows a shared model-scope descriptor to the backend model class required by ModelClassQuery.
92
+ * @param {import("../../utils/model-scope.js").ModelScopeDescriptor} scopeDescriptor - Shared scope descriptor.
93
+ * @returns {typeof import("../record/index.js").default} - Backend scope owner.
94
+ */
95
+ function backendScopeModelClass(scopeDescriptor) {
96
+ const modelClass = scopeDescriptor.modelClass
97
+
98
+ if (!("canonicalRecordMetadataModelClass" in modelClass)) {
99
+ throw new Error("A frontend-model scope cannot be applied to a database record query")
100
+ }
101
+
102
+ // The runtime member check above narrows the shared frontend/backend descriptor boundary.
103
+ return /** @type {typeof import("../record/index.js").default} */ (modelClass)
104
+ }
105
+
90
106
  /**
91
107
  * Deep-copies a preload select map (keyed by model name with attribute arrays)
92
108
  * so a cloned query's selections can be mutated without affecting the original.
@@ -490,6 +506,15 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
490
506
  return this.modelClass
491
507
  }
492
508
 
509
+ /**
510
+ * Binds a relationship target to this query's physical database generation.
511
+ * @param {typeof import("../record/index.js").default} modelClass - Canonical relationship target.
512
+ * @returns {typeof import("../record/index.js").default} - Query-bound relationship target.
513
+ */
514
+ bindModelClass(modelClass) {
515
+ return this.getModelClass().bindRecordMetadataModelClass(modelClass)
516
+ }
517
+
493
518
  /**
494
519
  * Runs get join base path.
495
520
  * @returns {string[]} - The join base path.
@@ -563,7 +588,7 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
563
588
  throw new Error(`No target model class for ${modelClass.name}#${relationshipName}`)
564
589
  }
565
590
 
566
- modelClass = targetModelClass
591
+ modelClass = this.bindModelClass(targetModelClass)
567
592
  }
568
593
 
569
594
  return modelClass
@@ -642,7 +667,9 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
642
667
  throw new Error("scope() expects a descriptor returned by defineScope(...).scope(...)")
643
668
  }
644
669
 
645
- if (scopeDescriptor.modelClass !== this.getModelClass()) {
670
+ const scopeModelClass = backendScopeModelClass(scopeDescriptor)
671
+
672
+ if (scopeModelClass.canonicalRecordMetadataModelClass() !== this.getModelClass().canonicalRecordMetadataModelClass()) {
646
673
  throw new Error(`Cannot apply ${scopeDescriptor.modelClass.name} scope to ${this.getModelClass().name} query`)
647
674
  }
648
675
 
@@ -671,7 +698,9 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
671
698
  const fullJoinPath = this.getJoinBasePath().concat(joinPath)
672
699
  const targetModelClass = this._resolveModelClassForJoinPath(fullJoinPath)
673
700
 
674
- if (scopeDescriptor.modelClass !== targetModelClass) {
701
+ const scopeModelClass = backendScopeModelClass(scopeDescriptor)
702
+
703
+ if (scopeModelClass.canonicalRecordMetadataModelClass() !== targetModelClass.canonicalRecordMetadataModelClass()) {
675
704
  throw new Error(`Cannot apply ${scopeDescriptor.modelClass.name} scope to join path ${fullJoinPath.join(".")} (${targetModelClass.name})`)
676
705
  }
677
706
 
@@ -1418,11 +1447,12 @@ function splitWhereHash({hash, modelClass}) {
1418
1447
 
1419
1448
  if (isNested) {
1420
1449
  if (relationship) {
1421
- const targetModelClass = relationship.getTargetModelClass()
1422
- if (!targetModelClass) {
1450
+ const rawTargetModelClass = relationship.getTargetModelClass()
1451
+ if (!rawTargetModelClass) {
1423
1452
  fallbackHash[key] = value
1424
1453
  continue
1425
1454
  }
1455
+ const targetModelClass = modelClass.bindRecordMetadataModelClass(rawTargetModelClass)
1426
1456
  const nestedResult = splitWhereHash({hash: value, modelClass: targetModelClass})
1427
1457
  const nestedResolvedKeys = Object.keys(nestedResult.resolvedHash)
1428
1458
  const nestedFallbackKeys = Object.keys(nestedResult.fallbackHash)
@@ -1476,8 +1506,9 @@ function buildJoinObjectFromWhereHash({hash, modelClass}) {
1476
1506
  if (!relationship) continue
1477
1507
 
1478
1508
  if (isPlainObject(value)) {
1479
- const targetModelClass = relationship.getTargetModelClass()
1480
- if (!targetModelClass) continue
1509
+ const rawTargetModelClass = relationship.getTargetModelClass()
1510
+ if (!rawTargetModelClass) continue
1511
+ const targetModelClass = modelClass.bindRecordMetadataModelClass(rawTargetModelClass)
1481
1512
  const nestedJoinObject = buildJoinObjectFromWhereHash({hash: value, modelClass: targetModelClass})
1482
1513
 
1483
1514
  joinObject[key] = Object.keys(nestedJoinObject).length > 0 ? nestedJoinObject : true
@@ -2,7 +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
+ import preloadQueryForModel, { bindPreloadModelClass } from "./query-for-model.js"
6
6
  import restArgsError from "../../../utils/rest-args-error.js"
7
7
 
8
8
  export default class VelociousDatabaseQueryPreloaderBelongsTo {
@@ -22,7 +22,8 @@ export default class VelociousDatabaseQueryPreloaderBelongsTo {
22
22
  }
23
23
 
24
24
  async run() {
25
- const foreignKey = this.relationship.getForeignKey()
25
+ const sourceModelClass = this.models[0].getModelClass()
26
+ const foreignKey = this.relationship.getForeignKeyForModelClasses({modelClass: sourceModelClass, targetModelClass: sourceModelClass})
26
27
  const primaryKey = this.relationship.getPrimaryKey()
27
28
  const relationshipName = this.relationship.getRelationshipName()
28
29
 
@@ -30,9 +31,11 @@ export default class VelociousDatabaseQueryPreloaderBelongsTo {
30
31
  return await this._runPolymorphic({foreignKey, primaryKey, relationshipName})
31
32
  }
32
33
 
33
- const targetModelClass = this.relationship.getTargetModelClass()
34
+ const rawTargetModelClass = this.relationship.getTargetModelClass()
34
35
 
35
- if (!targetModelClass) throw new Error("No target model class could be gotten from relationship")
36
+ if (!rawTargetModelClass) throw new Error("No target model class could be gotten from relationship")
37
+
38
+ const targetModelClass = bindPreloadModelClass(this.models, rawTargetModelClass)
36
39
 
37
40
  /**
38
41
  * Satisfied targets.
@@ -155,7 +158,7 @@ export default class VelociousDatabaseQueryPreloaderBelongsTo {
155
158
  for (const model of this.models) {
156
159
  const targetType = /** @type {string | undefined} */ (model.readColumn(typeColumn))
157
160
  const instanceRelationship = model.getRelationshipByName(relationshipName)
158
- const targetModelClass = targetType ? configuration.getModelClass(targetType) : undefined
161
+ const targetModelClass = targetType ? bindPreloadModelClass(this.models, configuration.getModelClass(targetType)) : undefined
159
162
 
160
163
  if (targetModelClass && this.selection.isSatisfied({instanceRelationship, targetModelClass, mappingColumns: [primaryKey]})) {
161
164
  const loaded = /** @type {import("../../record/index.js").default | undefined} */ (instanceRelationship.getLoadedOrUndefined())
@@ -203,7 +206,7 @@ export default class VelociousDatabaseQueryPreloaderBelongsTo {
203
206
  const targetModels = []
204
207
 
205
208
  for (const targetType in foreignKeyValuesByType) {
206
- const targetModelClass = configuration.getModelClass(targetType)
209
+ const targetModelClass = bindPreloadModelClass(this.models, configuration.getModelClass(targetType))
207
210
 
208
211
  await ensureModelClassInitialized(targetModelClass, configuration, this.models[0])
209
212
 
@@ -2,7 +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
+ import preloadQueryForModel, { bindPreloadModelClass } from "./query-for-model.js"
6
6
  import restArgsError from "../../../utils/rest-args-error.js"
7
7
 
8
8
  /**
@@ -18,19 +18,23 @@ export function hasManyThroughTargetForeignKey(relationship, throughModelClass,
18
18
  // (e.g. a default plus an alternate), so picking the first match would otherwise be ambiguous.
19
19
  const explicitForeignKey = relationship.getExplicitForeignKey()
20
20
 
21
- if (explicitForeignKey) return explicitForeignKey
21
+ if (explicitForeignKey) {
22
+ return targetModelClass.getAttributeNameToColumnNameMap()[explicitForeignKey] || explicitForeignKey
23
+ }
22
24
 
23
25
  for (const targetRelationship of targetModelClass.getRelationships()) {
24
26
  if (targetRelationship.getType() != "belongsTo") continue
25
27
 
26
28
  const relationshipTargetModelClass = targetRelationship.getTargetModelClass()
27
29
 
28
- if (relationshipTargetModelClass === throughModelClass) {
29
- return targetRelationship.getForeignKey()
30
+ if (!relationshipTargetModelClass) continue
31
+
32
+ if (relationshipTargetModelClass.canonicalRecordMetadataModelClass() === throughModelClass.canonicalRecordMetadataModelClass()) {
33
+ return targetRelationship.getForeignKeyForModelClasses({modelClass: targetModelClass, targetModelClass: throughModelClass})
30
34
  }
31
35
  }
32
36
 
33
- return relationship.getForeignKey()
37
+ return relationship.getForeignKeyForModelClasses({modelClass: throughModelClass, targetModelClass})
34
38
  }
35
39
 
36
40
  export default class VelociousDatabaseQueryPreloaderHasMany {
@@ -107,15 +111,19 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
107
111
  }
108
112
 
109
113
  const throughRelationshipName = /** @type {string} */ (this.relationship.through)
110
- const parentModelClass = this.relationship.getModelClass()
114
+ const parentModelClass = this.models[0].getModelClass()
111
115
  const throughRelationship = parentModelClass.getRelationshipByName(throughRelationshipName)
112
- const throughModelClass = throughRelationship.getTargetModelClass()
116
+ const rawThroughModelClass = throughRelationship.getTargetModelClass()
117
+
118
+ if (!rawThroughModelClass) throw new Error(`Through relationship ${throughRelationshipName} has no target model class`)
113
119
 
114
- if (!throughModelClass) throw new Error(`Through relationship ${throughRelationshipName} has no target model class`)
120
+ const throughModelClass = bindPreloadModelClass(this.models, rawThroughModelClass)
115
121
 
116
- const targetModelClass = this.relationship.getTargetModelClass()
122
+ const rawTargetModelClass = this.relationship.getTargetModelClass()
117
123
 
118
- if (!targetModelClass) throw new Error("No target model class could be gotten from relationship")
124
+ if (!rawTargetModelClass) throw new Error("No target model class could be gotten from relationship")
125
+
126
+ const targetModelClass = bindPreloadModelClass(this.models, rawTargetModelClass)
119
127
 
120
128
  const targetForeignKey = hasManyThroughTargetForeignKey(this.relationship, throughModelClass, targetModelClass)
121
129
  const {modelsToLoad, satisfiedTargets} = this._partition(targetModelClass, [targetForeignKey])
@@ -127,7 +135,7 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
127
135
  await ensureModelClassInitialized(throughModelClass, configuration, modelsToLoad[0])
128
136
  await ensureModelClassInitialized(targetModelClass, configuration, modelsToLoad[0])
129
137
 
130
- const throughForeignKey = throughRelationship.getForeignKey()
138
+ const throughForeignKey = throughRelationship.getForeignKeyForModelClasses({modelClass: parentModelClass, targetModelClass: throughModelClass})
131
139
 
132
140
  /**
133
141
  * Models primary key values.
@@ -260,16 +268,19 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
260
268
  * @returns {Promise<import("../../record/index.js").default[]>} - Loaded target models.
261
269
  */
262
270
  async _runDirect() {
263
- const foreignKey = this.relationship.getForeignKey()
264
271
  const primaryKey = this.relationship.getPrimaryKey()
265
272
 
266
273
  if (!primaryKey) {
267
274
  throw new Error(`${this.relationship.getModelClass().name}#${this.relationship.getRelationshipName()} doesn't have a primary key`)
268
275
  }
269
276
 
270
- const targetModelClass = this.relationship.getTargetModelClass()
277
+ const rawTargetModelClass = this.relationship.getTargetModelClass()
278
+
279
+ if (!rawTargetModelClass) throw new Error("No target model class could be gotten from relationship")
271
280
 
272
- if (!targetModelClass) throw new Error("No target model class could be gotten from relationship")
281
+ const sourceModelClass = this.models[0].getModelClass()
282
+ const targetModelClass = bindPreloadModelClass(this.models, rawTargetModelClass)
283
+ const foreignKey = this.relationship.getForeignKeyForModelClasses({modelClass: sourceModelClass, targetModelClass})
273
284
 
274
285
  const {modelsToLoad, satisfiedTargets} = this._partition(targetModelClass, [foreignKey])
275
286
 
@@ -2,7 +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
+ import preloadQueryForModel, { bindPreloadModelClass } from "./query-for-model.js"
6
6
  import restArgsError from "../../../utils/rest-args-error.js"
7
7
 
8
8
  export default class VelociousDatabaseQueryPreloaderHasOne {
@@ -32,13 +32,16 @@ export default class VelociousDatabaseQueryPreloaderHasOne {
32
32
  * @type {Record<number | string, Array<import("../../record/index.js").default>>} */
33
33
  const modelsByPrimaryKeyValue = {}
34
34
 
35
- const foreignKey = this.relationship.getForeignKey()
36
35
  const primaryKey = this.relationship.getPrimaryKey()
37
36
  const relationshipName = this.relationship.getRelationshipName()
38
37
 
39
- const targetModelClass = this.relationship.getTargetModelClass()
38
+ const rawTargetModelClass = this.relationship.getTargetModelClass()
40
39
 
41
- if (!targetModelClass) throw new Error("No target model class could be gotten from relationship")
40
+ if (!rawTargetModelClass) throw new Error("No target model class could be gotten from relationship")
41
+
42
+ const sourceModelClass = this.models[0].getModelClass()
43
+ const targetModelClass = bindPreloadModelClass(this.models, rawTargetModelClass)
44
+ const foreignKey = this.relationship.getForeignKeyForModelClasses({modelClass: sourceModelClass, targetModelClass})
42
45
 
43
46
  /**
44
47
  * Preload collections.
@@ -1,5 +1,20 @@
1
1
  // @ts-check
2
2
 
3
+ /**
4
+ * Binds a preload target to the source records' physical database generation.
5
+ * @template {typeof import("../../record/index.js").default} MC
6
+ * @param {import("../../record/index.js").default[]} models - Source records.
7
+ * @param {MC} ModelClass - Target model class.
8
+ * @returns {MC} - Generation-bound target model class.
9
+ */
10
+ export function bindPreloadModelClass(models, ModelClass) {
11
+ const sourceModel = models[0]
12
+
13
+ if (!sourceModel) throw new Error("Cannot bind a preload model class without source records")
14
+
15
+ return /** @type {MC} */ (sourceModel.getModelClass().bindRecordMetadataModelClass(ModelClass))
16
+ }
17
+
3
18
  /**
4
19
  * Builds a target query preserving the explicit database operation owned by
5
20
  * the source records.
@@ -13,5 +28,5 @@ export default function preloadQueryForModel(models, ModelClass) {
13
28
 
14
29
  if (!sourceModel) throw new Error("Cannot build a preload query without source records")
15
30
 
16
- return sourceModel.queryForModel(ModelClass)
31
+ return sourceModel.queryForModel(bindPreloadModelClass(models, ModelClass))
17
32
  }
@@ -88,7 +88,7 @@ export default class VelociousDatabaseQueryPreloader {
88
88
  static async preload(models, queryOrSpec, {force = false} = {}) {
89
89
  if (models.length == 0) return
90
90
 
91
- const modelClass = /** @type {typeof import("../record/index.js").default} */ (models[0].constructor)
91
+ const modelClass = models[0].getModelClass()
92
92
  const isQuery = Boolean(queryOrSpec) && typeof queryOrSpec == "object" && "_preload" in queryOrSpec
93
93
  // Reuse the query builder's preload/select normalization for raw specs
94
94
  // instead of duplicating it here.
@@ -176,15 +176,17 @@ export default class VelociousDatabaseQueryPreloader {
176
176
 
177
177
  if (models.length == 0) continue
178
178
 
179
- const targetModelClass = configuration.getModelClass(className)
179
+ const targetModelClass = this.modelClass.bindRecordMetadataModelClass(configuration.getModelClass(className))
180
180
  const preloader = new VelociousDatabaseQueryPreloader({modelClass: targetModelClass, models, preload: normalizedPreload, selection: this.selection})
181
181
 
182
182
  await preloader.run()
183
183
  }
184
184
  } else {
185
- const targetModelClass = relationship.getTargetModelClass()
185
+ const rawTargetModelClass = relationship.getTargetModelClass()
186
186
 
187
- if (!targetModelClass) throw new Error("No target model class could be gotten from relationship")
187
+ if (!rawTargetModelClass) throw new Error("No target model class could be gotten from relationship")
188
+
189
+ const targetModelClass = this.modelClass.bindRecordMetadataModelClass(rawTargetModelClass)
188
190
 
189
191
  const preloader = new VelociousDatabaseQueryPreloader({modelClass: targetModelClass, models: targetModels, preload: normalizedPreload, selection: this.selection})
190
192
 
@@ -153,13 +153,13 @@ function resolveTargetModelClass(rootModelClass, chain) {
153
153
 
154
154
  for (const segment of chain) {
155
155
  const relationship = modelClass.getRelationshipByName(segment)
156
- const target = relationship.getTargetModelClass()
156
+ const rawTarget = relationship.getTargetModelClass()
157
157
 
158
- if (!target) {
158
+ if (!rawTarget) {
159
159
  throw new Error(`queryData: could not resolve target model for ${modelClass.name}#${segment}`)
160
160
  }
161
161
 
162
- modelClass = target
162
+ modelClass = modelClass.bindRecordMetadataModelClass(rawTarget)
163
163
  }
164
164
 
165
165
  return modelClass
@@ -403,9 +403,11 @@ export default class VelociousDatabaseQueryWhereModelClassHash extends WhereBase
403
403
  if (relationship && tuples) {
404
404
  if (index > 0) sql += " AND "
405
405
 
406
- const targetModelClass = relationship.getTargetModelClass()
406
+ const rawTargetModelClass = relationship.getTargetModelClass()
407
407
 
408
- if (!targetModelClass) throw new Error(`Relationship "${whereKey}" for ${modelClass.name} has no target model class`)
408
+ if (!rawTargetModelClass) throw new Error(`Relationship "${whereKey}" for ${modelClass.name} has no target model class`)
409
+
410
+ const targetModelClass = modelClass.bindRecordMetadataModelClass(rawTargetModelClass)
409
411
 
410
412
  const nestedPath = path.concat([whereKey])
411
413
  const nestedTableName = modelQuery.getTableReferenceForJoin(...nestedPath)
@@ -431,9 +433,11 @@ export default class VelociousDatabaseQueryWhereModelClassHash extends WhereBase
431
433
  throw new Error(`Unknown relationship "${whereKey}" for ${modelClass.name}`)
432
434
  }
433
435
 
434
- const targetModelClass = relationship.getTargetModelClass()
436
+ const rawTargetModelClass = relationship.getTargetModelClass()
437
+
438
+ if (!rawTargetModelClass) throw new Error(`Relationship "${whereKey}" for ${modelClass.name} has no target model class`)
435
439
 
436
- if (!targetModelClass) throw new Error(`Relationship "${whereKey}" for ${modelClass.name} has no target model class`)
440
+ const targetModelClass = modelClass.bindRecordMetadataModelClass(rawTargetModelClass)
437
441
 
438
442
  const nestedHash = /** @type {WhereHash} */ (whereValue)
439
443
  const nestedPath = path.concat([whereKey])