@getstrata/core 2.2.28 → 2.2.30
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/dist/index.js
CHANGED
|
@@ -140,220 +140,6 @@ var init_defaultConnection = __esm(() => {
|
|
|
140
140
|
DEFAULT_CONNECTION_KEY = Symbol.for("@getstrata/defaultDatabaseConnection");
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
// ../../src/core/database/schema/driver.ts
|
|
144
|
-
function normalizeConnectionName(connection) {
|
|
145
|
-
const normalized = connection.trim().toLowerCase();
|
|
146
|
-
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
147
|
-
return "pgsql";
|
|
148
|
-
}
|
|
149
|
-
if (normalized === "mysql" || normalized === "mariadb") {
|
|
150
|
-
return "mysql";
|
|
151
|
-
}
|
|
152
|
-
if (normalized === "sqlite") {
|
|
153
|
-
return "sqlite";
|
|
154
|
-
}
|
|
155
|
-
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
156
|
-
}
|
|
157
|
-
function resolveDriverFromUrl(url) {
|
|
158
|
-
const normalized = url.trim().toLowerCase();
|
|
159
|
-
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
160
|
-
return "pgsql";
|
|
161
|
-
}
|
|
162
|
-
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
163
|
-
return "mysql";
|
|
164
|
-
}
|
|
165
|
-
if (normalized.startsWith("sqlite:")) {
|
|
166
|
-
return "sqlite";
|
|
167
|
-
}
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
function resolveDatabaseDriver(options = {}) {
|
|
171
|
-
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
172
|
-
if (connection) {
|
|
173
|
-
return normalizeConnectionName(connection);
|
|
174
|
-
}
|
|
175
|
-
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
176
|
-
const fromUrl = resolveDriverFromUrl(url);
|
|
177
|
-
if (fromUrl) {
|
|
178
|
-
return fromUrl;
|
|
179
|
-
}
|
|
180
|
-
return "pgsql";
|
|
181
|
-
}
|
|
182
|
-
var init_driver = () => {};
|
|
183
|
-
|
|
184
|
-
// ../../src/core/database/dialect.ts
|
|
185
|
-
function assertSafeIdentifier(identifier) {
|
|
186
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
187
|
-
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
188
|
-
}
|
|
189
|
-
return identifier;
|
|
190
|
-
}
|
|
191
|
-
function quoteIdentifierFor(dialect, column) {
|
|
192
|
-
return dialect.quoteIdentifier(column);
|
|
193
|
-
}
|
|
194
|
-
function dialectFor(driver) {
|
|
195
|
-
return dialects[driver];
|
|
196
|
-
}
|
|
197
|
-
function currentSqlDialect() {
|
|
198
|
-
return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
|
|
199
|
-
}
|
|
200
|
-
function useSqlDialect(driver) {
|
|
201
|
-
dialectOverride = dialectFor(driver);
|
|
202
|
-
return dialectOverride;
|
|
203
|
-
}
|
|
204
|
-
function sqlTimestamp(value = new Date) {
|
|
205
|
-
return currentSqlDialect().timestampValue(value);
|
|
206
|
-
}
|
|
207
|
-
function resetSqlDialect() {
|
|
208
|
-
dialectOverride = null;
|
|
209
|
-
}
|
|
210
|
-
function runWithSqlDialect(driver, callback) {
|
|
211
|
-
return dialectContext.run(dialectFor(driver), callback);
|
|
212
|
-
}
|
|
213
|
-
var postgresDialect, mysqlDialect, sqliteDialect, dialects, dialectContext, dialectOverride = null;
|
|
214
|
-
var init_dialect = __esm(() => {
|
|
215
|
-
init_asyncContextStore();
|
|
216
|
-
init_driver();
|
|
217
|
-
postgresDialect = {
|
|
218
|
-
driver: "pgsql",
|
|
219
|
-
placeholder(index) {
|
|
220
|
-
return `$${index}`;
|
|
221
|
-
},
|
|
222
|
-
quoteIdentifier(identifier) {
|
|
223
|
-
return `"${assertSafeIdentifier(identifier)}"`;
|
|
224
|
-
},
|
|
225
|
-
nowExpression() {
|
|
226
|
-
return "NOW()";
|
|
227
|
-
},
|
|
228
|
-
epochMillisecondsExpression() {
|
|
229
|
-
return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
|
|
230
|
-
},
|
|
231
|
-
rowLockClause(skipLocked = false) {
|
|
232
|
-
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
233
|
-
},
|
|
234
|
-
timestampValue(value) {
|
|
235
|
-
return value.toISOString();
|
|
236
|
-
},
|
|
237
|
-
returningClause(columns) {
|
|
238
|
-
return ` RETURNING ${columns}`;
|
|
239
|
-
},
|
|
240
|
-
ilikeOperator() {
|
|
241
|
-
return "ILIKE";
|
|
242
|
-
},
|
|
243
|
-
nullsLastSuffix() {
|
|
244
|
-
return " NULLS LAST";
|
|
245
|
-
},
|
|
246
|
-
castToText(expression) {
|
|
247
|
-
return `${expression}::text`;
|
|
248
|
-
},
|
|
249
|
-
upsertSuffix(conflictColumns, updateColumns) {
|
|
250
|
-
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
251
|
-
if (updateColumns.length === 0) {
|
|
252
|
-
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
253
|
-
}
|
|
254
|
-
const assignments = updateColumns.map((column) => {
|
|
255
|
-
const quoted = quoteIdentifierFor(this, column);
|
|
256
|
-
return `${quoted} = excluded.${quoted}`;
|
|
257
|
-
}).join(", ");
|
|
258
|
-
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
mysqlDialect = {
|
|
262
|
-
driver: "mysql",
|
|
263
|
-
placeholder() {
|
|
264
|
-
return "?";
|
|
265
|
-
},
|
|
266
|
-
quoteIdentifier(identifier) {
|
|
267
|
-
return `\`${assertSafeIdentifier(identifier)}\``;
|
|
268
|
-
},
|
|
269
|
-
nowExpression() {
|
|
270
|
-
return "CURRENT_TIMESTAMP";
|
|
271
|
-
},
|
|
272
|
-
epochMillisecondsExpression() {
|
|
273
|
-
return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
|
|
274
|
-
},
|
|
275
|
-
rowLockClause(skipLocked = false) {
|
|
276
|
-
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
277
|
-
},
|
|
278
|
-
timestampValue(value) {
|
|
279
|
-
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
280
|
-
},
|
|
281
|
-
returningClause() {
|
|
282
|
-
return "";
|
|
283
|
-
},
|
|
284
|
-
ilikeOperator() {
|
|
285
|
-
return "LIKE";
|
|
286
|
-
},
|
|
287
|
-
nullsLastSuffix() {
|
|
288
|
-
return "";
|
|
289
|
-
},
|
|
290
|
-
castToText(expression) {
|
|
291
|
-
return `CAST(${expression} AS CHAR)`;
|
|
292
|
-
},
|
|
293
|
-
upsertSuffix(conflictColumns, updateColumns) {
|
|
294
|
-
if (updateColumns.length === 0) {
|
|
295
|
-
const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
|
|
296
|
-
return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
|
|
297
|
-
}
|
|
298
|
-
const assignments = updateColumns.map((column) => {
|
|
299
|
-
const quoted = quoteIdentifierFor(this, column);
|
|
300
|
-
return `${quoted} = VALUES(${quoted})`;
|
|
301
|
-
}).join(", ");
|
|
302
|
-
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
sqliteDialect = {
|
|
306
|
-
driver: "sqlite",
|
|
307
|
-
placeholder() {
|
|
308
|
-
return "?";
|
|
309
|
-
},
|
|
310
|
-
quoteIdentifier(identifier) {
|
|
311
|
-
return `"${assertSafeIdentifier(identifier)}"`;
|
|
312
|
-
},
|
|
313
|
-
nowExpression() {
|
|
314
|
-
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
315
|
-
},
|
|
316
|
-
epochMillisecondsExpression() {
|
|
317
|
-
return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
|
|
318
|
-
},
|
|
319
|
-
rowLockClause() {
|
|
320
|
-
return "";
|
|
321
|
-
},
|
|
322
|
-
timestampValue(value) {
|
|
323
|
-
return value.toISOString();
|
|
324
|
-
},
|
|
325
|
-
returningClause(columns) {
|
|
326
|
-
return ` RETURNING ${columns}`;
|
|
327
|
-
},
|
|
328
|
-
ilikeOperator() {
|
|
329
|
-
return "LIKE";
|
|
330
|
-
},
|
|
331
|
-
nullsLastSuffix() {
|
|
332
|
-
return "";
|
|
333
|
-
},
|
|
334
|
-
castToText(expression) {
|
|
335
|
-
return `CAST(${expression} AS TEXT)`;
|
|
336
|
-
},
|
|
337
|
-
upsertSuffix(conflictColumns, updateColumns) {
|
|
338
|
-
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
339
|
-
if (updateColumns.length === 0) {
|
|
340
|
-
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
341
|
-
}
|
|
342
|
-
const assignments = updateColumns.map((column) => {
|
|
343
|
-
const quoted = quoteIdentifierFor(this, column);
|
|
344
|
-
return `${quoted} = excluded.${quoted}`;
|
|
345
|
-
}).join(", ");
|
|
346
|
-
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
dialects = {
|
|
350
|
-
pgsql: postgresDialect,
|
|
351
|
-
mysql: mysqlDialect,
|
|
352
|
-
sqlite: sqliteDialect
|
|
353
|
-
};
|
|
354
|
-
dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
|
|
355
|
-
});
|
|
356
|
-
|
|
357
143
|
// ../../src/core/tenant/tenantContext.ts
|
|
358
144
|
function runWithTenant(tenant, callback) {
|
|
359
145
|
return tenantContext.run(tenant, callback);
|
|
@@ -374,78 +160,68 @@ var init_tenantContext = __esm(() => {
|
|
|
374
160
|
tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
375
161
|
});
|
|
376
162
|
|
|
377
|
-
// ../../src/core/
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
function isTaggedStrings(value) {
|
|
392
|
-
if (!Array.isArray(value) || typeof value[0] !== "string") {
|
|
393
|
-
return false;
|
|
163
|
+
// ../../src/core/events/eventBus.ts
|
|
164
|
+
class EventBus {
|
|
165
|
+
constructor() {}
|
|
166
|
+
transactionalListeners = new Map;
|
|
167
|
+
listenTransactional(event, listener) {
|
|
168
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
169
|
+
handlers.add(listener);
|
|
170
|
+
this.transactionalListeners.set(event, handlers);
|
|
171
|
+
return () => {
|
|
172
|
+
handlers.delete(listener);
|
|
173
|
+
if (!handlers.size)
|
|
174
|
+
this.transactionalListeners.delete(event);
|
|
175
|
+
};
|
|
394
176
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
function runOnConnection(connection, args) {
|
|
398
|
-
if (typeof connection === "function") {
|
|
399
|
-
return connection(...args);
|
|
177
|
+
hasTransactionalListeners(event) {
|
|
178
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
400
179
|
}
|
|
401
|
-
|
|
402
|
-
|
|
180
|
+
async dispatchTransactional(event, payload) {
|
|
181
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
182
|
+
await listener(payload);
|
|
403
183
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
184
|
+
listeners = new Map;
|
|
185
|
+
on(event, listener) {
|
|
186
|
+
return this.listen(event, listener);
|
|
407
187
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
apply(_target, _thisArg, args) {
|
|
419
|
-
return runOnConnection(resolveRepositoryConnection(), args);
|
|
420
|
-
},
|
|
421
|
-
get(_target, property) {
|
|
422
|
-
const connection = resolveRepositoryConnection();
|
|
423
|
-
const value = connection[property];
|
|
424
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
// ../../src/core/tenant/tenancyConfig.ts
|
|
430
|
-
function readTenancyDriver(env = process.env) {
|
|
431
|
-
const raw = env.TENANCY_DRIVER?.trim();
|
|
432
|
-
if (raw === undefined || raw === "") {
|
|
433
|
-
return "rls";
|
|
188
|
+
listen(event, listener) {
|
|
189
|
+
const handlers = this.listeners.get(event) ?? new Set;
|
|
190
|
+
handlers.add(listener);
|
|
191
|
+
this.listeners.set(event, handlers);
|
|
192
|
+
return () => {
|
|
193
|
+
handlers.delete(listener);
|
|
194
|
+
if (handlers.size === 0) {
|
|
195
|
+
this.listeners.delete(event);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
434
198
|
}
|
|
435
|
-
|
|
436
|
-
|
|
199
|
+
async emit(event, payload) {
|
|
200
|
+
await this.dispatch(event, payload);
|
|
201
|
+
}
|
|
202
|
+
async dispatch(event, payload) {
|
|
203
|
+
const handlers = this.listeners.get(event);
|
|
204
|
+
if (!handlers || handlers.size === 0) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
for (const handler of handlers) {
|
|
208
|
+
await handler(payload);
|
|
209
|
+
}
|
|
437
210
|
}
|
|
438
|
-
throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
|
|
439
|
-
}
|
|
440
|
-
function isTenancyEnabled(env = process.env) {
|
|
441
|
-
return readTenancyDriver(env) !== "none";
|
|
442
211
|
}
|
|
443
|
-
function
|
|
444
|
-
|
|
212
|
+
function readSharedEventBus() {
|
|
213
|
+
const globalBus = globalThis[EVENT_BUS_KEY];
|
|
214
|
+
if (globalBus) {
|
|
215
|
+
return globalBus;
|
|
216
|
+
}
|
|
217
|
+
const bus = new EventBus;
|
|
218
|
+
globalThis[EVENT_BUS_KEY] = bus;
|
|
219
|
+
return bus;
|
|
445
220
|
}
|
|
446
|
-
var
|
|
447
|
-
var
|
|
448
|
-
|
|
221
|
+
var EVENT_BUS_KEY, eventBus;
|
|
222
|
+
var init_eventBus = __esm(() => {
|
|
223
|
+
EVENT_BUS_KEY = Symbol.for("@getstrata/eventBus");
|
|
224
|
+
eventBus = readSharedEventBus();
|
|
449
225
|
});
|
|
450
226
|
|
|
451
227
|
// ../../src/core/database/transactionControl.ts
|
|
@@ -526,68 +302,71 @@ var init_transactionControl = __esm(() => {
|
|
|
526
302
|
transactionScopes = createAsyncContextStore("@getstrata/transactionScopes");
|
|
527
303
|
});
|
|
528
304
|
|
|
529
|
-
// ../../src/core/
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
535
|
-
handlers.add(listener);
|
|
536
|
-
this.transactionalListeners.set(event, handlers);
|
|
537
|
-
return () => {
|
|
538
|
-
handlers.delete(listener);
|
|
539
|
-
if (!handlers.size)
|
|
540
|
-
this.transactionalListeners.delete(event);
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
hasTransactionalListeners(event) {
|
|
544
|
-
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
305
|
+
// ../../src/core/tenant/tenancyConfig.ts
|
|
306
|
+
function readTenancyDriver(env = process.env) {
|
|
307
|
+
const raw = env.TENANCY_DRIVER?.trim();
|
|
308
|
+
if (raw === undefined || raw === "") {
|
|
309
|
+
return "rls";
|
|
545
310
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
await listener(payload);
|
|
311
|
+
if (TENANCY_DRIVERS.includes(raw)) {
|
|
312
|
+
return raw;
|
|
549
313
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
314
|
+
throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
|
|
315
|
+
}
|
|
316
|
+
function isTenancyEnabled(env = process.env) {
|
|
317
|
+
return readTenancyDriver(env) !== "none";
|
|
318
|
+
}
|
|
319
|
+
function isRlsTenancy(env = process.env) {
|
|
320
|
+
return readTenancyDriver(env) === "rls";
|
|
321
|
+
}
|
|
322
|
+
var TENANCY_DRIVERS;
|
|
323
|
+
var init_tenancyConfig = __esm(() => {
|
|
324
|
+
TENANCY_DRIVERS = ["rls", "column", "none"];
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
328
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
329
|
+
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
330
|
+
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
331
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
332
|
+
}
|
|
333
|
+
async function runWithTenantDatabase(tenant, callback) {
|
|
334
|
+
const outerTenant = currentTenant();
|
|
335
|
+
if (hasActiveDatabaseConnection() && outerTenant && outerTenant.id !== tenant.id) {
|
|
336
|
+
throw new Error("Cannot switch tenants inside an active database transaction.");
|
|
553
337
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
handlers.add(listener);
|
|
557
|
-
this.listeners.set(event, handlers);
|
|
558
|
-
return () => {
|
|
559
|
-
handlers.delete(listener);
|
|
560
|
-
if (handlers.size === 0) {
|
|
561
|
-
this.listeners.delete(event);
|
|
562
|
-
}
|
|
563
|
-
};
|
|
338
|
+
if (!isTenancyEnabled() || !isRlsTenancy()) {
|
|
339
|
+
return await runWithTenant(tenant, callback);
|
|
564
340
|
}
|
|
565
|
-
|
|
566
|
-
|
|
341
|
+
if (hasActiveDatabaseConnection()) {
|
|
342
|
+
const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
343
|
+
return await runWithScopedTenantTransaction(() => applyTenantContextToTransaction(activeConnection, tenant.id), () => runWithTenant(tenant, callback));
|
|
567
344
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
for (const handler of handlers) {
|
|
574
|
-
await handler(payload);
|
|
575
|
-
}
|
|
345
|
+
const pool = getDefaultDatabasePool();
|
|
346
|
+
const begin = pool.begin;
|
|
347
|
+
if (typeof begin !== "function") {
|
|
348
|
+
throw new Error("RLS tenant scope requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
576
349
|
}
|
|
350
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
351
|
+
return await begin.bind(pool)(AsyncLocalStorage2.bind(async (transaction) => {
|
|
352
|
+
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
353
|
+
return await runWithDatabaseConnection(transaction, async () => {
|
|
354
|
+
return await runWithTransactionScope(async () => commitOrRollbackScope(await runWithTenant(tenant, callback)));
|
|
355
|
+
});
|
|
356
|
+
}));
|
|
357
|
+
}));
|
|
577
358
|
}
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
if (globalBus) {
|
|
581
|
-
return globalBus;
|
|
582
|
-
}
|
|
583
|
-
const bus = new EventBus;
|
|
584
|
-
globalThis[EVENT_BUS_KEY] = bus;
|
|
585
|
-
return bus;
|
|
359
|
+
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
360
|
+
return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
|
|
586
361
|
}
|
|
587
|
-
var
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
362
|
+
var init_tenantDatabaseScope = __esm(() => {
|
|
363
|
+
init_defaultConnection();
|
|
364
|
+
init_connectionContext();
|
|
365
|
+
init_transactionControl();
|
|
366
|
+
init_deferredModelEvents();
|
|
367
|
+
init_databaseTenantContext();
|
|
368
|
+
init_tenancyConfig();
|
|
369
|
+
init_tenantContext();
|
|
591
370
|
});
|
|
592
371
|
|
|
593
372
|
// ../../src/core/events/deferredModelEvents.ts
|
|
@@ -667,147 +446,413 @@ async function runWithDeferredModelEvents(callback) {
|
|
|
667
446
|
rollback();
|
|
668
447
|
throw error;
|
|
669
448
|
}
|
|
670
|
-
}
|
|
671
|
-
const state = { frames: [[]], rollbackFrames: [[]] };
|
|
672
|
-
let result;
|
|
673
|
-
try {
|
|
674
|
-
result = await deferredModelEvents.run(state, callback);
|
|
675
|
-
} catch (error) {
|
|
676
|
-
for (const rollback of (state.rollbackFrames[0] ?? []).reverse())
|
|
677
|
-
rollback();
|
|
678
|
-
throw error;
|
|
679
|
-
}
|
|
680
|
-
await dispatchQueuedModelEvents(state.frames[0] ?? []);
|
|
681
|
-
return result;
|
|
682
|
-
}
|
|
683
|
-
function onDeferredModelRollback(callback) {
|
|
684
|
-
deferredModelEvents.getStore()?.rollbackFrames.at(-1)?.push(callback);
|
|
685
|
-
}
|
|
686
|
-
var deferredModelEvents;
|
|
687
|
-
var init_deferredModelEvents = __esm(() => {
|
|
688
|
-
init_asyncContextStore();
|
|
689
|
-
init_tenantContext();
|
|
690
|
-
init_eventBus();
|
|
691
|
-
deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
// ../../src/core/database/acquisitionMetrics.ts
|
|
695
|
-
function createTransactionAcquisitionMetrics() {
|
|
696
|
-
const state = {
|
|
697
|
-
inflight: 0,
|
|
698
|
-
outcomes: outcomes.map((outcome) => ({
|
|
699
|
-
outcome,
|
|
700
|
-
count: 0,
|
|
701
|
-
sumSeconds: 0,
|
|
702
|
-
buckets: ACQUISITION_BUCKETS_SECONDS.map(() => 0)
|
|
703
|
-
}))
|
|
449
|
+
}
|
|
450
|
+
const state = { frames: [[]], rollbackFrames: [[]] };
|
|
451
|
+
let result;
|
|
452
|
+
try {
|
|
453
|
+
result = await deferredModelEvents.run(state, callback);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
for (const rollback of (state.rollbackFrames[0] ?? []).reverse())
|
|
456
|
+
rollback();
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
await dispatchQueuedModelEvents(state.frames[0] ?? []);
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
function onDeferredModelRollback(callback) {
|
|
463
|
+
deferredModelEvents.getStore()?.rollbackFrames.at(-1)?.push(callback);
|
|
464
|
+
}
|
|
465
|
+
var deferredModelEvents;
|
|
466
|
+
var init_deferredModelEvents = __esm(() => {
|
|
467
|
+
init_asyncContextStore();
|
|
468
|
+
init_tenantContext();
|
|
469
|
+
init_eventBus();
|
|
470
|
+
deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// ../../src/core/database/acquisitionMetrics.ts
|
|
474
|
+
function createTransactionAcquisitionMetrics() {
|
|
475
|
+
const state = {
|
|
476
|
+
inflight: 0,
|
|
477
|
+
outcomes: outcomes.map((outcome) => ({
|
|
478
|
+
outcome,
|
|
479
|
+
count: 0,
|
|
480
|
+
sumSeconds: 0,
|
|
481
|
+
buckets: ACQUISITION_BUCKETS_SECONDS.map(() => 0)
|
|
482
|
+
}))
|
|
483
|
+
};
|
|
484
|
+
const metrics = {
|
|
485
|
+
[metricsBrand]: true,
|
|
486
|
+
snapshot: () => ({
|
|
487
|
+
inflight: state.inflight,
|
|
488
|
+
outcomes: state.outcomes.map((histogram) => ({
|
|
489
|
+
...histogram,
|
|
490
|
+
buckets: [...histogram.buckets]
|
|
491
|
+
}))
|
|
492
|
+
})
|
|
493
|
+
};
|
|
494
|
+
states.set(metrics, state);
|
|
495
|
+
return metrics;
|
|
496
|
+
}
|
|
497
|
+
async function observeTransactionAcquisition(metrics, acquire, signal) {
|
|
498
|
+
const state = states.get(metrics);
|
|
499
|
+
if (!state)
|
|
500
|
+
throw new TypeError("Transaction acquisition metrics must be created by the framework factory.");
|
|
501
|
+
const started = performance.now();
|
|
502
|
+
state.inflight++;
|
|
503
|
+
let outcome = "acquired";
|
|
504
|
+
try {
|
|
505
|
+
return await acquire();
|
|
506
|
+
} catch (error) {
|
|
507
|
+
outcome = signal?.aborted ? "aborted" : "failed";
|
|
508
|
+
throw error;
|
|
509
|
+
} finally {
|
|
510
|
+
state.inflight--;
|
|
511
|
+
const elapsed = Math.max(0, performance.now() - started) / 1000;
|
|
512
|
+
const histogram = state.outcomes[outcomes.indexOf(outcome)];
|
|
513
|
+
if (histogram) {
|
|
514
|
+
histogram.count++;
|
|
515
|
+
histogram.sumSeconds += elapsed;
|
|
516
|
+
for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
|
|
517
|
+
const bound = ACQUISITION_BUCKETS_SECONDS[index];
|
|
518
|
+
if (bound !== undefined && elapsed <= bound)
|
|
519
|
+
histogram.buckets[index] = (histogram.buckets[index] ?? 0) + 1;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function renderTransactionAcquisitionMetrics(snapshot) {
|
|
525
|
+
const invalid = () => new TypeError("Invalid transaction acquisition metrics.");
|
|
526
|
+
if (!Number.isSafeInteger(snapshot.inflight) || snapshot.inflight < 0 || snapshot.outcomes.length !== outcomes.length)
|
|
527
|
+
throw invalid();
|
|
528
|
+
const name = "strata_database_transaction_acquisition_duration_seconds";
|
|
529
|
+
const lines = [
|
|
530
|
+
"# HELP strata_database_acquisition_collector_success Whether transaction acquisition observation succeeded.",
|
|
531
|
+
"# TYPE strata_database_acquisition_collector_success gauge",
|
|
532
|
+
"strata_database_acquisition_collector_success 1",
|
|
533
|
+
"# HELP strata_database_transaction_acquisition_inflight Native transaction checkouts awaiting settlement in this process.",
|
|
534
|
+
"# TYPE strata_database_transaction_acquisition_inflight gauge",
|
|
535
|
+
`strata_database_transaction_acquisition_inflight ${snapshot.inflight}`,
|
|
536
|
+
`# HELP ${name} Native checkout duration, including pool wait and connection establishment, for explicitly observed transactions.`,
|
|
537
|
+
`# TYPE ${name} histogram`
|
|
538
|
+
];
|
|
539
|
+
for (let i = 0;i < outcomes.length; i++) {
|
|
540
|
+
const histogram = snapshot.outcomes[i];
|
|
541
|
+
if (!histogram || histogram.outcome !== outcomes[i] || !Number.isSafeInteger(histogram.count) || histogram.count < 0 || !Number.isFinite(histogram.sumSeconds) || histogram.sumSeconds < 0 || histogram.buckets.length !== ACQUISITION_BUCKETS_SECONDS.length)
|
|
542
|
+
throw invalid();
|
|
543
|
+
let previous = 0;
|
|
544
|
+
for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
|
|
545
|
+
const value = histogram.buckets[index];
|
|
546
|
+
if (value === undefined || !Number.isSafeInteger(value) || value < previous || value > histogram.count)
|
|
547
|
+
throw invalid();
|
|
548
|
+
previous = value;
|
|
549
|
+
lines.push(`${name}_bucket{outcome="${histogram.outcome}",le="${ACQUISITION_BUCKETS_SECONDS[index]}"} ${value}`);
|
|
550
|
+
}
|
|
551
|
+
lines.push(`${name}_bucket{outcome="${histogram.outcome}",le="+Inf"} ${histogram.count}`, `${name}_count{outcome="${histogram.outcome}"} ${histogram.count}`, `${name}_sum{outcome="${histogram.outcome}"} ${histogram.sumSeconds}`);
|
|
552
|
+
}
|
|
553
|
+
return `${lines.join(`
|
|
554
|
+
`)}
|
|
555
|
+
`;
|
|
556
|
+
}
|
|
557
|
+
var ACQUISITION_BUCKETS_SECONDS, outcomes, metricsBrand, states;
|
|
558
|
+
var init_acquisitionMetrics = __esm(() => {
|
|
559
|
+
ACQUISITION_BUCKETS_SECONDS = [
|
|
560
|
+
0.001,
|
|
561
|
+
0.005,
|
|
562
|
+
0.01,
|
|
563
|
+
0.025,
|
|
564
|
+
0.05,
|
|
565
|
+
0.1,
|
|
566
|
+
0.25,
|
|
567
|
+
0.5,
|
|
568
|
+
1,
|
|
569
|
+
2.5,
|
|
570
|
+
5,
|
|
571
|
+
10
|
|
572
|
+
];
|
|
573
|
+
outcomes = ["acquired", "failed", "aborted"];
|
|
574
|
+
metricsBrand = Symbol("transaction acquisition metrics");
|
|
575
|
+
states = new WeakMap;
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// ../../src/core/database/connection.ts
|
|
579
|
+
function createDatabaseConnection(source) {
|
|
580
|
+
return {
|
|
581
|
+
async unsafe(query, params = []) {
|
|
582
|
+
return await source.unsafe(query, params);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
var init_connection = () => {};
|
|
587
|
+
|
|
588
|
+
// ../../src/core/database/schema/driver.ts
|
|
589
|
+
function normalizeConnectionName(connection) {
|
|
590
|
+
const normalized = connection.trim().toLowerCase();
|
|
591
|
+
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
592
|
+
return "pgsql";
|
|
593
|
+
}
|
|
594
|
+
if (normalized === "mysql" || normalized === "mariadb") {
|
|
595
|
+
return "mysql";
|
|
596
|
+
}
|
|
597
|
+
if (normalized === "sqlite") {
|
|
598
|
+
return "sqlite";
|
|
599
|
+
}
|
|
600
|
+
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
601
|
+
}
|
|
602
|
+
function resolveDriverFromUrl(url) {
|
|
603
|
+
const normalized = url.trim().toLowerCase();
|
|
604
|
+
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
605
|
+
return "pgsql";
|
|
606
|
+
}
|
|
607
|
+
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
608
|
+
return "mysql";
|
|
609
|
+
}
|
|
610
|
+
if (normalized.startsWith("sqlite:")) {
|
|
611
|
+
return "sqlite";
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
function resolveDatabaseDriver(options = {}) {
|
|
616
|
+
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
617
|
+
if (connection) {
|
|
618
|
+
return normalizeConnectionName(connection);
|
|
619
|
+
}
|
|
620
|
+
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
621
|
+
const fromUrl = resolveDriverFromUrl(url);
|
|
622
|
+
if (fromUrl) {
|
|
623
|
+
return fromUrl;
|
|
624
|
+
}
|
|
625
|
+
return "pgsql";
|
|
626
|
+
}
|
|
627
|
+
var init_driver = () => {};
|
|
628
|
+
|
|
629
|
+
// ../../src/core/database/dialect.ts
|
|
630
|
+
function assertSafeIdentifier(identifier) {
|
|
631
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
632
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
633
|
+
}
|
|
634
|
+
return identifier;
|
|
635
|
+
}
|
|
636
|
+
function quoteIdentifierFor(dialect, column) {
|
|
637
|
+
return dialect.quoteIdentifier(column);
|
|
638
|
+
}
|
|
639
|
+
function dialectFor(driver) {
|
|
640
|
+
return dialects[driver];
|
|
641
|
+
}
|
|
642
|
+
function currentSqlDialect() {
|
|
643
|
+
return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
|
|
644
|
+
}
|
|
645
|
+
function useSqlDialect(driver) {
|
|
646
|
+
dialectOverride = dialectFor(driver);
|
|
647
|
+
return dialectOverride;
|
|
648
|
+
}
|
|
649
|
+
function sqlTimestamp(value = new Date) {
|
|
650
|
+
return currentSqlDialect().timestampValue(value);
|
|
651
|
+
}
|
|
652
|
+
function resetSqlDialect() {
|
|
653
|
+
dialectOverride = null;
|
|
654
|
+
}
|
|
655
|
+
function runWithSqlDialect(driver, callback) {
|
|
656
|
+
return dialectContext.run(dialectFor(driver), callback);
|
|
657
|
+
}
|
|
658
|
+
var postgresDialect, mysqlDialect, sqliteDialect, dialects, dialectContext, dialectOverride = null;
|
|
659
|
+
var init_dialect = __esm(() => {
|
|
660
|
+
init_asyncContextStore();
|
|
661
|
+
init_driver();
|
|
662
|
+
postgresDialect = {
|
|
663
|
+
driver: "pgsql",
|
|
664
|
+
placeholder(index) {
|
|
665
|
+
return `$${index}`;
|
|
666
|
+
},
|
|
667
|
+
quoteIdentifier(identifier) {
|
|
668
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
669
|
+
},
|
|
670
|
+
nowExpression() {
|
|
671
|
+
return "NOW()";
|
|
672
|
+
},
|
|
673
|
+
epochMillisecondsExpression() {
|
|
674
|
+
return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
|
|
675
|
+
},
|
|
676
|
+
rowLockClause(skipLocked = false) {
|
|
677
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
678
|
+
},
|
|
679
|
+
timestampValue(value) {
|
|
680
|
+
return value.toISOString();
|
|
681
|
+
},
|
|
682
|
+
returningClause(columns) {
|
|
683
|
+
return ` RETURNING ${columns}`;
|
|
684
|
+
},
|
|
685
|
+
ilikeOperator() {
|
|
686
|
+
return "ILIKE";
|
|
687
|
+
},
|
|
688
|
+
nullsLastSuffix() {
|
|
689
|
+
return " NULLS LAST";
|
|
690
|
+
},
|
|
691
|
+
castToText(expression) {
|
|
692
|
+
return `${expression}::text`;
|
|
693
|
+
},
|
|
694
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
695
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
696
|
+
if (updateColumns.length === 0) {
|
|
697
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
698
|
+
}
|
|
699
|
+
const assignments = updateColumns.map((column) => {
|
|
700
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
701
|
+
return `${quoted} = excluded.${quoted}`;
|
|
702
|
+
}).join(", ");
|
|
703
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
mysqlDialect = {
|
|
707
|
+
driver: "mysql",
|
|
708
|
+
placeholder() {
|
|
709
|
+
return "?";
|
|
710
|
+
},
|
|
711
|
+
quoteIdentifier(identifier) {
|
|
712
|
+
return `\`${assertSafeIdentifier(identifier)}\``;
|
|
713
|
+
},
|
|
714
|
+
nowExpression() {
|
|
715
|
+
return "CURRENT_TIMESTAMP";
|
|
716
|
+
},
|
|
717
|
+
epochMillisecondsExpression() {
|
|
718
|
+
return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
|
|
719
|
+
},
|
|
720
|
+
rowLockClause(skipLocked = false) {
|
|
721
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
722
|
+
},
|
|
723
|
+
timestampValue(value) {
|
|
724
|
+
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
725
|
+
},
|
|
726
|
+
returningClause() {
|
|
727
|
+
return "";
|
|
728
|
+
},
|
|
729
|
+
ilikeOperator() {
|
|
730
|
+
return "LIKE";
|
|
731
|
+
},
|
|
732
|
+
nullsLastSuffix() {
|
|
733
|
+
return "";
|
|
734
|
+
},
|
|
735
|
+
castToText(expression) {
|
|
736
|
+
return `CAST(${expression} AS CHAR)`;
|
|
737
|
+
},
|
|
738
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
739
|
+
if (updateColumns.length === 0) {
|
|
740
|
+
const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
|
|
741
|
+
return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
|
|
742
|
+
}
|
|
743
|
+
const assignments = updateColumns.map((column) => {
|
|
744
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
745
|
+
return `${quoted} = VALUES(${quoted})`;
|
|
746
|
+
}).join(", ");
|
|
747
|
+
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
sqliteDialect = {
|
|
751
|
+
driver: "sqlite",
|
|
752
|
+
placeholder() {
|
|
753
|
+
return "?";
|
|
754
|
+
},
|
|
755
|
+
quoteIdentifier(identifier) {
|
|
756
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
757
|
+
},
|
|
758
|
+
nowExpression() {
|
|
759
|
+
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
760
|
+
},
|
|
761
|
+
epochMillisecondsExpression() {
|
|
762
|
+
return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
|
|
763
|
+
},
|
|
764
|
+
rowLockClause() {
|
|
765
|
+
return "";
|
|
766
|
+
},
|
|
767
|
+
timestampValue(value) {
|
|
768
|
+
return value.toISOString();
|
|
769
|
+
},
|
|
770
|
+
returningClause(columns) {
|
|
771
|
+
return ` RETURNING ${columns}`;
|
|
772
|
+
},
|
|
773
|
+
ilikeOperator() {
|
|
774
|
+
return "LIKE";
|
|
775
|
+
},
|
|
776
|
+
nullsLastSuffix() {
|
|
777
|
+
return "";
|
|
778
|
+
},
|
|
779
|
+
castToText(expression) {
|
|
780
|
+
return `CAST(${expression} AS TEXT)`;
|
|
781
|
+
},
|
|
782
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
783
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
784
|
+
if (updateColumns.length === 0) {
|
|
785
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
786
|
+
}
|
|
787
|
+
const assignments = updateColumns.map((column) => {
|
|
788
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
789
|
+
return `${quoted} = excluded.${quoted}`;
|
|
790
|
+
}).join(", ");
|
|
791
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
792
|
+
}
|
|
704
793
|
};
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
outcomes: state.outcomes.map((histogram) => ({
|
|
710
|
-
...histogram,
|
|
711
|
-
buckets: [...histogram.buckets]
|
|
712
|
-
}))
|
|
713
|
-
})
|
|
794
|
+
dialects = {
|
|
795
|
+
pgsql: postgresDialect,
|
|
796
|
+
mysql: mysqlDialect,
|
|
797
|
+
sqlite: sqliteDialect
|
|
714
798
|
};
|
|
715
|
-
|
|
716
|
-
|
|
799
|
+
dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
803
|
+
function resolveRepositoryConnection() {
|
|
804
|
+
return getActiveDatabaseConnection(getBoundDatabaseConnection() ?? getDefaultDatabaseQuery());
|
|
717
805
|
}
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
try {
|
|
726
|
-
return await acquire();
|
|
727
|
-
} catch (error) {
|
|
728
|
-
outcome = signal?.aborted ? "aborted" : "failed";
|
|
729
|
-
throw error;
|
|
730
|
-
} finally {
|
|
731
|
-
state.inflight--;
|
|
732
|
-
const elapsed = Math.max(0, performance.now() - started) / 1000;
|
|
733
|
-
const histogram = state.outcomes[outcomes.indexOf(outcome)];
|
|
734
|
-
if (histogram) {
|
|
735
|
-
histogram.count++;
|
|
736
|
-
histogram.sumSeconds += elapsed;
|
|
737
|
-
for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
|
|
738
|
-
const bound = ACQUISITION_BUCKETS_SECONDS[index];
|
|
739
|
-
if (bound !== undefined && elapsed <= bound)
|
|
740
|
-
histogram.buckets[index] = (histogram.buckets[index] ?? 0) + 1;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
806
|
+
function compileTaggedSql(strings, values) {
|
|
807
|
+
const dialect = currentSqlDialect();
|
|
808
|
+
let query = strings[0] ?? "";
|
|
809
|
+
const params = [];
|
|
810
|
+
for (let index = 0;index < values.length; index += 1) {
|
|
811
|
+
params.push(values[index]);
|
|
812
|
+
query += `${dialect.placeholder(params.length)}${strings[index + 1] ?? ""}`;
|
|
743
813
|
}
|
|
814
|
+
return { query, params };
|
|
744
815
|
}
|
|
745
|
-
function
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
throw invalid();
|
|
749
|
-
const name = "strata_database_transaction_acquisition_duration_seconds";
|
|
750
|
-
const lines = [
|
|
751
|
-
"# HELP strata_database_acquisition_collector_success Whether transaction acquisition observation succeeded.",
|
|
752
|
-
"# TYPE strata_database_acquisition_collector_success gauge",
|
|
753
|
-
"strata_database_acquisition_collector_success 1",
|
|
754
|
-
"# HELP strata_database_transaction_acquisition_inflight Native transaction checkouts awaiting settlement in this process.",
|
|
755
|
-
"# TYPE strata_database_transaction_acquisition_inflight gauge",
|
|
756
|
-
`strata_database_transaction_acquisition_inflight ${snapshot.inflight}`,
|
|
757
|
-
`# HELP ${name} Native checkout duration, including pool wait and connection establishment, for explicitly observed transactions.`,
|
|
758
|
-
`# TYPE ${name} histogram`
|
|
759
|
-
];
|
|
760
|
-
for (let i = 0;i < outcomes.length; i++) {
|
|
761
|
-
const histogram = snapshot.outcomes[i];
|
|
762
|
-
if (!histogram || histogram.outcome !== outcomes[i] || !Number.isSafeInteger(histogram.count) || histogram.count < 0 || !Number.isFinite(histogram.sumSeconds) || histogram.sumSeconds < 0 || histogram.buckets.length !== ACQUISITION_BUCKETS_SECONDS.length)
|
|
763
|
-
throw invalid();
|
|
764
|
-
let previous = 0;
|
|
765
|
-
for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
|
|
766
|
-
const value = histogram.buckets[index];
|
|
767
|
-
if (value === undefined || !Number.isSafeInteger(value) || value < previous || value > histogram.count)
|
|
768
|
-
throw invalid();
|
|
769
|
-
previous = value;
|
|
770
|
-
lines.push(`${name}_bucket{outcome="${histogram.outcome}",le="${ACQUISITION_BUCKETS_SECONDS[index]}"} ${value}`);
|
|
771
|
-
}
|
|
772
|
-
lines.push(`${name}_bucket{outcome="${histogram.outcome}",le="+Inf"} ${histogram.count}`, `${name}_count{outcome="${histogram.outcome}"} ${histogram.count}`, `${name}_sum{outcome="${histogram.outcome}"} ${histogram.sumSeconds}`);
|
|
816
|
+
function isTaggedStrings(value) {
|
|
817
|
+
if (!Array.isArray(value) || typeof value[0] !== "string") {
|
|
818
|
+
return false;
|
|
773
819
|
}
|
|
774
|
-
return
|
|
775
|
-
`)}
|
|
776
|
-
`;
|
|
820
|
+
return typeof value.raw !== "undefined";
|
|
777
821
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
5,
|
|
792
|
-
10
|
|
793
|
-
];
|
|
794
|
-
outcomes = ["acquired", "failed", "aborted"];
|
|
795
|
-
metricsBrand = Symbol("transaction acquisition metrics");
|
|
796
|
-
states = new WeakMap;
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
// ../../src/core/database/connection.ts
|
|
800
|
-
function createDatabaseConnection(source) {
|
|
801
|
-
return {
|
|
802
|
-
async unsafe(query, params = []) {
|
|
803
|
-
return await source.unsafe(query, params);
|
|
804
|
-
}
|
|
805
|
-
};
|
|
822
|
+
function runOnConnection(connection, args) {
|
|
823
|
+
if (typeof connection === "function") {
|
|
824
|
+
return connection(...args);
|
|
825
|
+
}
|
|
826
|
+
if (typeof connection.unsafe !== "function") {
|
|
827
|
+
throw new Error("Database connection cannot run SQL.");
|
|
828
|
+
}
|
|
829
|
+
const strings = args[0];
|
|
830
|
+
if (!isTaggedStrings(strings)) {
|
|
831
|
+
throw new Error("Database connection cannot run tagged SQL.");
|
|
832
|
+
}
|
|
833
|
+
const compiled = compileTaggedSql(strings, args.slice(1));
|
|
834
|
+
return connection.unsafe(compiled.query, compiled.params);
|
|
806
835
|
}
|
|
807
|
-
var
|
|
836
|
+
var repositoryConnection;
|
|
837
|
+
var init_repositoryConnection = __esm(() => {
|
|
838
|
+
init_boundConnection();
|
|
839
|
+
init_connectionContext();
|
|
840
|
+
init_defaultConnection();
|
|
841
|
+
init_dialect();
|
|
842
|
+
repositoryConnection = new Proxy(function repositoryConnection() {}, {
|
|
843
|
+
apply(_target, _thisArg, args) {
|
|
844
|
+
return runOnConnection(resolveRepositoryConnection(), args);
|
|
845
|
+
},
|
|
846
|
+
get(_target, property) {
|
|
847
|
+
const connection = resolveRepositoryConnection();
|
|
848
|
+
const value = connection[property];
|
|
849
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
});
|
|
808
853
|
|
|
809
854
|
// ../../src/core/database/transaction.ts
|
|
810
|
-
import { AsyncLocalStorage as
|
|
855
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
811
856
|
async function runInSavepoint(connection, operation) {
|
|
812
857
|
const inherited = transactionFrame.getStore();
|
|
813
858
|
let parent = inherited?.connection === connection ? inherited : outerFrames.get(connection);
|
|
@@ -867,7 +912,7 @@ async function runInTransaction(operation, options = {}) {
|
|
|
867
912
|
signal?.throwIfAborted();
|
|
868
913
|
if (!supportsTransactions(connection))
|
|
869
914
|
throw new Error("Reserved database connection does not support transactions.");
|
|
870
|
-
return await connection.begin(
|
|
915
|
+
return await connection.begin(AsyncLocalStorage3.bind(async (transaction) => {
|
|
871
916
|
return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
|
|
872
917
|
}));
|
|
873
918
|
} finally {
|
|
@@ -890,7 +935,7 @@ var init_transaction = __esm(() => {
|
|
|
890
935
|
});
|
|
891
936
|
|
|
892
937
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
893
|
-
import { AsyncLocalStorage as
|
|
938
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
894
939
|
async function applyBypassToTransaction(transaction, bypass) {
|
|
895
940
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, [
|
|
896
941
|
bypass ? "true" : "false"
|
|
@@ -942,7 +987,7 @@ async function runWithScopedTenantTransaction(apply, callback) {
|
|
|
942
987
|
throw new Error("RLS migration bypass requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
943
988
|
}
|
|
944
989
|
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
945
|
-
return await begin.bind(pool)(
|
|
990
|
+
return await begin.bind(pool)(AsyncLocalStorage4.bind(async (transaction) => {
|
|
946
991
|
await apply(transaction);
|
|
947
992
|
return await runWithDatabaseConnection(transaction, () => runWithTransactionScope(async () => commitOrRollbackScope(await callback())));
|
|
948
993
|
}));
|
|
@@ -964,51 +1009,6 @@ var init_databaseTenantContext = __esm(() => {
|
|
|
964
1009
|
init_tenancyConfig();
|
|
965
1010
|
});
|
|
966
1011
|
|
|
967
|
-
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
968
|
-
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
969
|
-
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
970
|
-
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
971
|
-
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
972
|
-
}
|
|
973
|
-
async function runWithTenantDatabase(tenant, callback) {
|
|
974
|
-
const outerTenant = currentTenant();
|
|
975
|
-
if (hasActiveDatabaseConnection() && outerTenant && outerTenant.id !== tenant.id) {
|
|
976
|
-
throw new Error("Cannot switch tenants inside an active database transaction.");
|
|
977
|
-
}
|
|
978
|
-
if (!isTenancyEnabled() || !isRlsTenancy()) {
|
|
979
|
-
return await runWithTenant(tenant, callback);
|
|
980
|
-
}
|
|
981
|
-
if (hasActiveDatabaseConnection()) {
|
|
982
|
-
const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
983
|
-
return await runWithScopedTenantTransaction(() => applyTenantContextToTransaction(activeConnection, tenant.id), () => runWithTenant(tenant, callback));
|
|
984
|
-
}
|
|
985
|
-
const pool = getDefaultDatabasePool();
|
|
986
|
-
const begin = pool.begin;
|
|
987
|
-
if (typeof begin !== "function") {
|
|
988
|
-
throw new Error("RLS tenant scope requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
989
|
-
}
|
|
990
|
-
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
991
|
-
return await begin.bind(pool)(AsyncLocalStorage4.bind(async (transaction) => {
|
|
992
|
-
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
993
|
-
return await runWithDatabaseConnection(transaction, async () => {
|
|
994
|
-
return await runWithTransactionScope(async () => commitOrRollbackScope(await runWithTenant(tenant, callback)));
|
|
995
|
-
});
|
|
996
|
-
}));
|
|
997
|
-
}));
|
|
998
|
-
}
|
|
999
|
-
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
1000
|
-
return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
|
|
1001
|
-
}
|
|
1002
|
-
var init_tenantDatabaseScope = __esm(() => {
|
|
1003
|
-
init_defaultConnection();
|
|
1004
|
-
init_connectionContext();
|
|
1005
|
-
init_transactionControl();
|
|
1006
|
-
init_deferredModelEvents();
|
|
1007
|
-
init_databaseTenantContext();
|
|
1008
|
-
init_tenancyConfig();
|
|
1009
|
-
init_tenantContext();
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
1012
|
// ../../src/core/admin/formatValue.ts
|
|
1013
1013
|
function formatAdminValue(value, type = "text") {
|
|
1014
1014
|
if (value === null || value === undefined) {
|
|
@@ -1304,6 +1304,7 @@ function resolveAuthUserDirectory(container) {
|
|
|
1304
1304
|
|
|
1305
1305
|
// ../../src/core/auth/basicAuthGuard.ts
|
|
1306
1306
|
init_boundConnection();
|
|
1307
|
+
init_connectionContext();
|
|
1307
1308
|
init_defaultConnection();
|
|
1308
1309
|
|
|
1309
1310
|
// ../../src/core/http/statelessAuth.ts
|
|
@@ -1344,6 +1345,9 @@ function readBasicCredentials(request) {
|
|
|
1344
1345
|
}
|
|
1345
1346
|
}
|
|
1346
1347
|
|
|
1348
|
+
// ../../src/core/auth/basicAuthGuard.ts
|
|
1349
|
+
init_databaseTenantContext();
|
|
1350
|
+
|
|
1347
1351
|
// ../../src/core/auth/password.ts
|
|
1348
1352
|
async function hashPassword(password) {
|
|
1349
1353
|
return await Bun.password.hash(password, {
|
|
@@ -1759,11 +1763,17 @@ async function persistConsumedRecoveryHash(sql, userId, currentRaw, consumedHash
|
|
|
1759
1763
|
if (!consumedHash) {
|
|
1760
1764
|
return;
|
|
1761
1765
|
}
|
|
1766
|
+
const hashes = parseRecoveryHashes(currentRaw);
|
|
1767
|
+
if (typeof currentRaw !== "string" || !hashes.includes(consumedHash)) {
|
|
1768
|
+
throw new UnprocessableEntityError("mfa_invalid");
|
|
1769
|
+
}
|
|
1762
1770
|
const dialect = currentSqlDialect();
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1771
|
+
const returning = dialect.returningClause("id");
|
|
1772
|
+
const rows = await sql.unsafe(`UPDATE users SET mfa_recovery_codes = ${dialect.placeholder(1)} WHERE id = ${dialect.placeholder(2)} AND mfa_recovery_codes = ${dialect.placeholder(3)}${returning}`, [JSON.stringify(hashes.filter((hash) => hash !== consumedHash)), userId, currentRaw]);
|
|
1773
|
+
const header = rows[0];
|
|
1774
|
+
const affected = returning ? rows.length : typeof header === "object" && header !== null && ("affectedRows" in header) ? Number(header.affectedRows) : 0;
|
|
1775
|
+
if (affected !== 1)
|
|
1776
|
+
throw new UnprocessableEntityError("mfa_invalid");
|
|
1767
1777
|
}
|
|
1768
1778
|
|
|
1769
1779
|
// ../../src/core/auth/basicAuthGuard.ts
|
|
@@ -1772,6 +1782,18 @@ class BasicAuthGuard {
|
|
|
1772
1782
|
constructor(container) {
|
|
1773
1783
|
this.container = container;
|
|
1774
1784
|
}
|
|
1785
|
+
async persistRecovery(record, hash) {
|
|
1786
|
+
try {
|
|
1787
|
+
await runWithMigrationBypassForIdentifier(record.id, async () => {
|
|
1788
|
+
await persistConsumedRecoveryHash(getActiveDatabaseConnection(getBoundDatabaseConnection() ?? getDefaultDatabasePool()), record.id, record.mfa_recovery_codes, hash);
|
|
1789
|
+
});
|
|
1790
|
+
return true;
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
if (error instanceof UnprocessableEntityError && error.message === "mfa_invalid")
|
|
1793
|
+
return false;
|
|
1794
|
+
throw error;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1775
1797
|
async resolve(request) {
|
|
1776
1798
|
const credentials = readBasicCredentials(request);
|
|
1777
1799
|
if (!credentials) {
|
|
@@ -1793,7 +1815,8 @@ class BasicAuthGuard {
|
|
|
1793
1815
|
return null;
|
|
1794
1816
|
}
|
|
1795
1817
|
if (mfa.consumedRecoveryHash) {
|
|
1796
|
-
|
|
1818
|
+
if (!await this.persistRecovery(record, mfa.consumedRecoveryHash))
|
|
1819
|
+
return null;
|
|
1797
1820
|
}
|
|
1798
1821
|
return {
|
|
1799
1822
|
id: record.id,
|
|
@@ -1821,7 +1844,8 @@ class BasicAuthGuard {
|
|
|
1821
1844
|
return null;
|
|
1822
1845
|
}
|
|
1823
1846
|
if (mfa.consumedRecoveryHash) {
|
|
1824
|
-
|
|
1847
|
+
if (!await this.persistRecovery(record, mfa.consumedRecoveryHash))
|
|
1848
|
+
return null;
|
|
1825
1849
|
}
|
|
1826
1850
|
return user;
|
|
1827
1851
|
}
|
|
@@ -2093,6 +2117,7 @@ var SESSION_GUARD_NAMES = ["web", "session", "default"];
|
|
|
2093
2117
|
|
|
2094
2118
|
class AuthManager {
|
|
2095
2119
|
guard;
|
|
2120
|
+
basicResolutions = new WeakMap;
|
|
2096
2121
|
namedGuards = new Map;
|
|
2097
2122
|
constructor(guard) {
|
|
2098
2123
|
this.guard = guard;
|
|
@@ -2104,6 +2129,7 @@ class AuthManager {
|
|
|
2104
2129
|
throw new Error("Auth guard name must not be empty.");
|
|
2105
2130
|
}
|
|
2106
2131
|
this.namedGuards.set(trimmed, next);
|
|
2132
|
+
this.basicResolutions = new WeakMap;
|
|
2107
2133
|
return this;
|
|
2108
2134
|
}
|
|
2109
2135
|
use(name = "default") {
|
|
@@ -2124,7 +2150,25 @@ class AuthManager {
|
|
|
2124
2150
|
if (!request) {
|
|
2125
2151
|
return { user: currentAuthUser(), credentialSource: null };
|
|
2126
2152
|
}
|
|
2127
|
-
|
|
2153
|
+
if (authorizationScheme(request) !== "basic") {
|
|
2154
|
+
this.basicResolutions.delete(request);
|
|
2155
|
+
return await this.authenticateRequest(request);
|
|
2156
|
+
}
|
|
2157
|
+
const authorization = request.headers.get("authorization");
|
|
2158
|
+
const mfaCode = request.headers.get("x-mfa-code");
|
|
2159
|
+
const cached = this.basicResolutions.get(request);
|
|
2160
|
+
if (cached && cached.authorization === authorization && cached.mfaCode === mfaCode) {
|
|
2161
|
+
return await cached.result;
|
|
2162
|
+
}
|
|
2163
|
+
const entry = { authorization, mfaCode, result: this.authenticateRequest(request) };
|
|
2164
|
+
this.basicResolutions.set(request, entry);
|
|
2165
|
+
try {
|
|
2166
|
+
return await entry.result;
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
if (this.basicResolutions.get(request) === entry)
|
|
2169
|
+
this.basicResolutions.delete(request);
|
|
2170
|
+
throw error;
|
|
2171
|
+
}
|
|
2128
2172
|
}
|
|
2129
2173
|
user(request) {
|
|
2130
2174
|
return this.resolve(request);
|