velocious 1.0.593 → 1.0.595
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.
- package/README.md +3 -2
- package/build/configuration.js +109 -14
- package/build/database/drivers/base.js +30 -0
- package/build/database/migrator.js +90 -24
- package/build/database/operation.js +55 -4
- package/build/database/query/join-object.js +7 -4
- package/build/database/query/model-class-query.js +38 -7
- package/build/database/query/preloader/belongs-to.js +9 -6
- package/build/database/query/preloader/has-many.js +25 -14
- package/build/database/query/preloader/has-one.js +7 -4
- package/build/database/query/preloader/query-for-model.js +16 -1
- package/build/database/query/preloader.js +6 -4
- package/build/database/query/query-data.js +3 -3
- package/build/database/query/where-model-class-hash.js +8 -4
- package/build/database/query/with-count.js +4 -3
- package/build/database/record/index.js +148 -5
- package/build/database/record/instance-relationships/base.js +13 -0
- package/build/database/record/instance-relationships/belongs-to.js +1 -1
- package/build/database/record/instance-relationships/has-many.js +5 -2
- package/build/database/record/instance-relationships/has-one.js +1 -1
- package/build/database/record/relationships/base.js +17 -0
- package/build/database/use-database.js +81 -28
- package/build/src/configuration.d.ts +31 -4
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +88 -17
- package/build/src/database/drivers/base.d.ts +11 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +28 -1
- package/build/src/database/migrator.d.ts +34 -0
- package/build/src/database/migrator.d.ts.map +1 -1
- package/build/src/database/migrator.js +83 -23
- package/build/src/database/operation.d.ts +15 -1
- package/build/src/database/operation.d.ts.map +1 -1
- package/build/src/database/operation.js +52 -5
- package/build/src/database/query/join-object.d.ts.map +1 -1
- package/build/src/database/query/join-object.js +7 -5
- package/build/src/database/query/model-class-query.d.ts +6 -0
- package/build/src/database/query/model-class-query.d.ts.map +1 -1
- package/build/src/database/query/model-class-query.js +33 -8
- package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
- package/build/src/database/query/preloader/belongs-to.js +9 -7
- package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-many.js +23 -16
- package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-one.js +7 -5
- package/build/src/database/query/preloader/query-for-model.d.ts +8 -0
- package/build/src/database/query/preloader/query-for-model.d.ts.map +1 -1
- package/build/src/database/query/preloader/query-for-model.js +15 -2
- package/build/src/database/query/preloader.d.ts.map +1 -1
- package/build/src/database/query/preloader.js +6 -5
- package/build/src/database/query/query-data.js +4 -4
- package/build/src/database/query/where-model-class-hash.d.ts.map +1 -1
- package/build/src/database/query/where-model-class-hash.js +7 -5
- package/build/src/database/query/with-count.js +5 -4
- package/build/src/database/record/index.d.ts +95 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +126 -6
- package/build/src/database/record/instance-relationships/base.d.ts +6 -0
- package/build/src/database/record/instance-relationships/base.d.ts.map +1 -1
- package/build/src/database/record/instance-relationships/base.js +12 -1
- package/build/src/database/record/instance-relationships/belongs-to.js +2 -2
- package/build/src/database/record/instance-relationships/has-many.d.ts.map +1 -1
- package/build/src/database/record/instance-relationships/has-many.js +6 -3
- package/build/src/database/record/instance-relationships/has-one.js +2 -2
- package/build/src/database/record/relationships/base.d.ts +11 -0
- package/build/src/database/record/relationships/base.d.ts.map +1 -1
- package/build/src/database/record/relationships/base.js +15 -1
- package/build/src/database/use-database.d.ts +33 -5
- package/build/src/database/use-database.d.ts.map +1 -1
- package/build/src/database/use-database.js +81 -30
- package/build/src/sync/server-change-feed.d.ts +14 -2
- package/build/src/sync/server-change-feed.d.ts.map +1 -1
- package/build/src/sync/server-change-feed.js +50 -10
- package/build/src/sync/sync-scope-store.d.ts +13 -2
- package/build/src/sync/sync-scope-store.d.ts.map +1 -1
- package/build/src/sync/sync-scope-store.js +48 -7
- package/build/src/tenants/frontend-tenant-sqlite-lifecycle.d.ts +43 -1
- package/build/src/tenants/frontend-tenant-sqlite-lifecycle.d.ts.map +1 -1
- package/build/src/tenants/frontend-tenant-sqlite-lifecycle.js +141 -17
- package/build/src/tenants/tenant-handle.d.ts +36 -0
- package/build/src/tenants/tenant-handle.d.ts.map +1 -1
- package/build/src/tenants/tenant-handle.js +73 -3
- package/build/src/testing/browser-test-app.js +3 -1
- package/build/src/testing/browser-use-database-hook-scenarios.d.ts +16 -0
- package/build/src/testing/browser-use-database-hook-scenarios.d.ts.map +1 -0
- package/build/src/testing/browser-use-database-hook-scenarios.js +182 -0
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +9 -8
- package/build/src/utils/model-scope.d.ts +2 -2
- package/build/src/utils/model-scope.d.ts.map +1 -1
- package/build/src/utils/model-scope.js +5 -3
- package/build/src/utils/ransack.d.ts.map +1 -1
- package/build/src/utils/ransack.js +5 -3
- package/build/sync/server-change-feed.js +58 -8
- package/build/sync/sync-scope-store.js +55 -5
- package/build/tenants/frontend-tenant-sqlite-lifecycle.js +150 -15
- package/build/tenants/tenant-handle.js +90 -6
- package/build/testing/browser-test-app.js +2 -0
- package/build/testing/browser-use-database-hook-scenarios.js +204 -0
- package/build/testing/test-runner.js +8 -7
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/model-scope.js +5 -2
- package/build/utils/ransack.js +5 -2
- package/package.json +2 -1
- package/scripts/browser-test-session.js +60 -0
- package/scripts/test-browser.js +7 -7
- package/src/configuration.js +109 -14
- package/src/database/drivers/base.js +30 -0
- package/src/database/migrator.js +90 -24
- package/src/database/operation.js +55 -4
- package/src/database/query/join-object.js +7 -4
- package/src/database/query/model-class-query.js +38 -7
- package/src/database/query/preloader/belongs-to.js +9 -6
- package/src/database/query/preloader/has-many.js +25 -14
- package/src/database/query/preloader/has-one.js +7 -4
- package/src/database/query/preloader/query-for-model.js +16 -1
- package/src/database/query/preloader.js +6 -4
- package/src/database/query/query-data.js +3 -3
- package/src/database/query/where-model-class-hash.js +8 -4
- package/src/database/query/with-count.js +4 -3
- package/src/database/record/index.js +148 -5
- package/src/database/record/instance-relationships/base.js +13 -0
- package/src/database/record/instance-relationships/belongs-to.js +1 -1
- package/src/database/record/instance-relationships/has-many.js +5 -2
- package/src/database/record/instance-relationships/has-one.js +1 -1
- package/src/database/record/relationships/base.js +17 -0
- package/src/database/use-database.js +81 -28
- package/src/sync/server-change-feed.js +58 -8
- package/src/sync/sync-scope-store.js +55 -5
- package/src/tenants/frontend-tenant-sqlite-lifecycle.js +150 -15
- package/src/tenants/tenant-handle.js +90 -6
- package/src/testing/browser-test-app.js +2 -0
- package/src/testing/browser-use-database-hook-scenarios.js +204 -0
- package/src/testing/test-runner.js +8 -7
- package/src/utils/model-scope.js +5 -2
- package/src/utils/ransack.js +5 -2
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Self-sustaining sync feeds: upstream imports triggered by the changes pull itself, with framework-owned coalescing and throttling (see [docs/sync-upstream-imports.md](docs/sync-upstream-imports.md))
|
|
21
21
|
* AwesomeTasks-shaped offline sync proof using routed resources, domain commands, signed offline grants, and peer-forwarded mutations (see the [developer guide](docs/shared-resource-sync-guide.md) and [proof](docs/awesome-tasks-offline-sync-proof.md))
|
|
22
22
|
* SQLite web persistence that automatically prefers OPFS, then IndexedDB, and migrates legacy persisted bytes when possible (see [docs/sqlite-web-persistence.md](docs/sqlite-web-persistence.md))
|
|
23
|
-
* Bounded frontend tenant SQLite handles with durable flush/close, backend-complete deletion, clean-only LRU eviction, and scoped pins (see [docs/frontend-tenant-sqlite-lifecycle.md](docs/frontend-tenant-sqlite-lifecycle.md))
|
|
23
|
+
* Bounded frontend tenant SQLite handles with independently deduplicated per-database migrations/model readiness, React lifecycle integration, durable flush/close, backend-complete deletion, clean-only LRU eviction, and scoped pins (see [docs/frontend-tenant-sqlite-lifecycle.md](docs/frontend-tenant-sqlite-lifecycle.md))
|
|
24
24
|
* Expo / Metro compatibility guidance and a real Expo export check (see [docs/expo-metro-compatibility.md](docs/expo-metro-compatibility.md))
|
|
25
25
|
* Gap-less positional lists with automatic reordering via `actsAsList`, including models with numeric, string, or UUID primary keys (see [docs/acts-as-list.md](docs/acts-as-list.md))
|
|
26
26
|
* Rails-style nested-attribute writes on frontend-model `save()` (see [docs/nested-attributes.md](docs/nested-attributes.md))
|
|
@@ -955,7 +955,8 @@ Translated models also get a `currentTranslation` `hasOne` relationship scoped t
|
|
|
955
955
|
Async class APIs initialize record metadata on first use when a model has not
|
|
956
956
|
already been initialized eagerly. See [docs/model-initialization.md](docs/model-initialization.md)
|
|
957
957
|
for the eager and lazy initialization behavior, including atomic shared bootstrap
|
|
958
|
-
and complete recovery after an eager initialization failure
|
|
958
|
+
and complete recovery after an eager initialization failure or database-connection
|
|
959
|
+
closure without overlapping stale and current bootstrap side effects.
|
|
959
960
|
|
|
960
961
|
## Lifecycle callbacks
|
|
961
962
|
|
package/build/configuration.js
CHANGED
|
@@ -302,8 +302,9 @@ export default class VelociousConfiguration {
|
|
|
302
302
|
*/
|
|
303
303
|
this._initializeModelsPromise = undefined
|
|
304
304
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
305
|
+
* Current `initialize()` promise, memoized so concurrent callers await the
|
|
306
|
+
* same bootstrap. Retained across a connection close until stale bootstrap
|
|
307
|
+
* work settles, then cleared by identity before the new generation retries.
|
|
307
308
|
* @type {Promise<void> | undefined}
|
|
308
309
|
*/
|
|
309
310
|
this._initializePromise = undefined
|
|
@@ -363,6 +364,15 @@ export default class VelociousConfiguration {
|
|
|
363
364
|
*/
|
|
364
365
|
this._websocketChannelSubscriptions = new Map()
|
|
365
366
|
|
|
367
|
+
/**
|
|
368
|
+
* In-flight local (per-process) websocket channel broadcast deliveries,
|
|
369
|
+
* launched fire-and-forget from `_broadcastToChannelLocal` so one slow
|
|
370
|
+
* subscriber never blocks another. Tracked here so
|
|
371
|
+
* `awaitPendingBroadcasts` can snapshot and drain them before settling.
|
|
372
|
+
* Settled deliveries are removed by the tracking-level cleanup.
|
|
373
|
+
* @type {Set<Promise<void>>} */
|
|
374
|
+
this._localBroadcastDeliveries = new Set()
|
|
375
|
+
|
|
366
376
|
/**
|
|
367
377
|
* Stores the websocket sessions value.
|
|
368
378
|
* @type {Set<import("./http-server/client/websocket-session.js").default>} - Live websocket sessions, including paused sessions within the grace window.
|
|
@@ -1102,6 +1112,18 @@ export default class VelociousConfiguration {
|
|
|
1102
1112
|
}
|
|
1103
1113
|
}
|
|
1104
1114
|
|
|
1115
|
+
/**
|
|
1116
|
+
* Invalidates record metadata owned by one closed/deleted physical tenant
|
|
1117
|
+
* database while preserving every other tenant generation.
|
|
1118
|
+
* @param {string} databaseIdentity - Logical identifier plus pool reuse key.
|
|
1119
|
+
* @returns {void}
|
|
1120
|
+
*/
|
|
1121
|
+
clearRecordMetadataForDatabaseIdentity(databaseIdentity) {
|
|
1122
|
+
for (const modelClass of Object.values(this.modelClasses)) {
|
|
1123
|
+
modelClass.clearRecordMetadataValuesForDatabaseIdentity(databaseIdentity)
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1105
1127
|
/**
|
|
1106
1128
|
* Runs get database pool type.
|
|
1107
1129
|
* @param {string} identifier - Identifier.
|
|
@@ -2054,10 +2076,25 @@ export default class VelociousConfiguration {
|
|
|
2054
2076
|
* @returns {Promise<void>} - Resolves when complete.
|
|
2055
2077
|
*/
|
|
2056
2078
|
async initializeModels(args = {type: "server"}) {
|
|
2079
|
+
const modelInitializationGeneration = this._modelInitializationGeneration
|
|
2080
|
+
|
|
2057
2081
|
if (this._modelsInitialized) return
|
|
2058
|
-
if (this._initializeModelsPromise)
|
|
2082
|
+
if (this._initializeModelsPromise) {
|
|
2083
|
+
const initializeModelsPromise = this._initializeModelsPromise
|
|
2084
|
+
|
|
2085
|
+
await initializeModelsPromise
|
|
2086
|
+
|
|
2087
|
+
if (this._modelInitializationGeneration === modelInitializationGeneration && !this._modelsInitialized) {
|
|
2088
|
+
if (this._initializeModelsPromise === initializeModelsPromise) {
|
|
2089
|
+
this._initializeModelsPromise = undefined
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
return await this.initializeModels(args)
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
return
|
|
2096
|
+
}
|
|
2059
2097
|
|
|
2060
|
-
const modelInitializationGeneration = this._modelInitializationGeneration
|
|
2061
2098
|
const initializeModelsPromise = (async () => {
|
|
2062
2099
|
const shouldSkipDummyModelInitialization = process.env.VELOCIOUS_SKIP_DUMMY_MODEL_INITIALIZATION === "1"
|
|
2063
2100
|
&& process.env.VELOCIOUS_BROWSER_TESTS === "true"
|
|
@@ -2110,6 +2147,12 @@ export default class VelociousConfiguration {
|
|
|
2110
2147
|
* @returns {Promise<void>} - Resolves when complete.
|
|
2111
2148
|
*/
|
|
2112
2149
|
async initialize({type} = {type: "undefined"}) {
|
|
2150
|
+
if (this._closeDatabaseConnectionsPromise) {
|
|
2151
|
+
await this._closeDatabaseConnectionsPromise
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
const initializationGeneration = this._modelInitializationGeneration
|
|
2155
|
+
|
|
2113
2156
|
if (this._isInitialized) return
|
|
2114
2157
|
// Memoize the in-progress initialization so concurrent callers await the same
|
|
2115
2158
|
// bootstrap instead of racing. `_isInitialized` was previously set to `true`
|
|
@@ -2117,15 +2160,29 @@ export default class VelociousConfiguration {
|
|
|
2117
2160
|
// `pooledRunnerConcurrency > 1` starting several jobs on a cold child) could
|
|
2118
2161
|
// skip initialization and load models / perform a job while the first call
|
|
2119
2162
|
// was still awaiting model discovery and initializers. Mirrors connectBeacon.
|
|
2120
|
-
if (this._initializePromise)
|
|
2163
|
+
if (this._initializePromise) {
|
|
2164
|
+
const initializePromise = this._initializePromise
|
|
2165
|
+
|
|
2166
|
+
await initializePromise
|
|
2167
|
+
|
|
2168
|
+
if (this._modelInitializationGeneration === initializationGeneration && !this._isInitialized) {
|
|
2169
|
+
if (this._initializePromise === initializePromise) {
|
|
2170
|
+
this._initializePromise = undefined
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
return await this.initialize({type})
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
return
|
|
2177
|
+
}
|
|
2121
2178
|
|
|
2122
|
-
|
|
2179
|
+
const initializePromise = (async () => {
|
|
2123
2180
|
await this.initializeModels({type})
|
|
2124
2181
|
|
|
2125
2182
|
// Model initialization can be invalidated by a concurrent connection close.
|
|
2126
2183
|
// If models are not ready, stop without marking the configuration initialized
|
|
2127
2184
|
// so the next caller retries a full bootstrap.
|
|
2128
|
-
if (!this._modelsInitialized) return
|
|
2185
|
+
if (this._modelInitializationGeneration !== initializationGeneration || !this._modelsInitialized) return
|
|
2129
2186
|
|
|
2130
2187
|
await this.getEnvironmentHandler().autoDiscoverResources(this)
|
|
2131
2188
|
this._mergeDiscoveredAbilityResources()
|
|
@@ -2147,22 +2204,28 @@ export default class VelociousConfiguration {
|
|
|
2147
2204
|
}
|
|
2148
2205
|
}
|
|
2149
2206
|
|
|
2150
|
-
this.
|
|
2207
|
+
if (this._modelInitializationGeneration === initializationGeneration) {
|
|
2208
|
+
this._isInitialized = true
|
|
2209
|
+
}
|
|
2151
2210
|
})()
|
|
2152
2211
|
|
|
2212
|
+
this._initializePromise = initializePromise
|
|
2213
|
+
|
|
2153
2214
|
try {
|
|
2154
|
-
await
|
|
2215
|
+
await initializePromise
|
|
2155
2216
|
} catch (error) {
|
|
2156
2217
|
// Let a later call retry a failed initialization instead of every future
|
|
2157
2218
|
// caller awaiting the same cached rejection.
|
|
2158
|
-
this._initializePromise
|
|
2219
|
+
if (this._initializePromise === initializePromise) {
|
|
2220
|
+
this._initializePromise = undefined
|
|
2221
|
+
}
|
|
2159
2222
|
throw error
|
|
2160
2223
|
}
|
|
2161
2224
|
|
|
2162
2225
|
// If the inner IIFE returned without marking the configuration initialized
|
|
2163
2226
|
// (e.g. because models were invalidated mid-bootstrap), clear the promise so
|
|
2164
2227
|
// a later call retries a full bootstrap.
|
|
2165
|
-
if (!this._isInitialized) {
|
|
2228
|
+
if (!this._isInitialized && this._initializePromise === initializePromise) {
|
|
2166
2229
|
this._initializePromise = undefined
|
|
2167
2230
|
}
|
|
2168
2231
|
}
|
|
@@ -2756,8 +2819,13 @@ export default class VelociousConfiguration {
|
|
|
2756
2819
|
const websocketEvents = this._websocketEvents
|
|
2757
2820
|
|
|
2758
2821
|
if (websocketEvents && typeof websocketEvents.awaitPendingBroadcasts === "function") {
|
|
2822
|
+
// Drain the host/worker publish queues (including event-log persistence)
|
|
2823
|
+
// before draining local deliveries, because host dispatch launches the
|
|
2824
|
+
// local deliveries synchronously and they must be part of the snapshot.
|
|
2759
2825
|
await websocketEvents.awaitPendingBroadcasts()
|
|
2760
2826
|
}
|
|
2827
|
+
|
|
2828
|
+
await this._awaitLocalBroadcastDeliveries()
|
|
2761
2829
|
}
|
|
2762
2830
|
|
|
2763
2831
|
/**
|
|
@@ -2791,7 +2859,7 @@ export default class VelociousConfiguration {
|
|
|
2791
2859
|
|
|
2792
2860
|
if (!matches) continue
|
|
2793
2861
|
|
|
2794
|
-
|
|
2862
|
+
const delivery = this.withoutCurrentConnectionContexts(() => {
|
|
2795
2863
|
return Promise
|
|
2796
2864
|
.resolve()
|
|
2797
2865
|
.then(() => this._deliverWebsocketChannelBroadcast(subscription, body, {eventId: meta?.eventId}))
|
|
@@ -2799,9 +2867,35 @@ export default class VelociousConfiguration {
|
|
|
2799
2867
|
console.error(`broadcastToChannel: ${name} subscription ${subscription.subscriptionId} deliverBroadcast threw`, error)
|
|
2800
2868
|
})
|
|
2801
2869
|
})
|
|
2870
|
+
|
|
2871
|
+
// Keep the fire-and-forget delivery (never awaited at broadcast time) but
|
|
2872
|
+
// track it so `awaitPendingBroadcasts` can drain it before settling. Remove
|
|
2873
|
+
// on settle; the failure handler also satisfies the promise so a rejected
|
|
2874
|
+
// delivery never becomes an unhandled rejection.
|
|
2875
|
+
this._localBroadcastDeliveries.add(delivery)
|
|
2876
|
+
|
|
2877
|
+
delivery.then(
|
|
2878
|
+
() => { this._localBroadcastDeliveries.delete(delivery) },
|
|
2879
|
+
() => { this._localBroadcastDeliveries.delete(delivery) }
|
|
2880
|
+
)
|
|
2802
2881
|
}
|
|
2803
2882
|
}
|
|
2804
2883
|
|
|
2884
|
+
/**
|
|
2885
|
+
* Awaits a snapshot of the in-flight local (per-process) websocket channel
|
|
2886
|
+
* broadcast deliveries. Called from `awaitPendingBroadcasts` after the host
|
|
2887
|
+
* publish queues drain, so every delivery those queues launched is captured.
|
|
2888
|
+
* New deliveries enqueued after the snapshot are not awaited. Individual
|
|
2889
|
+
* delivery errors are isolated per subscriber — the delivery chain already
|
|
2890
|
+
* logs them and resolves — so a snapshotted rejection never fails this barrier.
|
|
2891
|
+
* @returns {Promise<void>}
|
|
2892
|
+
*/
|
|
2893
|
+
async _awaitLocalBroadcastDeliveries() {
|
|
2894
|
+
const snapshot = [...this._localBroadcastDeliveries]
|
|
2895
|
+
|
|
2896
|
+
await Promise.allSettled(snapshot)
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2805
2899
|
/**
|
|
2806
2900
|
* Runs deliver websocket channel broadcast.
|
|
2807
2901
|
* @param {import("./http-server/websocket-channel.js").default} subscription - Channel subscription.
|
|
@@ -3072,11 +3166,11 @@ export default class VelociousConfiguration {
|
|
|
3072
3166
|
* database configuration. No ambient tenant value is read during checkout or
|
|
3073
3167
|
* execution.
|
|
3074
3168
|
* @template T
|
|
3075
|
-
* @param {{databaseConfiguration: import("./configuration-types.js").DatabaseConfigurationType, databaseIdentifier: string, name?: string, tenant?: object}} options - Captured operation options.
|
|
3169
|
+
* @param {{databaseConfiguration: import("./configuration-types.js").DatabaseConfigurationType, databaseIdentifier: string, name?: string, schemaGeneration?: string, tenant?: object}} options - Captured operation options.
|
|
3076
3170
|
* @param {(operation: DatabaseOperation) => Promise<T>} callback - Operation callback.
|
|
3077
3171
|
* @returns {Promise<T>} - Callback result.
|
|
3078
3172
|
*/
|
|
3079
|
-
async withDatabaseOperation({databaseConfiguration, databaseIdentifier, name = "Configuration.withDatabaseOperation", tenant, ...restArgs}, callback) {
|
|
3173
|
+
async withDatabaseOperation({databaseConfiguration, databaseIdentifier, name = "Configuration.withDatabaseOperation", schemaGeneration, tenant, ...restArgs}, callback) {
|
|
3080
3174
|
restArgsError(restArgs)
|
|
3081
3175
|
|
|
3082
3176
|
if (!databaseIdentifier) throw new Error("Configuration.withDatabaseOperation requires a databaseIdentifier")
|
|
@@ -3095,6 +3189,7 @@ export default class VelociousConfiguration {
|
|
|
3095
3189
|
databaseIdentifier,
|
|
3096
3190
|
enforceCurrentTenantReuseKey: false,
|
|
3097
3191
|
owner,
|
|
3192
|
+
schemaGeneration,
|
|
3098
3193
|
tenant
|
|
3099
3194
|
})
|
|
3100
3195
|
|
|
@@ -195,6 +195,10 @@ export default class VelociousDatabaseDriversBase {
|
|
|
195
195
|
* Narrows the runtime value to the documented type.
|
|
196
196
|
* @type {TransactionCallbackFrame[]} */
|
|
197
197
|
_transactionCallbackFrames
|
|
198
|
+
/** @type {Promise<void>} */
|
|
199
|
+
_transactionCompletionPromise
|
|
200
|
+
/** @type {(() => void) | undefined} */
|
|
201
|
+
_resolveTransactionCompletion
|
|
198
202
|
/**
|
|
199
203
|
* Narrows the runtime value to the documented type.
|
|
200
204
|
* @type {Map<string, Promise<ReturnType<typeof JSON.parse>>>} */
|
|
@@ -231,6 +235,8 @@ export default class VelociousDatabaseDriversBase {
|
|
|
231
235
|
this.logger = new Logger(this)
|
|
232
236
|
this._transactionCallbackFrames = []
|
|
233
237
|
this._transactionsCount = 0
|
|
238
|
+
this._transactionCompletionPromise = Promise.resolve()
|
|
239
|
+
this._resolveTransactionCompletion = undefined
|
|
234
240
|
this._transactionsActionsMutex = new Mutex()
|
|
235
241
|
this._schemaCache = new Map()
|
|
236
242
|
}
|
|
@@ -1426,6 +1432,12 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1426
1432
|
*/
|
|
1427
1433
|
insideTransaction() { return this._transactionsCount > 0 }
|
|
1428
1434
|
|
|
1435
|
+
/**
|
|
1436
|
+
* Returns the completion promise identifying the current outer transaction.
|
|
1437
|
+
* @returns {Promise<void>} Resolves after that transaction commits or rolls back.
|
|
1438
|
+
*/
|
|
1439
|
+
transactionCompletion() { return this._transactionCompletionPromise }
|
|
1440
|
+
|
|
1429
1441
|
/**
|
|
1430
1442
|
* Runs start transaction.
|
|
1431
1443
|
* @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
|
|
@@ -1446,6 +1458,12 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1446
1458
|
|
|
1447
1459
|
await this._startTransactionAction(options)
|
|
1448
1460
|
this._transactionsCount++
|
|
1461
|
+
|
|
1462
|
+
if (this._transactionsCount === 1) {
|
|
1463
|
+
this._transactionCompletionPromise = new Promise((resolve) => {
|
|
1464
|
+
this._resolveTransactionCompletion = resolve
|
|
1465
|
+
})
|
|
1466
|
+
}
|
|
1449
1467
|
})
|
|
1450
1468
|
|
|
1451
1469
|
if (!blockingOperationLease) return
|
|
@@ -1472,9 +1490,20 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1472
1490
|
await this._transactionsActionsMutex.sync(async () => {
|
|
1473
1491
|
await this._commitTransactionAction(options)
|
|
1474
1492
|
this._transactionsCount--
|
|
1493
|
+
this._resolveCompletedTransaction()
|
|
1475
1494
|
})
|
|
1476
1495
|
}
|
|
1477
1496
|
|
|
1497
|
+
/** Resolves the current outer transaction completion when it has finished. */
|
|
1498
|
+
_resolveCompletedTransaction() {
|
|
1499
|
+
if (this._transactionsCount !== 0) return
|
|
1500
|
+
|
|
1501
|
+
const resolve = this._resolveTransactionCompletion
|
|
1502
|
+
|
|
1503
|
+
this._resolveTransactionCompletion = undefined
|
|
1504
|
+
if (resolve) resolve()
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1478
1507
|
/**
|
|
1479
1508
|
* Runs commit transaction action.
|
|
1480
1509
|
* @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
|
|
@@ -2091,6 +2120,7 @@ export default class VelociousDatabaseDriversBase {
|
|
|
2091
2120
|
await this._rollbackTransactionAction(options)
|
|
2092
2121
|
} finally {
|
|
2093
2122
|
this._transactionsCount--
|
|
2123
|
+
this._resolveCompletedTransaction()
|
|
2094
2124
|
|
|
2095
2125
|
// A rolled-back transaction may have reverted DDL (e.g. a CREATE TABLE
|
|
2096
2126
|
// run lazily inside the transaction), so any cached schema metadata is
|
|
@@ -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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
176
|
-
migrationClassName
|
|
228
|
+
migrationClassName: inflection.camelize(match[2].replaceAll("-", "_"))
|
|
177
229
|
})
|
|
178
230
|
}
|
|
179
231
|
|
|
180
|
-
|
|
232
|
+
return migrations.sort((migration1, migration2) => migration1.date - migration2.date)
|
|
233
|
+
}
|
|
181
234
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
+
const boundModelClass = this.modelClass(ModelClass)
|
|
215
|
+
|
|
216
|
+
if (boundModelClass.isInitialized()) this.assertModel(ModelClass)
|
|
166
217
|
|
|
167
|
-
await
|
|
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
|
|
57
|
+
const rawTargetModelClass = relationship.getTargetModelClass()
|
|
58
58
|
|
|
59
|
-
if (!
|
|
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(
|
|
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(
|
|
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()}`)
|