velocious 1.0.520 → 1.0.522
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 +4 -2
- package/build/background-jobs/store.js +40 -0
- package/build/configuration-types.js +18 -4
- package/build/database/drivers/base.js +13 -0
- package/build/database/drivers/mysql/index.js +1 -0
- package/build/src/background-jobs/store.d.ts +14 -0
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +36 -1
- package/build/src/configuration-types.d.ts +36 -7
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +19 -5
- package/build/src/database/drivers/base.d.ts +12 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +13 -1
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +2 -1
- package/build/src/tenants/tenant-aggregator.d.ts +180 -0
- package/build/src/tenants/tenant-aggregator.d.ts.map +1 -0
- package/build/src/tenants/tenant-aggregator.js +285 -0
- package/build/src/tenants/tenant.d.ts +13 -0
- package/build/src/tenants/tenant.d.ts.map +1 -1
- package/build/src/tenants/tenant.js +17 -1
- package/build/tenants/tenant-aggregator.js +351 -0
- package/build/tenants/tenant.js +17 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/background-jobs/store.js +40 -0
- package/src/configuration-types.js +18 -4
- package/src/database/drivers/base.js +13 -0
- package/src/database/drivers/mysql/index.js +1 -0
- package/src/tenants/tenant-aggregator.js +351 -0
- package/src/tenants/tenant.js +17 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import Current from "../current.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {"SUM" | "COUNT" | "MAX" | "MIN"} AggregateOperation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {AggregateOperation | {op: AggregateOperation, column: string}} AggregateSpec
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} SubqueryContext
|
|
15
|
+
* @property {(tableName: string) => string} table - Quotes a table for this tenant. In a cross-database `UNION ALL` it returns a `database`.`table` qualified identifier; when each tenant runs on its own connection it returns the plain quoted table name. Use it for every table the subquery reads so the same subquery works on both execution paths.
|
|
16
|
+
* @property {((value: ?) => string) & {list: (values: Array<?>) => string}} quote - Quotes a value for the active connection. `quote.list(values)` quotes and comma-joins an array for `IN (...)` clauses.
|
|
17
|
+
* @property {object} tenant - The tenant descriptor this subquery is being built for.
|
|
18
|
+
* @property {import("../database/drivers/base.js").default} connection - The driver the query will run on.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} TenantAggregateOptions
|
|
23
|
+
* @property {string} identifier - Database identifier whose tenants are aggregated (for example `"projectTenant"`).
|
|
24
|
+
* @property {object[]} [tenants] - Explicit tenant descriptors to aggregate. Defaults to every tenant the identifier's provider `listTenants` returns.
|
|
25
|
+
* @property {(tenant: ?) => boolean} [filter] - Optional filter applied to the resolved tenant list.
|
|
26
|
+
* @property {string[]} [keyColumns] - Columns the aggregate is grouped by (for example `["docker_server_id"]`). Empty means a single grand-total row.
|
|
27
|
+
* @property {Record<string, AggregateSpec>} aggregates - Output column name to aggregate. `"SUM"` is shorthand for `{op: "SUM", column: <output name>}`; use `{op: "COUNT", column: "*"}` for `COUNT(*)`.
|
|
28
|
+
* @property {(context: SubqueryContext) => string} subquery - Builds one tenant's inner `SELECT`, which must select every `keyColumns` entry plus every aggregate source column.
|
|
29
|
+
* @property {import("../configuration.js").default} [configuration] - Configuration to run against. Defaults to the current configuration.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {{tenant: object, database: string | undefined, serverKey: string}} ResolvedTenant
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const AGGREGATE_OPERATIONS = new Set(["SUM", "COUNT", "MAX", "MIN"])
|
|
37
|
+
const DERIVED_TABLE_ALIAS = "velocious_tenant_aggregate"
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Runs one aggregate query across many tenant databases and merges the result. Tenant databases may
|
|
41
|
+
* be co-located on the default server or spread across other servers, and they can appear or
|
|
42
|
+
* disappear at runtime; this resolves the live tenant list, groups tenants by the server they live
|
|
43
|
+
* on, and — per server — emits a single cross-database `UNION ALL` when the driver supports qualified
|
|
44
|
+
* cross-database references (MySQL/MSSQL) or falls back to one query per tenant otherwise
|
|
45
|
+
* (PostgreSQL/SQLite). Results from every server are merged with the aggregate's own operation, so
|
|
46
|
+
* callers get one combined result set regardless of how the tenants are distributed. Reached through
|
|
47
|
+
* `Tenant.aggregateAcross`.
|
|
48
|
+
*/
|
|
49
|
+
export default class TenantAggregator {
|
|
50
|
+
/**
|
|
51
|
+
* Prepares an aggregate run, normalizing the aggregate specs up front.
|
|
52
|
+
* @param {TenantAggregateOptions} options - Aggregate configuration.
|
|
53
|
+
*/
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.options = options
|
|
56
|
+
this.configuration = options.configuration ?? Current.configuration()
|
|
57
|
+
this.keyColumns = options.keyColumns ?? []
|
|
58
|
+
this.aggregates = this._normalizeAggregates(options.aggregates)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolves the tenant list, runs the aggregate per server, and merges everything.
|
|
63
|
+
* @returns {Promise<Array<Record<string, ?>>>} - One merged row per distinct key-column combination.
|
|
64
|
+
*/
|
|
65
|
+
async run() {
|
|
66
|
+
const resolvedTenants = await this._resolveTenants()
|
|
67
|
+
|
|
68
|
+
if (resolvedTenants.length === 0) return []
|
|
69
|
+
|
|
70
|
+
/** @type {Array<Record<string, ?>>} */
|
|
71
|
+
const rows = []
|
|
72
|
+
|
|
73
|
+
for (const group of this._groupByServer(resolvedTenants)) {
|
|
74
|
+
const groupRows = await this._runForServer(group)
|
|
75
|
+
|
|
76
|
+
rows.push(...groupRows)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return this._mergeRows(rows)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves the tenant list (explicit or from the provider), then maps each to its server and
|
|
84
|
+
* database via the tenant database resolver.
|
|
85
|
+
* @returns {Promise<ResolvedTenant[]>} - Tenants to aggregate, each resolved to its server + database.
|
|
86
|
+
*/
|
|
87
|
+
async _resolveTenants() {
|
|
88
|
+
const tenants = this.options.tenants ?? await this._listProviderTenants()
|
|
89
|
+
const filtered = this.options.filter ? tenants.filter(this.options.filter) : tenants
|
|
90
|
+
|
|
91
|
+
return filtered.map((tenant) => {
|
|
92
|
+
const databaseConfiguration = this.configuration.resolveDatabaseConfiguration(this.options.identifier, tenant)
|
|
93
|
+
|
|
94
|
+
// The server key groups tenants that share a connection endpoint so co-located tenants can be
|
|
95
|
+
// UNION-ed on one connection. It only needs to be exact for the cross-database `UNION ALL`
|
|
96
|
+
// path, which is MySQL-only, where host/port/username/type fully identify the server. Every
|
|
97
|
+
// other driver takes the fan-out path (one connection per tenant), so a coarse key there just
|
|
98
|
+
// groups tenants that are then queried individually anyway — it can never route a query to the
|
|
99
|
+
// wrong server.
|
|
100
|
+
return {
|
|
101
|
+
database: databaseConfiguration.database,
|
|
102
|
+
serverKey: JSON.stringify([
|
|
103
|
+
databaseConfiguration.type ?? null,
|
|
104
|
+
databaseConfiguration.host ?? null,
|
|
105
|
+
databaseConfiguration.port ?? null,
|
|
106
|
+
databaseConfiguration.username ?? null
|
|
107
|
+
]),
|
|
108
|
+
tenant
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Lists every tenant for the identifier through its provider's `listTenants` hook.
|
|
115
|
+
* @returns {Promise<object[]>} - Every tenant the provider lists for the identifier.
|
|
116
|
+
*/
|
|
117
|
+
async _listProviderTenants() {
|
|
118
|
+
const provider = this.configuration.getTenantDatabaseProvider(this.options.identifier)
|
|
119
|
+
const listedTenants = await this.configuration.ensureConnections(
|
|
120
|
+
{name: `Tenant.aggregateAcross: ${this.options.identifier}`},
|
|
121
|
+
async () => await provider.listTenants({configuration: this.configuration, identifier: this.options.identifier})
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if (!Array.isArray(listedTenants)) {
|
|
125
|
+
throw new Error(`Tenant database provider for ${this.options.identifier} must return an array from listTenants`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return listedTenants
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Groups tenants by the server they live on so co-located tenants can share one query.
|
|
133
|
+
* @param {ResolvedTenant[]} resolvedTenants - Tenants resolved to their server.
|
|
134
|
+
* @returns {ResolvedTenant[][]} - Tenants grouped by the server they live on.
|
|
135
|
+
*/
|
|
136
|
+
_groupByServer(resolvedTenants) {
|
|
137
|
+
/** @type {Map<string, ResolvedTenant[]>} */
|
|
138
|
+
const groups = new Map()
|
|
139
|
+
|
|
140
|
+
for (const resolvedTenant of resolvedTenants) {
|
|
141
|
+
const group = groups.get(resolvedTenant.serverKey)
|
|
142
|
+
|
|
143
|
+
if (group) {
|
|
144
|
+
group.push(resolvedTenant)
|
|
145
|
+
} else {
|
|
146
|
+
groups.set(resolvedTenant.serverKey, [resolvedTenant])
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return Array.from(groups.values())
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Runs the aggregate for one server's tenants, using a single cross-database `UNION ALL` when the
|
|
155
|
+
* driver supports it or one query per tenant otherwise. The driver capability is probed in its own
|
|
156
|
+
* connection scope that is released before the fan-out runs, so a `max: 1` tenant pool is never
|
|
157
|
+
* asked for a second connection while the first is still held (which would deadlock).
|
|
158
|
+
* @param {ResolvedTenant[]} group - Tenants sharing one server.
|
|
159
|
+
* @returns {Promise<Array<Record<string, ?>>>} - Rows produced for this server.
|
|
160
|
+
*/
|
|
161
|
+
async _runForServer(group) {
|
|
162
|
+
const [firstTenant] = group
|
|
163
|
+
const supportsCrossDatabaseReferences = await this._withTenant(
|
|
164
|
+
firstTenant.tenant,
|
|
165
|
+
async (connections) => connections[this.options.identifier].supportsCrossDatabaseReferences()
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if (supportsCrossDatabaseReferences) {
|
|
169
|
+
return await this._withTenant(firstTenant.tenant, async (connections) => {
|
|
170
|
+
const connection = connections[this.options.identifier]
|
|
171
|
+
|
|
172
|
+
return await connection.query(this.buildAggregateSql({connection, entries: group, qualified: true}))
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @type {Array<Record<string, ?>>} */
|
|
177
|
+
const rows = []
|
|
178
|
+
|
|
179
|
+
for (const resolvedTenant of group) {
|
|
180
|
+
const tenantRows = await this._withTenant(resolvedTenant.tenant, async (connections) => {
|
|
181
|
+
const connection = connections[this.options.identifier]
|
|
182
|
+
|
|
183
|
+
return await connection.query(this.buildAggregateSql({connection, entries: [resolvedTenant], qualified: false}))
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
rows.push(...tenantRows)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return rows
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Runs `callback` inside a tenant's context with its connections established, mirroring
|
|
194
|
+
* `Tenant.with` without importing it (which would create an import cycle).
|
|
195
|
+
* @template T
|
|
196
|
+
* @param {object} tenant - Tenant descriptor to switch into.
|
|
197
|
+
* @param {(connections: Record<string, import("../database/drivers/base.js").default>) => Promise<T>} callback - Operation to run with the tenant's active connections.
|
|
198
|
+
* @returns {Promise<T>} - Callback result from within the tenant context.
|
|
199
|
+
*/
|
|
200
|
+
async _withTenant(tenant, callback) {
|
|
201
|
+
return await this.configuration.runWithTenant(tenant, async () => await this.configuration.ensureConnections(callback))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Builds the aggregate SQL: an outer `GROUP BY` over a `UNION ALL` of each entry's subquery.
|
|
206
|
+
* @param {{connection: import("../database/drivers/base.js").default, entries: ResolvedTenant[], qualified: boolean}} args - Connection, tenants, and whether to qualify tables with their database name.
|
|
207
|
+
* @returns {string} - Executable aggregate SQL.
|
|
208
|
+
*/
|
|
209
|
+
buildAggregateSql({connection, entries, qualified}) {
|
|
210
|
+
const options = connection.options()
|
|
211
|
+
const quote = this._buildQuote(connection)
|
|
212
|
+
const subqueries = entries.map((entry) => {
|
|
213
|
+
if (qualified && !entry.database) {
|
|
214
|
+
throw new Error(`Cannot build a cross-database query for a tenant without a resolved database name (identifier: ${this.options.identifier}).`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const table = (/** @type {string} */ tableName) => qualified
|
|
218
|
+
? `${options.quoteDatabaseName(/** @type {string} */ (entry.database))}.${options.quoteTableName(tableName)}`
|
|
219
|
+
: options.quoteTableName(tableName)
|
|
220
|
+
|
|
221
|
+
return this.options.subquery({connection, quote, table, tenant: entry.tenant})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const selectParts = this.keyColumns.map((keyColumn) => options.quoteColumnName(keyColumn))
|
|
225
|
+
|
|
226
|
+
for (const [name, spec] of Object.entries(this.aggregates)) {
|
|
227
|
+
const aggregateArgument = spec.column === "*" ? "*" : options.quoteColumnName(spec.column)
|
|
228
|
+
|
|
229
|
+
selectParts.push(`${spec.op}(${aggregateArgument}) AS ${options.quoteColumnName(name)}`)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const unionSql = subqueries.map((subquery) => `SELECT * FROM (${subquery}) ${options.quoteTableName(`${DERIVED_TABLE_ALIAS}_source`)}`).join("\nUNION ALL\n")
|
|
233
|
+
const groupBySql = this.keyColumns.length > 0
|
|
234
|
+
? `\nGROUP BY ${this.keyColumns.map((keyColumn) => options.quoteColumnName(keyColumn)).join(", ")}`
|
|
235
|
+
: ""
|
|
236
|
+
|
|
237
|
+
return `SELECT ${selectParts.join(", ")}\nFROM (\n${unionSql}\n) ${options.quoteTableName(DERIVED_TABLE_ALIAS)}${groupBySql}`
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Merges rows from every server by combining each aggregate with its own operation. A `NULL`
|
|
242
|
+
* aggregate value (an empty tenant's `SUM`/`MAX`/`MIN` returns `NULL` on the fan-out path) is
|
|
243
|
+
* treated as "no contribution" and skipped, not coerced to `0` — otherwise an empty tenant would
|
|
244
|
+
* drag a `MAX` of negatives or a `MIN` of positives to `0`. A key whose every tenant contributed
|
|
245
|
+
* `NULL` stays `NULL`, matching SQL aggregate semantics over no rows.
|
|
246
|
+
* @param {Array<Record<string, ?>>} rows - Rows collected from all servers/tenants.
|
|
247
|
+
* @returns {Array<Record<string, ?>>} - One merged row per distinct key-column combination.
|
|
248
|
+
*/
|
|
249
|
+
_mergeRows(rows) {
|
|
250
|
+
/** @type {Map<string, Record<string, ?>>} */
|
|
251
|
+
const merged = new Map()
|
|
252
|
+
|
|
253
|
+
for (const row of rows) {
|
|
254
|
+
const mapKey = JSON.stringify(this.keyColumns.map((keyColumn) => row[keyColumn]))
|
|
255
|
+
let accumulator = merged.get(mapKey)
|
|
256
|
+
|
|
257
|
+
if (!accumulator) {
|
|
258
|
+
accumulator = {}
|
|
259
|
+
|
|
260
|
+
for (const keyColumn of this.keyColumns) {
|
|
261
|
+
accumulator[keyColumn] = row[keyColumn]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const name of Object.keys(this.aggregates)) {
|
|
265
|
+
accumulator[name] = null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
merged.set(mapKey, accumulator)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const [name, spec] of Object.entries(this.aggregates)) {
|
|
272
|
+
const rawValue = row[name]
|
|
273
|
+
|
|
274
|
+
if (rawValue === null || rawValue === undefined) continue
|
|
275
|
+
|
|
276
|
+
const value = this._toExactNumber(rawValue)
|
|
277
|
+
|
|
278
|
+
accumulator[name] = accumulator[name] === null ? value : this._combine(spec.op, accumulator[name], value)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return Array.from(merged.values())
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Combines two non-null per-server aggregate values with the aggregate's own operation.
|
|
287
|
+
* @param {AggregateOperation} op - Aggregate operation.
|
|
288
|
+
* @param {number} current - Accumulated value.
|
|
289
|
+
* @param {number} value - Incoming per-server value.
|
|
290
|
+
* @returns {number} - Combined value.
|
|
291
|
+
*/
|
|
292
|
+
_combine(op, current, value) {
|
|
293
|
+
if (op === "MAX") return Math.max(current, value)
|
|
294
|
+
if (op === "MIN") return Math.min(current, value)
|
|
295
|
+
|
|
296
|
+
return current + value
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Converts a driver-returned aggregate value to a number, failing loudly rather than silently
|
|
301
|
+
* losing precision. Drivers return exact integer aggregates (MySQL `SUM`/`COUNT`, PostgreSQL
|
|
302
|
+
* `bigint`) as strings; an integer beyond `Number.MAX_SAFE_INTEGER` cannot be represented exactly
|
|
303
|
+
* as a JS number, so cross-server merging would corrupt the result — throw instead. (Fractional
|
|
304
|
+
* `DECIMAL`/`NUMERIC` values are still subject to normal floating-point representation.)
|
|
305
|
+
* @param {?} rawValue - Value returned by the driver for an aggregate column.
|
|
306
|
+
* @returns {number} - The value as a number.
|
|
307
|
+
*/
|
|
308
|
+
_toExactNumber(rawValue) {
|
|
309
|
+
const value = Number(rawValue)
|
|
310
|
+
|
|
311
|
+
if (typeof rawValue === "string" && /^-?\d+$/.test(rawValue.trim()) && !Number.isSafeInteger(value)) {
|
|
312
|
+
throw new Error(`Aggregate value ${rawValue} exceeds the safe-integer range and cannot be merged without losing precision.`)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return value
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Builds the value quoter passed to subqueries, with a `.list` helper for `IN (...)` clauses.
|
|
320
|
+
* @param {import("../database/drivers/base.js").default} connection - Driver whose quoting is used.
|
|
321
|
+
* @returns {((value: ?) => string) & {list: (values: Array<?>) => string}} - Value quoter with a `.list` helper.
|
|
322
|
+
*/
|
|
323
|
+
_buildQuote(connection) {
|
|
324
|
+
return Object.assign(
|
|
325
|
+
(/** @type {?} */ value) => String(connection.quote(value)),
|
|
326
|
+
{list: (/** @type {Array<?>} */ values) => values.map((value) => String(connection.quote(value))).join(", ")}
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Normalizes the aggregate specs (string shorthand or object) and validates each operation.
|
|
332
|
+
* @param {Record<string, AggregateSpec>} aggregates - Raw aggregate specs.
|
|
333
|
+
* @returns {Record<string, {op: AggregateOperation, column: string}>} - Normalized aggregate specs.
|
|
334
|
+
*/
|
|
335
|
+
_normalizeAggregates(aggregates) {
|
|
336
|
+
/** @type {Record<string, {op: AggregateOperation, column: string}>} */
|
|
337
|
+
const normalized = {}
|
|
338
|
+
|
|
339
|
+
for (const [name, spec] of Object.entries(aggregates)) {
|
|
340
|
+
const op = /** @type {AggregateOperation} */ ((typeof spec === "string" ? spec : spec.op).toUpperCase())
|
|
341
|
+
|
|
342
|
+
if (!AGGREGATE_OPERATIONS.has(op)) {
|
|
343
|
+
throw new Error(`Unsupported aggregate operation for ${name}: ${op}. Supported: ${Array.from(AGGREGATE_OPERATIONS).join(", ")}.`)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
normalized[name] = {column: typeof spec === "string" ? name : spec.column, op}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return normalized
|
|
350
|
+
}
|
|
351
|
+
}
|
package/build/tenants/tenant.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import Current from "../current.js"
|
|
4
4
|
import {dropTenantDatabase} from "./default-tenant-database-provisioning.js"
|
|
5
|
+
import TenantAggregator from "./tenant-aggregator.js"
|
|
5
6
|
import TenantIterator from "./tenant-iterator.js"
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -74,6 +75,22 @@ export default class Tenant {
|
|
|
74
75
|
})
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Runs one aggregate query across many tenant databases and returns the merged result. The
|
|
80
|
+
* tenants may be co-located on the default server or spread across other servers, and can be
|
|
81
|
+
* created or dropped at runtime; the live tenant list is resolved (from `tenants` or the
|
|
82
|
+
* provider's `listTenants`), grouped by server, aggregated with a single cross-database
|
|
83
|
+
* `UNION ALL` where the driver supports it (MySQL/MSSQL) or one query per tenant otherwise
|
|
84
|
+
* (PostgreSQL/SQLite), and merged with each aggregate's own operation. The caller writes only one
|
|
85
|
+
* per-tenant subquery and declares the key columns and aggregates; see
|
|
86
|
+
* {@link import("./tenant-aggregator.js").TenantAggregateOptions}.
|
|
87
|
+
* @param {import("./tenant-aggregator.js").TenantAggregateOptions} options - Aggregate configuration.
|
|
88
|
+
* @returns {Promise<Array<Record<string, ?>>>} - One merged row per distinct key-column combination.
|
|
89
|
+
*/
|
|
90
|
+
static async aggregateAcross(options) {
|
|
91
|
+
return await new TenantAggregator(options).run()
|
|
92
|
+
}
|
|
93
|
+
|
|
77
94
|
/**
|
|
78
95
|
* Drops one tenant's database/schema through the provider's `dropDatabase` hook.
|
|
79
96
|
* @param {{identifier: string, tenant: object, configuration?: import("../configuration.js").default}} args - Tenant descriptor and database identifier to drop.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../index.js","../bin/velocious.js","../src/application.js","../src/configuration-resolver.js","../src/configuration-types.js","../src/configuration.js","../src/controller.js","../src/current-configuration.js","../src/current.js","../src/error-logger.js","../src/frontend-model-controller.js","../src/initializer.js","../src/logger.js","../src/mailer.js","../src/record-payload-values.js","../src/time-zone.js","../src/velocious-error.js","../src/authorization/ability.js","../src/authorization/base-resource.js","../src/background-jobs/client.js","../src/background-jobs/cron-expression.js","../src/background-jobs/forked-runner-child.js","../src/background-jobs/job-record.js","../src/background-jobs/job-registry.js","../src/background-jobs/job-runner.js","../src/background-jobs/job.js","../src/background-jobs/json-socket.js","../src/background-jobs/main.js","../src/background-jobs/normalize-error.js","../src/background-jobs/scheduler.js","../src/background-jobs/socket-request.js","../src/background-jobs/status-reporter.js","../src/background-jobs/store.js","../src/background-jobs/types.js","../src/background-jobs/worker.js","../src/background-jobs/web/authorization.js","../src/background-jobs/web/controller.js","../src/background-jobs/web/index.js","../src/background-jobs/web/path-matcher.js","../src/background-jobs/web/registry.js","../src/beacon/client.js","../src/beacon/in-process-broker.js","../src/beacon/in-process-client.js","../src/beacon/server.js","../src/beacon/types.js","../src/cli/base-command.js","../src/cli/browser-cli.js","../src/cli/index.js","../src/cli/tenant-database-command-helper.js","../src/cli/use-browser-cli.js","../src/cli/commands/background-jobs-main.js","../src/cli/commands/background-jobs-runner.js","../src/cli/commands/background-jobs-worker.js","../src/cli/commands/beacon.js","../src/cli/commands/console.js","../src/cli/commands/init.js","../src/cli/commands/routes.js","../src/cli/commands/run-script.js","../src/cli/commands/runner.js","../src/cli/commands/server.js","../src/cli/commands/test.js","../src/cli/commands/db/base-command.js","../src/cli/commands/db/create.js","../src/cli/commands/db/drop.js","../src/cli/commands/db/migrate.js","../src/cli/commands/db/reset.js","../src/cli/commands/db/rollback.js","../src/cli/commands/db/seed.js","../src/cli/commands/db/schema/dump.js","../src/cli/commands/db/schema/load.js","../src/cli/commands/db/tenants/check.js","../src/cli/commands/db/tenants/create.js","../src/cli/commands/db/tenants/drop.js","../src/cli/commands/db/tenants/migrate.js","../src/cli/commands/destroy/migration.js","../src/cli/commands/generate/base-models.js","../src/cli/commands/generate/frontend-models.js","../src/cli/commands/generate/migration.js","../src/cli/commands/generate/model.js","../src/cli/commands/lint/relationships.js","../src/database/advisory-lock-runner.js","../src/database/annotations-async-hooks.js","../src/database/annotations.js","../src/database/column-types.js","../src/database/datetime-storage.js","../src/database/handler.js","../src/database/initializer-from-require-context.js","../src/database/live-query.js","../src/database/migrations-ledger.js","../src/database/migrator.js","../src/database/record-changes.js","../src/database/use-database.js","../src/database/use-live-query.js","../src/database/drivers/base-column.js","../src/database/drivers/base-columns-index.js","../src/database/drivers/base-foreign-key.js","../src/database/drivers/base-table.js","../src/database/drivers/base.js","../src/database/drivers/mssql/column.js","../src/database/drivers/mssql/columns-index.js","../src/database/drivers/mssql/connect-connection.js","../src/database/drivers/mssql/foreign-key.js","../src/database/drivers/mssql/index.js","../src/database/drivers/mssql/options.js","../src/database/drivers/mssql/query-parser.js","../src/database/drivers/mssql/structure-sql.js","../src/database/drivers/mssql/table.js","../src/database/drivers/mssql/sql/alter-table.js","../src/database/drivers/mssql/sql/create-database.js","../src/database/drivers/mssql/sql/create-index.js","../src/database/drivers/mssql/sql/create-table.js","../src/database/drivers/mssql/sql/delete.js","../src/database/drivers/mssql/sql/drop-database.js","../src/database/drivers/mssql/sql/drop-table.js","../src/database/drivers/mssql/sql/insert.js","../src/database/drivers/mssql/sql/remove-index.js","../src/database/drivers/mssql/sql/update.js","../src/database/drivers/mssql/sql/upsert.js","../src/database/drivers/mysql/column.js","../src/database/drivers/mysql/columns-index.js","../src/database/drivers/mysql/foreign-key.js","../src/database/drivers/mysql/index.js","../src/database/drivers/mysql/options.js","../src/database/drivers/mysql/query-parser.js","../src/database/drivers/mysql/query.js","../src/database/drivers/mysql/structure-sql.js","../src/database/drivers/mysql/table.js","../src/database/drivers/mysql/sql/alter-table.js","../src/database/drivers/mysql/sql/create-database.js","../src/database/drivers/mysql/sql/create-index.js","../src/database/drivers/mysql/sql/create-table.js","../src/database/drivers/mysql/sql/delete.js","../src/database/drivers/mysql/sql/drop-database.js","../src/database/drivers/mysql/sql/drop-table.js","../src/database/drivers/mysql/sql/insert.js","../src/database/drivers/mysql/sql/remove-index.js","../src/database/drivers/mysql/sql/update.js","../src/database/drivers/mysql/sql/upsert.js","../src/database/drivers/pgsql/column.js","../src/database/drivers/pgsql/columns-index.js","../src/database/drivers/pgsql/foreign-key.js","../src/database/drivers/pgsql/index.js","../src/database/drivers/pgsql/options.js","../src/database/drivers/pgsql/query-parser.js","../src/database/drivers/pgsql/structure-sql.js","../src/database/drivers/pgsql/table.js","../src/database/drivers/pgsql/sql/alter-table.js","../src/database/drivers/pgsql/sql/create-database.js","../src/database/drivers/pgsql/sql/create-index.js","../src/database/drivers/pgsql/sql/create-table.js","../src/database/drivers/pgsql/sql/delete.js","../src/database/drivers/pgsql/sql/drop-database.js","../src/database/drivers/pgsql/sql/drop-table.js","../src/database/drivers/pgsql/sql/insert.js","../src/database/drivers/pgsql/sql/remove-index.js","../src/database/drivers/pgsql/sql/update.js","../src/database/drivers/pgsql/sql/upsert.js","../src/database/drivers/sqlite/base.js","../src/database/drivers/sqlite/column.js","../src/database/drivers/sqlite/columns-index.js","../src/database/drivers/sqlite/connection-sql-js.js","../src/database/drivers/sqlite/foreign-key.js","../src/database/drivers/sqlite/index.js","../src/database/drivers/sqlite/index.native.js","../src/database/drivers/sqlite/index.web.js","../src/database/drivers/sqlite/options.js","../src/database/drivers/sqlite/query-parser.js","../src/database/drivers/sqlite/query.js","../src/database/drivers/sqlite/query.native.js","../src/database/drivers/sqlite/query.web.js","../src/database/drivers/sqlite/structure-sql.js","../src/database/drivers/sqlite/table-rebuilder.js","../src/database/drivers/sqlite/table.js","../src/database/drivers/sqlite/web-persistence.js","../src/database/drivers/sqlite/sql/alter-table.js","../src/database/drivers/sqlite/sql/create-index.js","../src/database/drivers/sqlite/sql/create-table.js","../src/database/drivers/sqlite/sql/delete.js","../src/database/drivers/sqlite/sql/drop-table.js","../src/database/drivers/sqlite/sql/insert.js","../src/database/drivers/sqlite/sql/remove-index.js","../src/database/drivers/sqlite/sql/update.js","../src/database/drivers/sqlite/sql/upsert.js","../src/database/drivers/structure-sql/utils.js","../src/database/migration/index.js","../src/database/migrator/files-finder.js","../src/database/migrator/types.js","../src/database/pool/async-tracked-multi-connection.js","../src/database/pool/base-methods-forward.js","../src/database/pool/base.js","../src/database/pool/single-multi-use.js","../src/database/query/alter-table-base.js","../src/database/query/base.js","../src/database/query/create-database-base.js","../src/database/query/create-index-base.js","../src/database/query/create-table-base.js","../src/database/query/delete-base.js","../src/database/query/drop-database-base.js","../src/database/query/drop-table-base.js","../src/database/query/from-base.js","../src/database/query/from-plain.js","../src/database/query/from-table.js","../src/database/query/index.js","../src/database/query/insert-base.js","../src/database/query/join-base.js","../src/database/query/join-object.js","../src/database/query/join-plain.js","../src/database/query/join-tracker.js","../src/database/query/model-class-query.js","../src/database/query/order-base.js","../src/database/query/order-column.js","../src/database/query/order-plain.js","../src/database/query/preloader.js","../src/database/query/query-data.js","../src/database/query/remove-index-base.js","../src/database/query/select-base.js","../src/database/query/select-plain.js","../src/database/query/select-table-and-column.js","../src/database/query/update-base.js","../src/database/query/upsert-base.js","../src/database/query/where-base.js","../src/database/query/where-combinator.js","../src/database/query/where-hash.js","../src/database/query/where-model-class-hash.js","../src/database/query/where-not.js","../src/database/query/where-plain.js","../src/database/query/with-count.js","../src/database/query/preloader/belongs-to.js","../src/database/query/preloader/ensure-model-class-initialized.js","../src/database/query/preloader/has-many.js","../src/database/query/preloader/has-one.js","../src/database/query/preloader/selection.js","../src/database/query-parser/base-query-parser.js","../src/database/query-parser/from-parser.js","../src/database/query-parser/group-parser.js","../src/database/query-parser/joins-parser.js","../src/database/query-parser/limit-parser.js","../src/database/query-parser/options.js","../src/database/query-parser/order-parser.js","../src/database/query-parser/select-parser.js","../src/database/query-parser/where-parser.js","../src/database/record/acts-as-list.js","../src/database/record/auditing.js","../src/database/record/counter-cache-magnitude.js","../src/database/record/index.js","../src/database/record/record-not-found-error.js","../src/database/record/state-machine.js","../src/database/record/user-module.js","../src/database/record/validation-messages.js","../src/database/record/attachments/attachment-record.js","../src/database/record/attachments/download.js","../src/database/record/attachments/handle.js","../src/database/record/attachments/normalize-input.js","../src/database/record/attachments/store.js","../src/database/record/attachments/storage-drivers/filesystem.js","../src/database/record/attachments/storage-drivers/native.js","../src/database/record/attachments/storage-drivers/s3.js","../src/database/record/instance-relationships/base.js","../src/database/record/instance-relationships/belongs-to.js","../src/database/record/instance-relationships/has-many.js","../src/database/record/instance-relationships/has-one.js","../src/database/record/relationships/base.js","../src/database/record/relationships/belongs-to.js","../src/database/record/relationships/has-many.js","../src/database/record/relationships/has-one.js","../src/database/record/validators/base.js","../src/database/record/validators/format.js","../src/database/record/validators/length.js","../src/database/record/validators/presence.js","../src/database/record/validators/uniqueness.js","../src/database/table-data/index.js","../src/database/table-data/table-column.js","../src/database/table-data/table-foreign-key.js","../src/database/table-data/table-index.js","../src/database/table-data/table-reference.js","../src/database/tenants/data-copier.js","../src/database/tenants/schema-cloner.js","../src/database/tenants/tenant-table-plan.js","../src/environment-handlers/base.js","../src/environment-handlers/browser.js","../src/environment-handlers/node.js","../src/environment-handlers/node/cli/commands/background-jobs-main.js","../src/environment-handlers/node/cli/commands/background-jobs-runner.js","../src/environment-handlers/node/cli/commands/background-jobs-worker.js","../src/environment-handlers/node/cli/commands/beacon.js","../src/environment-handlers/node/cli/commands/cli-command-context.js","../src/environment-handlers/node/cli/commands/console.js","../src/environment-handlers/node/cli/commands/init.js","../src/environment-handlers/node/cli/commands/routes.js","../src/environment-handlers/node/cli/commands/run-script.js","../src/environment-handlers/node/cli/commands/runner.js","../src/environment-handlers/node/cli/commands/server.js","../src/environment-handlers/node/cli/commands/test.js","../src/environment-handlers/node/cli/commands/db/seed.js","../src/environment-handlers/node/cli/commands/db/schema/dump.js","../src/environment-handlers/node/cli/commands/db/schema/load.js","../src/environment-handlers/node/cli/commands/destroy/migration.js","../src/environment-handlers/node/cli/commands/generate/base-models.js","../src/environment-handlers/node/cli/commands/generate/frontend-models.js","../src/environment-handlers/node/cli/commands/generate/generated-file-banner.js","../src/environment-handlers/node/cli/commands/generate/migration.js","../src/environment-handlers/node/cli/commands/generate/model.js","../src/environment-handlers/node/cli/commands/lint/relationships.js","../src/error-reporting/request-details.js","../src/frontend-model-resource/base-resource.js","../src/frontend-model-resource/velocious-attachment-resource.js","../src/frontend-models/base.js","../src/frontend-models/built-in-resources.js","../src/frontend-models/clear-pending-debounced-callback.js","../src/frontend-models/event-hook-models.js","../src/frontend-models/model-registry.js","../src/frontend-models/outgoing-event-buffer.js","../src/frontend-models/preloader.js","../src/frontend-models/query.js","../src/frontend-models/resource-config-validation.js","../src/frontend-models/resource-definition.js","../src/frontend-models/transport-serialization.js","../src/frontend-models/use-created-event.js","../src/frontend-models/use-destroyed-event.js","../src/frontend-models/use-model-class-event.js","../src/frontend-models/use-updated-event.js","../src/frontend-models/websocket-channel.js","../src/frontend-models/websocket-publishers.js","../src/http-client/header.js","../src/http-client/index.js","../src/http-client/request.js","../src/http-client/response.js","../src/http-client/websocket-client.js","../src/http-server/cookie.js","../src/http-server/development-reloader.js","../src/http-server/index.js","../src/http-server/remote-address.js","../src/http-server/server-client.js","../src/http-server/server-lock.js","../src/http-server/websocket-channel-subscribers.js","../src/http-server/websocket-channel.js","../src/http-server/websocket-connection.js","../src/http-server/websocket-event-log-store.js","../src/http-server/websocket-events-host.js","../src/http-server/websocket-events.js","../src/http-server/client/index.js","../src/http-server/client/params-to-object.js","../src/http-server/client/request-parser.js","../src/http-server/client/request-runner.js","../src/http-server/client/request-timing.js","../src/http-server/client/request.js","../src/http-server/client/response.js","../src/http-server/client/websocket-request.js","../src/http-server/client/websocket-session.js","../src/http-server/client/request-buffer/form-data-part.js","../src/http-server/client/request-buffer/header.js","../src/http-server/client/request-buffer/index.js","../src/http-server/client/uploaded-file/memory-uploaded-file.js","../src/http-server/client/uploaded-file/temporary-uploaded-file.js","../src/http-server/client/uploaded-file/uploaded-file.js","../src/http-server/worker-handler/channel-subscriber-dispatch.js","../src/http-server/worker-handler/in-process.js","../src/http-server/worker-handler/index.js","../src/http-server/worker-handler/worker-script.js","../src/http-server/worker-handler/worker-thread.js","../src/jobs/mail-delivery.js","../src/jobs/prune-terminal-background-jobs.js","../src/logger/base-logger.js","../src/logger/console-logger.js","../src/logger/file-logger.js","../src/logger/outputs/array-output.js","../src/logger/outputs/console-output.js","../src/logger/outputs/file-output.js","../src/logger/outputs/stdout-output.js","../src/mailer/base.js","../src/mailer/delivery.js","../src/mailer/index.js","../src/mailer/backends/smtp.js","../src/packages/velocious-package.js","../src/plugins/sqljs-wasm-route-controller.js","../src/plugins/sqljs-wasm-route.js","../src/routes/app-routes.js","../src/routes/base-route.js","../src/routes/basic-route.js","../src/routes/get-route.js","../src/routes/index.js","../src/routes/namespace-route.js","../src/routes/plugin-routes.js","../src/routes/post-route.js","../src/routes/resolver.js","../src/routes/resource-route.js","../src/routes/root-route.js","../src/routes/built-in/api-manifest/controller.js","../src/routes/built-in/debug/controller.js","../src/routes/built-in/errors/controller.js","../src/routes/hooks/frontend-model-command-route-hook.js","../src/sync/conflict-strategy.js","../src/sync/device-identity.js","../src/sync/local-mutation-log.js","../src/sync/offline-grant.js","../src/sync/peer-mutation-bundle.js","../src/sync/query-scope.js","../src/sync/server-change-feed.js","../src/sync/server-sequence-allocator.js","../src/sync/stable-json.js","../src/sync/sync-api-client-types.js","../src/sync/sync-api-client.js","../src/sync/sync-api-controller.js","../src/sync/sync-change-fanout.js","../src/sync/sync-channel-name.js","../src/sync/sync-client-registry.js","../src/sync/sync-client-types.js","../src/sync/sync-client.js","../src/sync/sync-envelope-replay-service.js","../src/sync/sync-model-change-feed-service.js","../src/sync/sync-publish-suppression.js","../src/sync/sync-publisher-types.js","../src/sync/sync-publisher.js","../src/sync/sync-realtime-bridge.js","../src/sync/sync-replay-upsert-applier.js","../src/sync/sync-resource-base.js","../src/sync/sync-scope-attributes.js","../src/sync/sync-scope-store.js","../src/sync/sync-websocket-channel.js","../src/tenants/default-tenant-database-provisioning.js","../src/tenants/tenant-iterator.js","../src/tenants/tenant.js","../src/testing/base-expect.js","../src/testing/browser-frontend-model-event-hook-scenarios.js","../src/testing/browser-test-app.js","../src/testing/expect-to-change.js","../src/testing/expect-utils.js","../src/testing/expect.js","../src/testing/request-client.js","../src/testing/test-files-finder.js","../src/testing/test-filter-parser.js","../src/testing/test-runner.js","../src/testing/test-suite-splitter.js","../src/testing/test.js","../src/types/external-modules.d.ts","../src/utils/backtrace-cleaner-node.js","../src/utils/backtrace-cleaner.js","../src/utils/deburr-column-name.js","../src/utils/event-emitter.js","../src/utils/file-exists.js","../src/utils/format-value.js","../src/utils/is-date.js","../src/utils/model-scope.js","../src/utils/nest-callbacks.js","../src/utils/plain-object.js","../src/utils/ransack.js","../src/utils/rest-args-error.js","../src/utils/singularize-model-name.js","../src/utils/split-sql-statements.js","../src/utils/to-import-specifier.js","../src/utils/with-tracked-stack-async-hooks.js","../src/utils/with-tracked-stack.js"],"version":"6.0.3"}
|
|
1
|
+
{"root":["../index.js","../bin/velocious.js","../src/application.js","../src/configuration-resolver.js","../src/configuration-types.js","../src/configuration.js","../src/controller.js","../src/current-configuration.js","../src/current.js","../src/error-logger.js","../src/frontend-model-controller.js","../src/initializer.js","../src/logger.js","../src/mailer.js","../src/record-payload-values.js","../src/time-zone.js","../src/velocious-error.js","../src/authorization/ability.js","../src/authorization/base-resource.js","../src/background-jobs/client.js","../src/background-jobs/cron-expression.js","../src/background-jobs/forked-runner-child.js","../src/background-jobs/job-record.js","../src/background-jobs/job-registry.js","../src/background-jobs/job-runner.js","../src/background-jobs/job.js","../src/background-jobs/json-socket.js","../src/background-jobs/main.js","../src/background-jobs/normalize-error.js","../src/background-jobs/scheduler.js","../src/background-jobs/socket-request.js","../src/background-jobs/status-reporter.js","../src/background-jobs/store.js","../src/background-jobs/types.js","../src/background-jobs/worker.js","../src/background-jobs/web/authorization.js","../src/background-jobs/web/controller.js","../src/background-jobs/web/index.js","../src/background-jobs/web/path-matcher.js","../src/background-jobs/web/registry.js","../src/beacon/client.js","../src/beacon/in-process-broker.js","../src/beacon/in-process-client.js","../src/beacon/server.js","../src/beacon/types.js","../src/cli/base-command.js","../src/cli/browser-cli.js","../src/cli/index.js","../src/cli/tenant-database-command-helper.js","../src/cli/use-browser-cli.js","../src/cli/commands/background-jobs-main.js","../src/cli/commands/background-jobs-runner.js","../src/cli/commands/background-jobs-worker.js","../src/cli/commands/beacon.js","../src/cli/commands/console.js","../src/cli/commands/init.js","../src/cli/commands/routes.js","../src/cli/commands/run-script.js","../src/cli/commands/runner.js","../src/cli/commands/server.js","../src/cli/commands/test.js","../src/cli/commands/db/base-command.js","../src/cli/commands/db/create.js","../src/cli/commands/db/drop.js","../src/cli/commands/db/migrate.js","../src/cli/commands/db/reset.js","../src/cli/commands/db/rollback.js","../src/cli/commands/db/seed.js","../src/cli/commands/db/schema/dump.js","../src/cli/commands/db/schema/load.js","../src/cli/commands/db/tenants/check.js","../src/cli/commands/db/tenants/create.js","../src/cli/commands/db/tenants/drop.js","../src/cli/commands/db/tenants/migrate.js","../src/cli/commands/destroy/migration.js","../src/cli/commands/generate/base-models.js","../src/cli/commands/generate/frontend-models.js","../src/cli/commands/generate/migration.js","../src/cli/commands/generate/model.js","../src/cli/commands/lint/relationships.js","../src/database/advisory-lock-runner.js","../src/database/annotations-async-hooks.js","../src/database/annotations.js","../src/database/column-types.js","../src/database/datetime-storage.js","../src/database/handler.js","../src/database/initializer-from-require-context.js","../src/database/live-query.js","../src/database/migrations-ledger.js","../src/database/migrator.js","../src/database/record-changes.js","../src/database/use-database.js","../src/database/use-live-query.js","../src/database/drivers/base-column.js","../src/database/drivers/base-columns-index.js","../src/database/drivers/base-foreign-key.js","../src/database/drivers/base-table.js","../src/database/drivers/base.js","../src/database/drivers/mssql/column.js","../src/database/drivers/mssql/columns-index.js","../src/database/drivers/mssql/connect-connection.js","../src/database/drivers/mssql/foreign-key.js","../src/database/drivers/mssql/index.js","../src/database/drivers/mssql/options.js","../src/database/drivers/mssql/query-parser.js","../src/database/drivers/mssql/structure-sql.js","../src/database/drivers/mssql/table.js","../src/database/drivers/mssql/sql/alter-table.js","../src/database/drivers/mssql/sql/create-database.js","../src/database/drivers/mssql/sql/create-index.js","../src/database/drivers/mssql/sql/create-table.js","../src/database/drivers/mssql/sql/delete.js","../src/database/drivers/mssql/sql/drop-database.js","../src/database/drivers/mssql/sql/drop-table.js","../src/database/drivers/mssql/sql/insert.js","../src/database/drivers/mssql/sql/remove-index.js","../src/database/drivers/mssql/sql/update.js","../src/database/drivers/mssql/sql/upsert.js","../src/database/drivers/mysql/column.js","../src/database/drivers/mysql/columns-index.js","../src/database/drivers/mysql/foreign-key.js","../src/database/drivers/mysql/index.js","../src/database/drivers/mysql/options.js","../src/database/drivers/mysql/query-parser.js","../src/database/drivers/mysql/query.js","../src/database/drivers/mysql/structure-sql.js","../src/database/drivers/mysql/table.js","../src/database/drivers/mysql/sql/alter-table.js","../src/database/drivers/mysql/sql/create-database.js","../src/database/drivers/mysql/sql/create-index.js","../src/database/drivers/mysql/sql/create-table.js","../src/database/drivers/mysql/sql/delete.js","../src/database/drivers/mysql/sql/drop-database.js","../src/database/drivers/mysql/sql/drop-table.js","../src/database/drivers/mysql/sql/insert.js","../src/database/drivers/mysql/sql/remove-index.js","../src/database/drivers/mysql/sql/update.js","../src/database/drivers/mysql/sql/upsert.js","../src/database/drivers/pgsql/column.js","../src/database/drivers/pgsql/columns-index.js","../src/database/drivers/pgsql/foreign-key.js","../src/database/drivers/pgsql/index.js","../src/database/drivers/pgsql/options.js","../src/database/drivers/pgsql/query-parser.js","../src/database/drivers/pgsql/structure-sql.js","../src/database/drivers/pgsql/table.js","../src/database/drivers/pgsql/sql/alter-table.js","../src/database/drivers/pgsql/sql/create-database.js","../src/database/drivers/pgsql/sql/create-index.js","../src/database/drivers/pgsql/sql/create-table.js","../src/database/drivers/pgsql/sql/delete.js","../src/database/drivers/pgsql/sql/drop-database.js","../src/database/drivers/pgsql/sql/drop-table.js","../src/database/drivers/pgsql/sql/insert.js","../src/database/drivers/pgsql/sql/remove-index.js","../src/database/drivers/pgsql/sql/update.js","../src/database/drivers/pgsql/sql/upsert.js","../src/database/drivers/sqlite/base.js","../src/database/drivers/sqlite/column.js","../src/database/drivers/sqlite/columns-index.js","../src/database/drivers/sqlite/connection-sql-js.js","../src/database/drivers/sqlite/foreign-key.js","../src/database/drivers/sqlite/index.js","../src/database/drivers/sqlite/index.native.js","../src/database/drivers/sqlite/index.web.js","../src/database/drivers/sqlite/options.js","../src/database/drivers/sqlite/query-parser.js","../src/database/drivers/sqlite/query.js","../src/database/drivers/sqlite/query.native.js","../src/database/drivers/sqlite/query.web.js","../src/database/drivers/sqlite/structure-sql.js","../src/database/drivers/sqlite/table-rebuilder.js","../src/database/drivers/sqlite/table.js","../src/database/drivers/sqlite/web-persistence.js","../src/database/drivers/sqlite/sql/alter-table.js","../src/database/drivers/sqlite/sql/create-index.js","../src/database/drivers/sqlite/sql/create-table.js","../src/database/drivers/sqlite/sql/delete.js","../src/database/drivers/sqlite/sql/drop-table.js","../src/database/drivers/sqlite/sql/insert.js","../src/database/drivers/sqlite/sql/remove-index.js","../src/database/drivers/sqlite/sql/update.js","../src/database/drivers/sqlite/sql/upsert.js","../src/database/drivers/structure-sql/utils.js","../src/database/migration/index.js","../src/database/migrator/files-finder.js","../src/database/migrator/types.js","../src/database/pool/async-tracked-multi-connection.js","../src/database/pool/base-methods-forward.js","../src/database/pool/base.js","../src/database/pool/single-multi-use.js","../src/database/query/alter-table-base.js","../src/database/query/base.js","../src/database/query/create-database-base.js","../src/database/query/create-index-base.js","../src/database/query/create-table-base.js","../src/database/query/delete-base.js","../src/database/query/drop-database-base.js","../src/database/query/drop-table-base.js","../src/database/query/from-base.js","../src/database/query/from-plain.js","../src/database/query/from-table.js","../src/database/query/index.js","../src/database/query/insert-base.js","../src/database/query/join-base.js","../src/database/query/join-object.js","../src/database/query/join-plain.js","../src/database/query/join-tracker.js","../src/database/query/model-class-query.js","../src/database/query/order-base.js","../src/database/query/order-column.js","../src/database/query/order-plain.js","../src/database/query/preloader.js","../src/database/query/query-data.js","../src/database/query/remove-index-base.js","../src/database/query/select-base.js","../src/database/query/select-plain.js","../src/database/query/select-table-and-column.js","../src/database/query/update-base.js","../src/database/query/upsert-base.js","../src/database/query/where-base.js","../src/database/query/where-combinator.js","../src/database/query/where-hash.js","../src/database/query/where-model-class-hash.js","../src/database/query/where-not.js","../src/database/query/where-plain.js","../src/database/query/with-count.js","../src/database/query/preloader/belongs-to.js","../src/database/query/preloader/ensure-model-class-initialized.js","../src/database/query/preloader/has-many.js","../src/database/query/preloader/has-one.js","../src/database/query/preloader/selection.js","../src/database/query-parser/base-query-parser.js","../src/database/query-parser/from-parser.js","../src/database/query-parser/group-parser.js","../src/database/query-parser/joins-parser.js","../src/database/query-parser/limit-parser.js","../src/database/query-parser/options.js","../src/database/query-parser/order-parser.js","../src/database/query-parser/select-parser.js","../src/database/query-parser/where-parser.js","../src/database/record/acts-as-list.js","../src/database/record/auditing.js","../src/database/record/counter-cache-magnitude.js","../src/database/record/index.js","../src/database/record/record-not-found-error.js","../src/database/record/state-machine.js","../src/database/record/user-module.js","../src/database/record/validation-messages.js","../src/database/record/attachments/attachment-record.js","../src/database/record/attachments/download.js","../src/database/record/attachments/handle.js","../src/database/record/attachments/normalize-input.js","../src/database/record/attachments/store.js","../src/database/record/attachments/storage-drivers/filesystem.js","../src/database/record/attachments/storage-drivers/native.js","../src/database/record/attachments/storage-drivers/s3.js","../src/database/record/instance-relationships/base.js","../src/database/record/instance-relationships/belongs-to.js","../src/database/record/instance-relationships/has-many.js","../src/database/record/instance-relationships/has-one.js","../src/database/record/relationships/base.js","../src/database/record/relationships/belongs-to.js","../src/database/record/relationships/has-many.js","../src/database/record/relationships/has-one.js","../src/database/record/validators/base.js","../src/database/record/validators/format.js","../src/database/record/validators/length.js","../src/database/record/validators/presence.js","../src/database/record/validators/uniqueness.js","../src/database/table-data/index.js","../src/database/table-data/table-column.js","../src/database/table-data/table-foreign-key.js","../src/database/table-data/table-index.js","../src/database/table-data/table-reference.js","../src/database/tenants/data-copier.js","../src/database/tenants/schema-cloner.js","../src/database/tenants/tenant-table-plan.js","../src/environment-handlers/base.js","../src/environment-handlers/browser.js","../src/environment-handlers/node.js","../src/environment-handlers/node/cli/commands/background-jobs-main.js","../src/environment-handlers/node/cli/commands/background-jobs-runner.js","../src/environment-handlers/node/cli/commands/background-jobs-worker.js","../src/environment-handlers/node/cli/commands/beacon.js","../src/environment-handlers/node/cli/commands/cli-command-context.js","../src/environment-handlers/node/cli/commands/console.js","../src/environment-handlers/node/cli/commands/init.js","../src/environment-handlers/node/cli/commands/routes.js","../src/environment-handlers/node/cli/commands/run-script.js","../src/environment-handlers/node/cli/commands/runner.js","../src/environment-handlers/node/cli/commands/server.js","../src/environment-handlers/node/cli/commands/test.js","../src/environment-handlers/node/cli/commands/db/seed.js","../src/environment-handlers/node/cli/commands/db/schema/dump.js","../src/environment-handlers/node/cli/commands/db/schema/load.js","../src/environment-handlers/node/cli/commands/destroy/migration.js","../src/environment-handlers/node/cli/commands/generate/base-models.js","../src/environment-handlers/node/cli/commands/generate/frontend-models.js","../src/environment-handlers/node/cli/commands/generate/generated-file-banner.js","../src/environment-handlers/node/cli/commands/generate/migration.js","../src/environment-handlers/node/cli/commands/generate/model.js","../src/environment-handlers/node/cli/commands/lint/relationships.js","../src/error-reporting/request-details.js","../src/frontend-model-resource/base-resource.js","../src/frontend-model-resource/velocious-attachment-resource.js","../src/frontend-models/base.js","../src/frontend-models/built-in-resources.js","../src/frontend-models/clear-pending-debounced-callback.js","../src/frontend-models/event-hook-models.js","../src/frontend-models/model-registry.js","../src/frontend-models/outgoing-event-buffer.js","../src/frontend-models/preloader.js","../src/frontend-models/query.js","../src/frontend-models/resource-config-validation.js","../src/frontend-models/resource-definition.js","../src/frontend-models/transport-serialization.js","../src/frontend-models/use-created-event.js","../src/frontend-models/use-destroyed-event.js","../src/frontend-models/use-model-class-event.js","../src/frontend-models/use-updated-event.js","../src/frontend-models/websocket-channel.js","../src/frontend-models/websocket-publishers.js","../src/http-client/header.js","../src/http-client/index.js","../src/http-client/request.js","../src/http-client/response.js","../src/http-client/websocket-client.js","../src/http-server/cookie.js","../src/http-server/development-reloader.js","../src/http-server/index.js","../src/http-server/remote-address.js","../src/http-server/server-client.js","../src/http-server/server-lock.js","../src/http-server/websocket-channel-subscribers.js","../src/http-server/websocket-channel.js","../src/http-server/websocket-connection.js","../src/http-server/websocket-event-log-store.js","../src/http-server/websocket-events-host.js","../src/http-server/websocket-events.js","../src/http-server/client/index.js","../src/http-server/client/params-to-object.js","../src/http-server/client/request-parser.js","../src/http-server/client/request-runner.js","../src/http-server/client/request-timing.js","../src/http-server/client/request.js","../src/http-server/client/response.js","../src/http-server/client/websocket-request.js","../src/http-server/client/websocket-session.js","../src/http-server/client/request-buffer/form-data-part.js","../src/http-server/client/request-buffer/header.js","../src/http-server/client/request-buffer/index.js","../src/http-server/client/uploaded-file/memory-uploaded-file.js","../src/http-server/client/uploaded-file/temporary-uploaded-file.js","../src/http-server/client/uploaded-file/uploaded-file.js","../src/http-server/worker-handler/channel-subscriber-dispatch.js","../src/http-server/worker-handler/in-process.js","../src/http-server/worker-handler/index.js","../src/http-server/worker-handler/worker-script.js","../src/http-server/worker-handler/worker-thread.js","../src/jobs/mail-delivery.js","../src/jobs/prune-terminal-background-jobs.js","../src/logger/base-logger.js","../src/logger/console-logger.js","../src/logger/file-logger.js","../src/logger/outputs/array-output.js","../src/logger/outputs/console-output.js","../src/logger/outputs/file-output.js","../src/logger/outputs/stdout-output.js","../src/mailer/base.js","../src/mailer/delivery.js","../src/mailer/index.js","../src/mailer/backends/smtp.js","../src/packages/velocious-package.js","../src/plugins/sqljs-wasm-route-controller.js","../src/plugins/sqljs-wasm-route.js","../src/routes/app-routes.js","../src/routes/base-route.js","../src/routes/basic-route.js","../src/routes/get-route.js","../src/routes/index.js","../src/routes/namespace-route.js","../src/routes/plugin-routes.js","../src/routes/post-route.js","../src/routes/resolver.js","../src/routes/resource-route.js","../src/routes/root-route.js","../src/routes/built-in/api-manifest/controller.js","../src/routes/built-in/debug/controller.js","../src/routes/built-in/errors/controller.js","../src/routes/hooks/frontend-model-command-route-hook.js","../src/sync/conflict-strategy.js","../src/sync/device-identity.js","../src/sync/local-mutation-log.js","../src/sync/offline-grant.js","../src/sync/peer-mutation-bundle.js","../src/sync/query-scope.js","../src/sync/server-change-feed.js","../src/sync/server-sequence-allocator.js","../src/sync/stable-json.js","../src/sync/sync-api-client-types.js","../src/sync/sync-api-client.js","../src/sync/sync-api-controller.js","../src/sync/sync-change-fanout.js","../src/sync/sync-channel-name.js","../src/sync/sync-client-registry.js","../src/sync/sync-client-types.js","../src/sync/sync-client.js","../src/sync/sync-envelope-replay-service.js","../src/sync/sync-model-change-feed-service.js","../src/sync/sync-publish-suppression.js","../src/sync/sync-publisher-types.js","../src/sync/sync-publisher.js","../src/sync/sync-realtime-bridge.js","../src/sync/sync-replay-upsert-applier.js","../src/sync/sync-resource-base.js","../src/sync/sync-scope-attributes.js","../src/sync/sync-scope-store.js","../src/sync/sync-websocket-channel.js","../src/tenants/default-tenant-database-provisioning.js","../src/tenants/tenant-aggregator.js","../src/tenants/tenant-iterator.js","../src/tenants/tenant.js","../src/testing/base-expect.js","../src/testing/browser-frontend-model-event-hook-scenarios.js","../src/testing/browser-test-app.js","../src/testing/expect-to-change.js","../src/testing/expect-utils.js","../src/testing/expect.js","../src/testing/request-client.js","../src/testing/test-files-finder.js","../src/testing/test-filter-parser.js","../src/testing/test-runner.js","../src/testing/test-suite-splitter.js","../src/testing/test.js","../src/types/external-modules.d.ts","../src/utils/backtrace-cleaner-node.js","../src/utils/backtrace-cleaner.js","../src/utils/deburr-column-name.js","../src/utils/event-emitter.js","../src/utils/file-exists.js","../src/utils/format-value.js","../src/utils/is-date.js","../src/utils/model-scope.js","../src/utils/nest-callbacks.js","../src/utils/plain-object.js","../src/utils/ransack.js","../src/utils/rest-args-error.js","../src/utils/singularize-model-name.js","../src/utils/split-sql-statements.js","../src/utils/to-import-specifier.js","../src/utils/with-tracked-stack-async-hooks.js","../src/utils/with-tracked-stack.js"],"version":"6.0.3"}
|
package/package.json
CHANGED
|
@@ -225,6 +225,12 @@ export default class BackgroundJobsStore {
|
|
|
225
225
|
query = query.where({execution_mode: executionModes})
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
if (scheduledAtOperator === "<=") {
|
|
229
|
+
const priorityOrder = this._queuePriorityOrderSql(db)
|
|
230
|
+
|
|
231
|
+
if (priorityOrder) query = query.order(`${priorityOrder} DESC`)
|
|
232
|
+
}
|
|
233
|
+
|
|
228
234
|
query = query
|
|
229
235
|
.order("scheduled_at_ms ASC")
|
|
230
236
|
.order("created_at_ms ASC")
|
|
@@ -238,6 +244,40 @@ export default class BackgroundJobsStore {
|
|
|
238
244
|
return this._normalizeJobRow(row)
|
|
239
245
|
}
|
|
240
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Builds a raw SQL ORDER BY expression ranking queued jobs by their queue's
|
|
249
|
+
* configured priority (`backgroundJobs.queues[queue].priority`, default `0`),
|
|
250
|
+
* so the dispatcher picks higher-priority queues first regardless of enqueue
|
|
251
|
+
* order. Only applied to the dispatch path (`scheduledAtOperator === "<="`);
|
|
252
|
+
* the future-scheduled lookup must stay strictly time-ordered. Composes with
|
|
253
|
+
* the concurrency EXISTS filter: a higher-priority queue already at its cap is
|
|
254
|
+
* filtered out, so dispatch falls through to the next eligible lower-priority
|
|
255
|
+
* job. Returns null when no queue configures a non-zero priority so the plain
|
|
256
|
+
* FIFO ordering is left untouched (and no needless filesort is introduced).
|
|
257
|
+
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
258
|
+
* @returns {string | null} - Raw SQL CASE expression, or null when no queue is prioritized.
|
|
259
|
+
*/
|
|
260
|
+
_queuePriorityOrderSql(db) {
|
|
261
|
+
const queues = this.configuration.getBackgroundJobsConfig().queues || {}
|
|
262
|
+
/** @type {Array<[string, number]>} */
|
|
263
|
+
const prioritized = []
|
|
264
|
+
|
|
265
|
+
for (const [queue, queueConfig] of Object.entries(queues)) {
|
|
266
|
+
const priority = queueConfig?.priority
|
|
267
|
+
|
|
268
|
+
if (Number.isFinite(priority) && Number(priority) !== 0) prioritized.push([queue, Number(priority)])
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (prioritized.length === 0) return null
|
|
272
|
+
|
|
273
|
+
const queueColumn = db.quoteColumn("queue")
|
|
274
|
+
const whens = prioritized
|
|
275
|
+
.map(([queue, priority]) => `WHEN ${db.quote(queue)} THEN ${priority}`)
|
|
276
|
+
.join(" ")
|
|
277
|
+
|
|
278
|
+
return `CASE COALESCE(${queueColumn}, ${db.quote(DEFAULT_QUEUE)}) ${whens} ELSE 0 END`
|
|
279
|
+
}
|
|
280
|
+
|
|
241
281
|
/**
|
|
242
282
|
* Runs get job.
|
|
243
283
|
* @param {string} jobId - Job id.
|
|
@@ -164,15 +164,29 @@
|
|
|
164
164
|
* allowed to keep in flight. Default: `4`. This is a per-worker safety cap;
|
|
165
165
|
* for workload-shaped limits use per-queue caps (`queues`) instead, which are
|
|
166
166
|
* enforced cluster-wide and are immune to duplicate worker processes.
|
|
167
|
-
* @property {Record<string, {maxConcurrent?: number}>} [queues] - Per-queue
|
|
168
|
-
* concurrency caps, Sidekiq-style. A job declares its queue (static
|
|
169
|
-
* the job class, or the `queue` enqueue option; defaults to `"default"`), and
|
|
167
|
+
* @property {Record<string, {maxConcurrent?: number, priority?: number}>} [queues] - Per-queue
|
|
168
|
+
* concurrency caps and dispatch priorities, Sidekiq-style. A job declares its queue (static
|
|
169
|
+
* `queue` on the job class, or the `queue` enqueue option; defaults to `"default"`), and
|
|
170
170
|
* `queues[name].maxConcurrent` bounds how many jobs from that queue may be
|
|
171
171
|
* in flight across the whole cluster (enforced via durable per-key
|
|
172
172
|
* concurrency, so it holds regardless of how many worker processes run).
|
|
173
173
|
* Size each queue to its workload: I/O-bound queues (e.g. build runners
|
|
174
174
|
* waiting on remote Docker servers) can run far above the core count, while
|
|
175
|
-
* CPU-bound queues should stay near it.
|
|
175
|
+
* CPU-bound queues should stay near it. `queues[name].priority` (default `0`)
|
|
176
|
+
* makes the main process dispatch higher-priority queues before lower-priority
|
|
177
|
+
* ones regardless of enqueue order, so a small time-critical queue can never be
|
|
178
|
+
* starved by a flood of low-priority work sharing a worker pool. Unlike
|
|
179
|
+
* Sidekiq's strict queue ordering, priority composes with the per-queue caps:
|
|
180
|
+
* a higher-priority queue that is already at its `maxConcurrent` is skipped and
|
|
181
|
+
* dispatch falls through to the next eligible lower-priority job, so a busy
|
|
182
|
+
* high-priority queue does not block everything else. This fallthrough is a
|
|
183
|
+
* property of the queue-derived cap; a job that supplies its own explicit
|
|
184
|
+
* `concurrencyKey`/`maxConcurrency` bypasses the queue cap entirely (an
|
|
185
|
+
* explicit key always wins — see above) and is bounded only by that key, so it
|
|
186
|
+
* is not held back by the queue's cap and priority simply orders it normally.
|
|
187
|
+
* Priorities may be any number, including negative to sink a queue below the
|
|
188
|
+
* default. Jobs within the same priority keep FIFO (`scheduled_at`, then
|
|
189
|
+
* `created_at`) order. Default: `{}` (no queue caps, all queues priority `0`).
|
|
176
190
|
* @property {BackgroundJobsDispatchStrategy} [dispatchStrategy] - How the main process
|
|
177
191
|
* detects new work. Defaults to `"beacon"` (event-driven). Set to `"polling"`
|
|
178
192
|
* to restore the legacy fixed-interval poll.
|