velocious 1.0.489 → 1.0.492
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/build/application.js +3 -0
- package/build/cli/commands/db/tenants/create.js +7 -5
- package/build/cli/commands/db/tenants/drop.js +6 -6
- package/build/configuration-types.js +8 -0
- package/build/configuration.js +29 -0
- package/build/database/query/index.js +30 -0
- package/build/database/query/model-class-query.js +17 -0
- package/build/database/record/index.js +16 -0
- package/build/src/application.d.ts.map +1 -1
- package/build/src/application.js +3 -1
- package/build/src/cli/commands/db/tenants/create.d.ts.map +1 -1
- package/build/src/cli/commands/db/tenants/create.js +8 -5
- package/build/src/cli/commands/db/tenants/drop.d.ts +1 -1
- package/build/src/cli/commands/db/tenants/drop.d.ts.map +1 -1
- package/build/src/cli/commands/db/tenants/drop.js +7 -6
- package/build/src/configuration-types.d.ts +24 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +8 -1
- package/build/src/configuration.d.ts +6 -0
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +26 -1
- package/build/src/database/query/index.d.ts +25 -0
- package/build/src/database/query/index.d.ts.map +1 -1
- package/build/src/database/query/index.js +26 -1
- package/build/src/database/query/model-class-query.d.ts +10 -0
- package/build/src/database/query/model-class-query.d.ts.map +1 -1
- package/build/src/database/query/model-class-query.js +16 -1
- package/build/src/database/record/index.d.ts +7 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +15 -1
- package/build/src/sync/query-scope.d.ts +18 -0
- package/build/src/sync/query-scope.d.ts.map +1 -0
- package/build/src/sync/query-scope.js +77 -0
- package/build/src/sync/server-change-feed.d.ts.map +1 -1
- package/build/src/sync/server-change-feed.js +2 -24
- package/build/src/sync/stable-json.d.ts +9 -0
- package/build/src/sync/stable-json.d.ts.map +1 -0
- package/build/src/sync/stable-json.js +27 -0
- package/build/src/sync/sync-api-client.d.ts +185 -5
- package/build/src/sync/sync-api-client.d.ts.map +1 -1
- package/build/src/sync/sync-api-client.js +309 -17
- package/build/src/sync/sync-api-controller.d.ts +8 -0
- package/build/src/sync/sync-api-controller.d.ts.map +1 -1
- package/build/src/sync/sync-api-controller.js +17 -1
- package/build/src/sync/sync-client-registry.d.ts +12 -0
- package/build/src/sync/sync-client-registry.d.ts.map +1 -0
- package/build/src/sync/sync-client-registry.js +27 -0
- package/build/src/sync/sync-client-types.d.ts +127 -0
- package/build/src/sync/sync-client-types.d.ts.map +1 -0
- package/build/src/sync/sync-client-types.js +3 -0
- package/build/src/sync/sync-client.d.ts +176 -0
- package/build/src/sync/sync-client.d.ts.map +1 -0
- package/build/src/sync/sync-client.js +380 -0
- package/build/src/sync/sync-envelope-replay-service.d.ts +38 -4
- package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/sync-envelope-replay-service.js +73 -6
- package/build/src/sync/sync-resource-base.d.ts +98 -0
- package/build/src/sync/sync-resource-base.d.ts.map +1 -0
- package/build/src/sync/sync-resource-base.js +122 -0
- package/build/src/sync/sync-scope-store.d.ts +128 -0
- package/build/src/sync/sync-scope-store.d.ts.map +1 -0
- package/build/src/sync/sync-scope-store.js +282 -0
- package/build/src/tenants/default-tenant-database-provisioning.d.ts +21 -0
- package/build/src/tenants/default-tenant-database-provisioning.d.ts.map +1 -0
- package/build/src/tenants/default-tenant-database-provisioning.js +106 -0
- package/build/src/tenants/tenant.d.ts.map +1 -1
- package/build/src/tenants/tenant.js +6 -5
- package/build/sync/query-scope.js +84 -0
- package/build/sync/server-change-feed.js +1 -25
- package/build/sync/stable-json.js +28 -0
- package/build/sync/sync-api-client.js +331 -17
- package/build/sync/sync-api-controller.js +20 -0
- package/build/sync/sync-client-registry.js +30 -0
- package/build/sync/sync-client-types.js +41 -0
- package/build/sync/sync-client.js +433 -0
- package/build/sync/sync-envelope-replay-service.js +77 -5
- package/build/sync/sync-resource-base.js +143 -0
- package/build/sync/sync-scope-store.js +327 -0
- package/build/tenants/default-tenant-database-provisioning.js +124 -0
- package/build/tenants/tenant.js +5 -5
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/application.js +3 -0
- package/src/cli/commands/db/tenants/create.js +7 -5
- package/src/cli/commands/db/tenants/drop.js +6 -6
- package/src/configuration-types.js +8 -0
- package/src/configuration.js +29 -0
- package/src/database/query/index.js +30 -0
- package/src/database/query/model-class-query.js +17 -0
- package/src/database/record/index.js +16 -0
- package/src/sync/query-scope.js +84 -0
- package/src/sync/server-change-feed.js +1 -25
- package/src/sync/stable-json.js +28 -0
- package/src/sync/sync-api-client.js +331 -17
- package/src/sync/sync-api-controller.js +20 -0
- package/src/sync/sync-client-registry.js +30 -0
- package/src/sync/sync-client-types.js +41 -0
- package/src/sync/sync-client.js +433 -0
- package/src/sync/sync-envelope-replay-service.js +77 -5
- package/src/sync/sync-resource-base.js +143 -0
- package/src/sync/sync-scope-store.js +327 -0
- package/src/tenants/default-tenant-database-provisioning.js +124 -0
- package/src/tenants/tenant.js +5 -5
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import TableData from "../database/table-data/index.js"
|
|
4
|
+
import UUID from "pure-uuid"
|
|
5
|
+
|
|
6
|
+
import {scopeKey} from "./query-scope.js"
|
|
7
|
+
import stableJsonStringify from "./stable-json.js"
|
|
8
|
+
|
|
9
|
+
const TABLE_NAME = "velocious_sync_scopes"
|
|
10
|
+
const SCOPE_DIGEST_PREFIX = "velocious-sync-scope:"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Digests a serialized scope into a fixed-size deterministic key, so scope
|
|
14
|
+
* identities with long condition values fit an indexed string column.
|
|
15
|
+
* @param {import("./sync-client-types.js").SerializedSyncScope} scope - Serialized sync scope.
|
|
16
|
+
* @returns {string} Deterministic UUIDv5 digest of the canonical scope key.
|
|
17
|
+
*/
|
|
18
|
+
function scopeDigestForScope(scope) {
|
|
19
|
+
return new UUID(5, "ns:URL", `${SCOPE_DIGEST_PREFIX}${scopeKey(scope)}`).format()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} SyncScopeRow
|
|
24
|
+
* @property {Record<string, ?>} conditions - Scope attribute conditions.
|
|
25
|
+
* @property {string | null} cursorPayload - Persisted cursor JSON payload.
|
|
26
|
+
* @property {string} id - Scope row id.
|
|
27
|
+
* @property {string} resourceType - Scope resource/model name.
|
|
28
|
+
* @property {string} scopeDigest - Fixed-size deterministic digest of the canonical scope key.
|
|
29
|
+
* @property {string} state - Scope state ("active" or "removed").
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Framework-owned local persistence for declared sync scopes and their cursors.
|
|
34
|
+
*
|
|
35
|
+
* Backed by an auto-created `velocious_sync_scopes` table on the configured
|
|
36
|
+
* database, with a process-local memory fallback when no database is
|
|
37
|
+
* configured (mirroring the server change-feed store).
|
|
38
|
+
*/
|
|
39
|
+
export default class SyncScopeStore {
|
|
40
|
+
/**
|
|
41
|
+
* Creates a sync scope store.
|
|
42
|
+
* @param {object} args - Options.
|
|
43
|
+
* @param {import("../configuration.js").default} args.configuration - Configuration owning the database.
|
|
44
|
+
* @param {string} [args.databaseIdentifier] - Database identifier.
|
|
45
|
+
*/
|
|
46
|
+
constructor({configuration, databaseIdentifier = "default"}) {
|
|
47
|
+
this.configuration = configuration
|
|
48
|
+
this.databaseIdentifier = databaseIdentifier
|
|
49
|
+
/** @type {Map<string, SyncScopeRow>} */
|
|
50
|
+
this._memoryScopes = new Map()
|
|
51
|
+
this._isReady = false
|
|
52
|
+
/** @type {Promise<void> | null} */
|
|
53
|
+
this._readyPromise = null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Ensures the backing table exists.
|
|
58
|
+
* @returns {Promise<void>} Resolves when ready.
|
|
59
|
+
*/
|
|
60
|
+
async ensureReady() {
|
|
61
|
+
if (this._isReady) return
|
|
62
|
+
|
|
63
|
+
if (this._usesMemoryStorage()) {
|
|
64
|
+
this._isReady = true
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (this._readyPromise) return await this._readyPromise
|
|
69
|
+
|
|
70
|
+
this._readyPromise = this._withDb(async (db) => {
|
|
71
|
+
await this._ensureScopesTable(db)
|
|
72
|
+
this._isReady = true
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await this._readyPromise
|
|
77
|
+
} finally {
|
|
78
|
+
if (!this._isReady) this._readyPromise = null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Finds or creates the scope row for a serialized scope, reactivating removed scopes.
|
|
84
|
+
* @param {import("./sync-client-types.js").SerializedSyncScope} scope - Serialized sync scope.
|
|
85
|
+
* @returns {Promise<SyncScopeRow>} Persisted scope row.
|
|
86
|
+
*/
|
|
87
|
+
async findOrCreateScope(scope) {
|
|
88
|
+
await this.ensureReady()
|
|
89
|
+
|
|
90
|
+
const digest = scopeDigestForScope(scope)
|
|
91
|
+
|
|
92
|
+
if (this._usesMemoryStorage()) {
|
|
93
|
+
const existingScope = this._memoryScopes.get(digest)
|
|
94
|
+
|
|
95
|
+
if (existingScope) {
|
|
96
|
+
if (existingScope.state !== "active") existingScope.state = "active"
|
|
97
|
+
|
|
98
|
+
return existingScope
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const newScope = {
|
|
102
|
+
conditions: scope.conditions,
|
|
103
|
+
cursorPayload: null,
|
|
104
|
+
id: new UUID(4).format(),
|
|
105
|
+
resourceType: scope.resourceType,
|
|
106
|
+
scopeDigest: digest,
|
|
107
|
+
state: "active"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this._memoryScopes.set(digest, newScope)
|
|
111
|
+
|
|
112
|
+
return newScope
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return await this._withDb(async (db) => {
|
|
116
|
+
const existingRow = await this._rowByScopeDigest(db, digest)
|
|
117
|
+
|
|
118
|
+
if (existingRow) {
|
|
119
|
+
if (existingRow.state !== "active") {
|
|
120
|
+
await db.update({conditions: {id: existingRow.id}, data: {state: "active", updated_at: new Date()}, tableName: TABLE_NAME})
|
|
121
|
+
existingRow.state = "active"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return existingRow
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await db.insert({
|
|
128
|
+
tableName: TABLE_NAME,
|
|
129
|
+
data: {
|
|
130
|
+
conditions_json: stableJsonStringify(scope.conditions),
|
|
131
|
+
created_at: new Date(),
|
|
132
|
+
cursor_json: null,
|
|
133
|
+
id: new UUID(4).format(),
|
|
134
|
+
resource_type: scope.resourceType,
|
|
135
|
+
scope_digest: digest,
|
|
136
|
+
state: "active",
|
|
137
|
+
updated_at: new Date()
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const createdRow = await this._rowByScopeDigest(db, digest)
|
|
142
|
+
|
|
143
|
+
if (!createdRow) throw new Error("Failed to persist sync scope")
|
|
144
|
+
|
|
145
|
+
return createdRow
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Returns all active scope rows.
|
|
151
|
+
* @returns {Promise<SyncScopeRow[]>} Active scope rows.
|
|
152
|
+
*/
|
|
153
|
+
async activeScopes() {
|
|
154
|
+
await this.ensureReady()
|
|
155
|
+
|
|
156
|
+
if (this._usesMemoryStorage()) {
|
|
157
|
+
return [...this._memoryScopes.values()].filter((scope) => scope.state === "active")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return await this._withDb(async (db) => {
|
|
161
|
+
const rows = /** @type {Array<Record<string, ?>>} */ (await db
|
|
162
|
+
.newQuery()
|
|
163
|
+
.from(TABLE_NAME)
|
|
164
|
+
.where({state: "active"})
|
|
165
|
+
.order("created_at ASC")
|
|
166
|
+
.results())
|
|
167
|
+
|
|
168
|
+
return rows.map((row) => this._normalizeScopeRow(row))
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Loads the persisted cursor payload for a scope row.
|
|
174
|
+
* @param {SyncScopeRow} scopeRow - Scope row.
|
|
175
|
+
* @returns {Promise<string | null>} Cursor JSON payload.
|
|
176
|
+
*/
|
|
177
|
+
async loadCursor(scopeRow) {
|
|
178
|
+
await this.ensureReady()
|
|
179
|
+
|
|
180
|
+
if (this._usesMemoryStorage()) {
|
|
181
|
+
return this._memoryScopes.get(scopeRow.scopeDigest)?.cursorPayload ?? null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return await this._withDb(async (db) => {
|
|
185
|
+
const row = await this._rowByScopeDigest(db, scopeRow.scopeDigest)
|
|
186
|
+
|
|
187
|
+
return row ? row.cursorPayload : null
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Persists the acknowledged cursor for a scope row.
|
|
193
|
+
* @param {SyncScopeRow} scopeRow - Scope row.
|
|
194
|
+
* @param {import("./sync-api-client-types.js").SyncCursor} cursor - Acknowledged cursor.
|
|
195
|
+
* @returns {Promise<void>}
|
|
196
|
+
*/
|
|
197
|
+
async saveCursor(scopeRow, cursor) {
|
|
198
|
+
if (!cursor) return
|
|
199
|
+
|
|
200
|
+
await this.ensureReady()
|
|
201
|
+
|
|
202
|
+
const cursorPayload = JSON.stringify(cursor)
|
|
203
|
+
|
|
204
|
+
if (this._usesMemoryStorage()) {
|
|
205
|
+
const memoryScope = this._memoryScopes.get(scopeRow.scopeDigest)
|
|
206
|
+
|
|
207
|
+
if (!memoryScope) throw new Error(`No sync scope found for: ${scopeRow.scopeDigest}`)
|
|
208
|
+
|
|
209
|
+
memoryScope.cursorPayload = cursorPayload
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await this._withDb(async (db) => {
|
|
214
|
+
await db.update({
|
|
215
|
+
conditions: {scope_digest: scopeRow.scopeDigest},
|
|
216
|
+
data: {cursor_json: cursorPayload, updated_at: new Date()},
|
|
217
|
+
tableName: TABLE_NAME
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Deactivates the scope row for a serialized scope.
|
|
224
|
+
* @param {import("./sync-client-types.js").SerializedSyncScope} scope - Serialized sync scope.
|
|
225
|
+
* @returns {Promise<void>}
|
|
226
|
+
*/
|
|
227
|
+
async deactivate(scope) {
|
|
228
|
+
await this.ensureReady()
|
|
229
|
+
|
|
230
|
+
const digest = scopeDigestForScope(scope)
|
|
231
|
+
|
|
232
|
+
if (this._usesMemoryStorage()) {
|
|
233
|
+
const memoryScope = this._memoryScopes.get(digest)
|
|
234
|
+
|
|
235
|
+
if (memoryScope) memoryScope.state = "removed"
|
|
236
|
+
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await this._withDb(async (db) => {
|
|
241
|
+
await db.update({conditions: {scope_digest: digest}, data: {state: "removed", updated_at: new Date()}, tableName: TABLE_NAME})
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Whether the store runs without a configured database.
|
|
247
|
+
* @returns {boolean} Whether memory storage is used.
|
|
248
|
+
*/
|
|
249
|
+
_usesMemoryStorage() {
|
|
250
|
+
try {
|
|
251
|
+
return !this.configuration.getDatabaseConfiguration()[this.databaseIdentifier]
|
|
252
|
+
} catch {
|
|
253
|
+
return true
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Runs a callback with a database connection.
|
|
259
|
+
* @template Result
|
|
260
|
+
* @param {(db: import("../database/drivers/base.js").default) => Promise<Result>} callback - Database callback.
|
|
261
|
+
* @returns {Promise<Result>} Callback result.
|
|
262
|
+
*/
|
|
263
|
+
async _withDb(callback) {
|
|
264
|
+
return await this.configuration.ensureConnections({name: "Sync scope store"}, async (dbs) => {
|
|
265
|
+
const db = dbs[this.databaseIdentifier]
|
|
266
|
+
|
|
267
|
+
if (!db) throw new Error(`No database connection available for identifier: ${this.databaseIdentifier}`)
|
|
268
|
+
|
|
269
|
+
return await callback(db)
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Ensures the scopes table exists.
|
|
275
|
+
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
276
|
+
* @returns {Promise<void>}
|
|
277
|
+
*/
|
|
278
|
+
async _ensureScopesTable(db) {
|
|
279
|
+
if (await db.tableExists(TABLE_NAME)) return
|
|
280
|
+
|
|
281
|
+
const table = new TableData(TABLE_NAME, {ifNotExists: true})
|
|
282
|
+
|
|
283
|
+
table.string("id", {null: false, primaryKey: true})
|
|
284
|
+
table.string("scope_digest", {index: true, null: false})
|
|
285
|
+
table.string("resource_type", {index: true, null: false})
|
|
286
|
+
table.text("conditions_json", {null: false})
|
|
287
|
+
table.text("cursor_json", {null: true})
|
|
288
|
+
table.string("state", {index: true, null: false})
|
|
289
|
+
table.datetime("created_at", {null: false})
|
|
290
|
+
table.datetime("updated_at", {null: false})
|
|
291
|
+
|
|
292
|
+
await db.createTable(table)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Resolves a scope row by its digest.
|
|
297
|
+
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
298
|
+
* @param {string} digest - Fixed-size scope digest.
|
|
299
|
+
* @returns {Promise<SyncScopeRow | null>} Scope row or null.
|
|
300
|
+
*/
|
|
301
|
+
async _rowByScopeDigest(db, digest) {
|
|
302
|
+
const rows = /** @type {Array<Record<string, ?>>} */ (await db
|
|
303
|
+
.newQuery()
|
|
304
|
+
.from(TABLE_NAME)
|
|
305
|
+
.where({scope_digest: digest})
|
|
306
|
+
.limit(1)
|
|
307
|
+
.results())
|
|
308
|
+
|
|
309
|
+
return rows[0] ? this._normalizeScopeRow(rows[0]) : null
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Normalizes a raw scope table row.
|
|
314
|
+
* @param {Record<string, ?>} row - Raw table row.
|
|
315
|
+
* @returns {SyncScopeRow} Normalized scope row.
|
|
316
|
+
*/
|
|
317
|
+
_normalizeScopeRow(row) {
|
|
318
|
+
return {
|
|
319
|
+
conditions: /** @type {Record<string, ?>} */ (JSON.parse(String(row.conditions_json))),
|
|
320
|
+
cursorPayload: row.cursor_json === null || row.cursor_json === undefined ? null : String(row.cursor_json),
|
|
321
|
+
id: String(row.id),
|
|
322
|
+
resourceType: String(row.resource_type),
|
|
323
|
+
scopeDigest: String(row.scope_digest),
|
|
324
|
+
state: String(row.state)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default tenant-database provisioning used by `db:tenants:create`,
|
|
5
|
+
* `db:tenants:drop`, and `Tenant.drop` when a tenant-database provider does not
|
|
6
|
+
* define its own `createDatabase`/`dropDatabase`. This is the generic,
|
|
7
|
+
* driver-agnostic lifecycle every app on the tenant framework needs, so it lives
|
|
8
|
+
* in the framework instead of being reimplemented in each consuming project.
|
|
9
|
+
*
|
|
10
|
+
* File-backed drivers (sqlite) have no server-side CREATE/DROP DATABASE — the file
|
|
11
|
+
* is created on connect and left in place on drop — so creating just ensures the
|
|
12
|
+
* tenant's connection and dropping is a no-op. Server drivers (mysql, pgsql, …)
|
|
13
|
+
* connect to the configured maintenance database (`useDatabase`) and run the
|
|
14
|
+
* driver's own `createDatabaseSql`/`dropDatabaseSql`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const SAFE_TENANT_DATABASE_NAME = /^[a-z0-9_]+$/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether the driver stores each database as a local file instead of on a server.
|
|
21
|
+
* @param {import("../configuration-types.js").DatabaseConfigurationType} databaseConfiguration - Resolved tenant database configuration.
|
|
22
|
+
* @returns {boolean} - True for file-backed drivers such as sqlite.
|
|
23
|
+
*/
|
|
24
|
+
function fileBackedDriver(databaseConfiguration) {
|
|
25
|
+
return databaseConfiguration.type === "sqlite"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validates the tenant database name against a strict allowlist before it is
|
|
30
|
+
* interpolated into DDL.
|
|
31
|
+
* @param {import("../configuration-types.js").DatabaseConfigurationType} databaseConfiguration - Resolved tenant database configuration.
|
|
32
|
+
* @returns {string} - The validated tenant database name.
|
|
33
|
+
*/
|
|
34
|
+
function guardedDatabaseName(databaseConfiguration) {
|
|
35
|
+
const databaseName = databaseConfiguration.database
|
|
36
|
+
|
|
37
|
+
if (!databaseName || !SAFE_TENANT_DATABASE_NAME.test(databaseName)) {
|
|
38
|
+
throw new Error(`Unsafe tenant database name: ${databaseName}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return databaseName
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Opens a driver connected to the server's maintenance database (`useDatabase`),
|
|
46
|
+
* from which `CREATE DATABASE` / `DROP DATABASE` for the tenant can be issued.
|
|
47
|
+
* @param {import("../configuration-types.js").DatabaseConfigurationType} databaseConfiguration - Resolved tenant database configuration.
|
|
48
|
+
* @param {import("../configuration.js").default} configuration - The Velocious configuration.
|
|
49
|
+
* @returns {import("../database/drivers/base.js").default} - A driver bound to the maintenance database.
|
|
50
|
+
*/
|
|
51
|
+
function maintenanceConnection(databaseConfiguration, configuration) {
|
|
52
|
+
const DriverClass = databaseConfiguration.driver
|
|
53
|
+
|
|
54
|
+
if (!DriverClass) {
|
|
55
|
+
throw new Error("Tenant database configuration is missing a driver.")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!databaseConfiguration.useDatabase) {
|
|
59
|
+
throw new Error("Tenant database configuration is missing a maintenance database in useDatabase.")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new DriverClass({...databaseConfiguration, database: databaseConfiguration.useDatabase}, configuration)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates the tenant database for one tenant.
|
|
67
|
+
* @param {{configuration: import("../configuration.js").default, databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType, tenant: ?}} args - Provisioning arguments.
|
|
68
|
+
* @returns {Promise<void>} - Resolves once the tenant database exists.
|
|
69
|
+
*/
|
|
70
|
+
export async function createTenantDatabase({configuration, databaseConfiguration, tenant}) {
|
|
71
|
+
if (fileBackedDriver(databaseConfiguration)) {
|
|
72
|
+
await configuration.runWithTenant(tenant, async () => {
|
|
73
|
+
await configuration.ensureConnections(async () => {})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const databaseName = guardedDatabaseName(databaseConfiguration)
|
|
80
|
+
const connection = maintenanceConnection(databaseConfiguration, configuration)
|
|
81
|
+
|
|
82
|
+
await connection.connect()
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const sqls = connection.createDatabaseSql(databaseName, {
|
|
86
|
+
databaseCharset: databaseConfiguration.databaseCharset,
|
|
87
|
+
databaseCollation: databaseConfiguration.databaseCollation,
|
|
88
|
+
ifNotExists: true
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
for (const sql of sqls) {
|
|
92
|
+
await connection.query(sql)
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
await connection.close()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Drops the tenant database for one tenant. Uses `DROP DATABASE IF EXISTS`, so it
|
|
101
|
+
* is a safe no-op for a tenant that was never fully provisioned.
|
|
102
|
+
* @param {{configuration: import("../configuration.js").default, databaseConfiguration: import("../configuration-types.js").DatabaseConfigurationType}} args - Provisioning arguments.
|
|
103
|
+
* @returns {Promise<void>} - Resolves once the tenant database is gone.
|
|
104
|
+
*/
|
|
105
|
+
export async function dropTenantDatabase({configuration, databaseConfiguration}) {
|
|
106
|
+
if (fileBackedDriver(databaseConfiguration)) {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const databaseName = guardedDatabaseName(databaseConfiguration)
|
|
111
|
+
const connection = maintenanceConnection(databaseConfiguration, configuration)
|
|
112
|
+
|
|
113
|
+
await connection.connect()
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const sqls = connection.dropDatabaseSql(databaseName, {ifExists: true})
|
|
117
|
+
|
|
118
|
+
for (const sql of sqls) {
|
|
119
|
+
await connection.query(sql)
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
await connection.close()
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/tenants/tenant.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import Current from "../current.js"
|
|
4
|
+
import {dropTenantDatabase} from "./default-tenant-database-provisioning.js"
|
|
4
5
|
import TenantIterator from "./tenant-iterator.js"
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -80,10 +81,9 @@ export default class Tenant {
|
|
|
80
81
|
*/
|
|
81
82
|
static async drop({identifier, tenant, configuration = Current.configuration()}) {
|
|
82
83
|
const provider = configuration.getTenantDatabaseProvider(identifier)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
84
|
+
const dropDatabase = typeof provider.dropDatabase === "function"
|
|
85
|
+
? provider.dropDatabase.bind(provider)
|
|
86
|
+
: dropTenantDatabase
|
|
87
87
|
|
|
88
88
|
await configuration.runWithTenant(tenant, async () => {
|
|
89
89
|
// Guard against an unresolved tenant. resolveDatabaseConfiguration falls back to the
|
|
@@ -94,7 +94,7 @@ export default class Tenant {
|
|
|
94
94
|
throw new Error(`Tenant database identifier ${identifier} is inactive for tenant: ${TenantIterator.tenantLabel(tenant)}`)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
await
|
|
97
|
+
await dropDatabase({
|
|
98
98
|
configuration,
|
|
99
99
|
databaseConfiguration: configuration.resolveDatabaseConfiguration(identifier),
|
|
100
100
|
identifier,
|