@getstrata/core 2.0.1 → 2.0.3
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/CHANGELOG.md +10 -0
- package/dist/core/database/dialect.d.ts +4 -0
- package/dist/core/database/mysqlConnection.d.ts +12 -2
- package/dist/core/database/sqliteConnection.d.ts +2 -1
- package/dist/core/database/transaction.d.ts +2 -2
- package/dist/core/database/transactionControl.d.ts +2 -1
- package/dist/core/events/outbox/index.d.ts +54 -0
- package/dist/core/events/outbox/migration.d.ts +7 -0
- package/dist/core/http/requestMetaContext.d.ts +2 -0
- package/dist/core/http/response.d.ts +2 -1
- package/dist/core/security/safeFetch.d.ts +1 -0
- package/dist/entries/audit/exportAuditLogs.js +119 -17
- package/dist/entries/auth/basicAuthGuard.js +18 -0
- package/dist/entries/auth/oauth/oidcProvider.js +100 -17
- package/dist/entries/auth/oauth/providers.js +100 -17
- package/dist/entries/auth/oauth/samlProvider.js +18 -0
- package/dist/entries/auth/oneTimeToken.js +18 -0
- package/dist/entries/auth/passwordLogin.js +18 -0
- package/dist/entries/auth/saml/samlServiceProvider.js +18 -0
- package/dist/entries/auth/scimAuthMiddleware.js +19 -0
- package/dist/entries/database/migrations.js +18 -0
- package/dist/entries/database/sqliteConnection.js +101 -7
- package/dist/entries/events/outbox.js +1 -0
- package/dist/entries/http/response.js +39 -5
- package/dist/entries/jobs/exportAuditLogsJob.js +119 -17
- package/dist/entries/security/safeFetch.js +100 -17
- package/dist/entries/tenant/databaseTenantContext.js +19 -0
- package/dist/entries/tenant/tenantDatabaseScope.js +19 -0
- package/dist/entries/tracing/tracingMiddleware.js +100 -17
- package/dist/framework/public-api.d.ts +3 -1
- package/dist/index.js +591 -50
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -221,6 +221,12 @@ var init_dialect = __esm(() => {
|
|
|
221
221
|
nowExpression() {
|
|
222
222
|
return "NOW()";
|
|
223
223
|
},
|
|
224
|
+
epochMillisecondsExpression() {
|
|
225
|
+
return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
|
|
226
|
+
},
|
|
227
|
+
rowLockClause(skipLocked = false) {
|
|
228
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
229
|
+
},
|
|
224
230
|
timestampValue(value) {
|
|
225
231
|
return value.toISOString();
|
|
226
232
|
},
|
|
@@ -259,6 +265,12 @@ var init_dialect = __esm(() => {
|
|
|
259
265
|
nowExpression() {
|
|
260
266
|
return "CURRENT_TIMESTAMP";
|
|
261
267
|
},
|
|
268
|
+
epochMillisecondsExpression() {
|
|
269
|
+
return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
|
|
270
|
+
},
|
|
271
|
+
rowLockClause(skipLocked = false) {
|
|
272
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
273
|
+
},
|
|
262
274
|
timestampValue(value) {
|
|
263
275
|
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
264
276
|
},
|
|
@@ -297,6 +309,12 @@ var init_dialect = __esm(() => {
|
|
|
297
309
|
nowExpression() {
|
|
298
310
|
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
299
311
|
},
|
|
312
|
+
epochMillisecondsExpression() {
|
|
313
|
+
return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
|
|
314
|
+
},
|
|
315
|
+
rowLockClause() {
|
|
316
|
+
return "";
|
|
317
|
+
},
|
|
300
318
|
timestampValue(value) {
|
|
301
319
|
return value.toISOString();
|
|
302
320
|
},
|
|
@@ -446,6 +464,9 @@ function unwrapTransactionRollback(error, depth = 0) {
|
|
|
446
464
|
}
|
|
447
465
|
return unwrapTransactionRollback(error.cause, depth + 1);
|
|
448
466
|
}
|
|
467
|
+
function hasActiveTransaction() {
|
|
468
|
+
return hasActiveDatabaseConnection() && (transactionScopes.getStore()?.rollback.length ?? 0) > 0;
|
|
469
|
+
}
|
|
449
470
|
function requestTransactionRollback() {
|
|
450
471
|
const state = transactionScopes.getStore();
|
|
451
472
|
const index = state ? state.rollback.length - 1 : -1;
|
|
@@ -499,6 +520,7 @@ async function runWithTransactionScope(callback) {
|
|
|
499
520
|
var TransactionRollback, transactionScopes;
|
|
500
521
|
var init_transactionControl = __esm(() => {
|
|
501
522
|
init_asyncContextStore();
|
|
523
|
+
init_connectionContext();
|
|
502
524
|
TransactionRollback = class TransactionRollback extends Error {
|
|
503
525
|
result;
|
|
504
526
|
constructor(result) {
|
|
@@ -711,6 +733,11 @@ var init_transaction = __esm(() => {
|
|
|
711
733
|
});
|
|
712
734
|
|
|
713
735
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
736
|
+
async function applyBypassToTransaction(transaction, bypass) {
|
|
737
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, [
|
|
738
|
+
bypass ? "true" : "false"
|
|
739
|
+
]);
|
|
740
|
+
}
|
|
714
741
|
function stringifyBypassIdentifier(identifier) {
|
|
715
742
|
if (typeof identifier === "number") {
|
|
716
743
|
return String(identifier);
|
|
@@ -763,6 +790,9 @@ async function runWithScopedTenantTransaction(apply, callback) {
|
|
|
763
790
|
});
|
|
764
791
|
}));
|
|
765
792
|
}
|
|
793
|
+
async function runWithMigrationBypass(callback) {
|
|
794
|
+
return await runWithScopedTenantTransaction((transaction) => applyBypassToTransaction(transaction, true), callback);
|
|
795
|
+
}
|
|
766
796
|
async function runWithMigrationBypassForIdentifier(identifier, callback) {
|
|
767
797
|
assertBypassIdentifier(identifier);
|
|
768
798
|
return await runWithScopedTenantTransaction((transaction) => applyIdentifierToTransaction(transaction, identifier), callback);
|
|
@@ -2349,50 +2379,133 @@ function resetDnsLookupForTests() {
|
|
|
2349
2379
|
|
|
2350
2380
|
// ../../src/core/security/safeFetch.ts
|
|
2351
2381
|
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
2382
|
+
function untilAborted(work, signal) {
|
|
2383
|
+
signal.throwIfAborted();
|
|
2384
|
+
return new Promise((resolve, reject) => {
|
|
2385
|
+
const onAbort = () => reject(signal.reason);
|
|
2386
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2387
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
function preserveResponseMetadata(response, original) {
|
|
2391
|
+
Object.defineProperties(response, {
|
|
2392
|
+
url: { value: original.url },
|
|
2393
|
+
redirected: { value: original.redirected },
|
|
2394
|
+
type: { value: original.type }
|
|
2395
|
+
});
|
|
2396
|
+
const clone = response.clone.bind(response);
|
|
2397
|
+
response.clone = () => preserveResponseMetadata(clone(), original);
|
|
2398
|
+
return response;
|
|
2399
|
+
}
|
|
2400
|
+
function withBodyDeadline(response, signal, release) {
|
|
2401
|
+
if (!response.body) {
|
|
2402
|
+
release();
|
|
2403
|
+
return response;
|
|
2404
|
+
}
|
|
2405
|
+
const reader = response.body.getReader();
|
|
2406
|
+
let stopped = false;
|
|
2407
|
+
let bodyController;
|
|
2408
|
+
const stop = (reason, errorBody) => {
|
|
2409
|
+
if (stopped)
|
|
2410
|
+
return;
|
|
2411
|
+
stopped = true;
|
|
2412
|
+
release();
|
|
2413
|
+
signal.removeEventListener("abort", onAbort);
|
|
2414
|
+
if (errorBody)
|
|
2415
|
+
bodyController.error(reason);
|
|
2416
|
+
reader.cancel(reason).catch(() => {}).finally(() => reader.releaseLock());
|
|
2417
|
+
};
|
|
2418
|
+
const onAbort = () => stop(signal.reason, true);
|
|
2419
|
+
const body = new ReadableStream({
|
|
2420
|
+
start(controller) {
|
|
2421
|
+
bodyController = controller;
|
|
2422
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2423
|
+
if (signal.aborted)
|
|
2424
|
+
onAbort();
|
|
2425
|
+
},
|
|
2426
|
+
async pull(controller) {
|
|
2427
|
+
try {
|
|
2428
|
+
const result = await untilAborted(reader.read(), signal);
|
|
2429
|
+
if (stopped)
|
|
2430
|
+
return;
|
|
2431
|
+
if (result.done) {
|
|
2432
|
+
stopped = true;
|
|
2433
|
+
release();
|
|
2434
|
+
signal.removeEventListener("abort", onAbort);
|
|
2435
|
+
reader.releaseLock();
|
|
2436
|
+
controller.close();
|
|
2437
|
+
} else {
|
|
2438
|
+
controller.enqueue(result.value);
|
|
2439
|
+
}
|
|
2440
|
+
} catch (error) {
|
|
2441
|
+
stop(error, true);
|
|
2442
|
+
}
|
|
2443
|
+
},
|
|
2444
|
+
cancel(reason) {
|
|
2445
|
+
stop(reason, false);
|
|
2446
|
+
}
|
|
2447
|
+
}, { highWaterMark: 0 });
|
|
2448
|
+
return preserveResponseMetadata(new Response(body, {
|
|
2449
|
+
status: response.status,
|
|
2450
|
+
statusText: response.statusText,
|
|
2451
|
+
headers: response.headers
|
|
2452
|
+
}), response);
|
|
2453
|
+
}
|
|
2352
2454
|
async function safeFetch(input, init = {}, options = {}) {
|
|
2455
|
+
init.signal?.throwIfAborted();
|
|
2353
2456
|
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
2457
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
2458
|
+
throw new RangeError("safeFetch timeoutMs must be a positive finite number.");
|
|
2459
|
+
}
|
|
2354
2460
|
const maxRedirects = options.maxRedirects ?? 0;
|
|
2355
|
-
const resolveDns = options.resolveDns ?? true;
|
|
2356
2461
|
const urlOptions = {
|
|
2357
2462
|
allowHttp: options.allowHttp,
|
|
2358
|
-
resolveDns,
|
|
2463
|
+
resolveDns: options.resolveDns ?? true,
|
|
2359
2464
|
allowPrivate: options.allowPrivate
|
|
2360
2465
|
};
|
|
2361
2466
|
const controller = new AbortController;
|
|
2362
|
-
const
|
|
2467
|
+
const forwardAbort = () => controller.abort(init.signal?.reason);
|
|
2468
|
+
init.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
2469
|
+
const timeout = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
|
|
2470
|
+
const release = () => {
|
|
2471
|
+
clearTimeout(timeout);
|
|
2472
|
+
init.signal?.removeEventListener("abort", forwardAbort);
|
|
2473
|
+
};
|
|
2363
2474
|
try {
|
|
2364
|
-
let current = await resolveSafeOutboundTarget(input, urlOptions);
|
|
2475
|
+
let current = await untilAborted(resolveSafeOutboundTarget(input, urlOptions), controller.signal);
|
|
2365
2476
|
let redirectCount = 0;
|
|
2366
2477
|
while (true) {
|
|
2478
|
+
controller.signal.throwIfAborted();
|
|
2367
2479
|
const address = current.addresses[0];
|
|
2368
2480
|
const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
|
|
2369
2481
|
const headers = new Headers(init.headers);
|
|
2370
|
-
if (address && !headers.has("host"))
|
|
2482
|
+
if (address && !headers.has("host"))
|
|
2371
2483
|
headers.set("Host", current.url.host);
|
|
2372
|
-
}
|
|
2373
2484
|
const fetchInit = {
|
|
2374
2485
|
...init,
|
|
2375
2486
|
headers,
|
|
2376
2487
|
signal: controller.signal,
|
|
2377
2488
|
redirect: "manual"
|
|
2378
2489
|
};
|
|
2379
|
-
if (address)
|
|
2490
|
+
if (address)
|
|
2380
2491
|
fetchInit.tls = { serverName: current.url.hostname };
|
|
2381
|
-
|
|
2382
|
-
const response = await fetch(fetchUrl, fetchInit);
|
|
2492
|
+
const response = await untilAborted(fetch(fetchUrl, fetchInit), controller.signal);
|
|
2383
2493
|
if (response.status >= 300 && response.status < 400) {
|
|
2384
2494
|
const location = response.headers.get("location");
|
|
2385
|
-
if (
|
|
2386
|
-
|
|
2495
|
+
if (location && redirectCount < maxRedirects) {
|
|
2496
|
+
if (response.body)
|
|
2497
|
+
await untilAborted(response.body.cancel(), controller.signal);
|
|
2498
|
+
current = await untilAborted(resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions), controller.signal);
|
|
2499
|
+
redirectCount++;
|
|
2500
|
+
continue;
|
|
2387
2501
|
}
|
|
2388
|
-
current = await resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions);
|
|
2389
|
-
redirectCount += 1;
|
|
2390
|
-
continue;
|
|
2391
2502
|
}
|
|
2392
|
-
return response;
|
|
2503
|
+
return withBodyDeadline(response, controller.signal, release);
|
|
2393
2504
|
}
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2505
|
+
} catch (error) {
|
|
2506
|
+
controller.abort(error);
|
|
2507
|
+
release();
|
|
2508
|
+
throw error;
|
|
2396
2509
|
}
|
|
2397
2510
|
}
|
|
2398
2511
|
|
|
@@ -7269,6 +7382,7 @@ function registerModelRepository(model, repository) {
|
|
|
7269
7382
|
return model;
|
|
7270
7383
|
}
|
|
7271
7384
|
// ../../src/core/database/mysqlConnection.ts
|
|
7385
|
+
init_connectionContext();
|
|
7272
7386
|
var MYSQL_SESSION_UTC = "SET time_zone = '+00:00'";
|
|
7273
7387
|
var mysqlModule;
|
|
7274
7388
|
var mysqlPending;
|
|
@@ -7324,18 +7438,71 @@ function rowsFromResult(result) {
|
|
|
7324
7438
|
}
|
|
7325
7439
|
return [];
|
|
7326
7440
|
}
|
|
7327
|
-
function
|
|
7328
|
-
|
|
7441
|
+
async function executeMysql(target, query, params) {
|
|
7442
|
+
const [result] = params.length === 0 && target.query ? await target.query(query) : await target.execute(query, [...params]);
|
|
7443
|
+
return rowsFromResult(result);
|
|
7444
|
+
}
|
|
7445
|
+
function createMysqlAdapter(getPool, close) {
|
|
7446
|
+
const transactions = new WeakSet;
|
|
7447
|
+
const connection = {
|
|
7329
7448
|
async unsafe(query, params = []) {
|
|
7330
|
-
const
|
|
7331
|
-
|
|
7449
|
+
const active = getActiveDatabaseConnection(connection);
|
|
7450
|
+
if (transactions.has(active))
|
|
7451
|
+
return active.unsafe(query, params);
|
|
7452
|
+
return executeMysql(await getPool(), query, params);
|
|
7332
7453
|
},
|
|
7333
|
-
async
|
|
7334
|
-
if (
|
|
7335
|
-
|
|
7454
|
+
async begin(callback) {
|
|
7455
|
+
if (transactions.has(getActiveDatabaseConnection(connection))) {
|
|
7456
|
+
throw new Error("Use runInTransaction for nested MySQL transactions.");
|
|
7336
7457
|
}
|
|
7337
|
-
|
|
7458
|
+
const pool = await getPool();
|
|
7459
|
+
if (!pool.getConnection)
|
|
7460
|
+
throw new Error("MySQL transactions require a pool with getConnection().");
|
|
7461
|
+
const reserved = await pool.getConnection();
|
|
7462
|
+
let open = false;
|
|
7463
|
+
let discard = false;
|
|
7464
|
+
const transaction = {
|
|
7465
|
+
async unsafe(query, params = []) {
|
|
7466
|
+
if (!open)
|
|
7467
|
+
throw new Error("MySQL transaction is no longer active.");
|
|
7468
|
+
return executeMysql(reserved, query, params);
|
|
7469
|
+
}
|
|
7470
|
+
};
|
|
7471
|
+
transactions.add(transaction);
|
|
7472
|
+
try {
|
|
7473
|
+
await reserved.beginTransaction();
|
|
7474
|
+
open = true;
|
|
7475
|
+
const result = await runWithDatabaseConnection(transaction, () => callback(transaction));
|
|
7476
|
+
await reserved.commit();
|
|
7477
|
+
return result;
|
|
7478
|
+
} catch (error) {
|
|
7479
|
+
if (open) {
|
|
7480
|
+
try {
|
|
7481
|
+
await reserved.rollback();
|
|
7482
|
+
} catch (rollbackError) {
|
|
7483
|
+
discard = true;
|
|
7484
|
+
reserved.destroy();
|
|
7485
|
+
throw new AggregateError([error, rollbackError], "MySQL rollback failed; connection discarded.");
|
|
7486
|
+
}
|
|
7487
|
+
} else {
|
|
7488
|
+
discard = true;
|
|
7489
|
+
reserved.destroy();
|
|
7490
|
+
}
|
|
7491
|
+
throw error;
|
|
7492
|
+
} finally {
|
|
7493
|
+
open = false;
|
|
7494
|
+
if (!discard)
|
|
7495
|
+
reserved.release();
|
|
7496
|
+
}
|
|
7497
|
+
},
|
|
7498
|
+
close
|
|
7338
7499
|
};
|
|
7500
|
+
return connection;
|
|
7501
|
+
}
|
|
7502
|
+
function createMysqlConnectionFromPool(pool) {
|
|
7503
|
+
return createMysqlAdapter(async () => pool, async () => {
|
|
7504
|
+
await pool.end?.();
|
|
7505
|
+
});
|
|
7339
7506
|
}
|
|
7340
7507
|
function pinSessionToUtc(connection) {
|
|
7341
7508
|
connection.query(MYSQL_SESSION_UTC, (error) => {
|
|
@@ -7368,23 +7535,16 @@ function createMysqlConnection(url) {
|
|
|
7368
7535
|
}
|
|
7369
7536
|
return poolPending;
|
|
7370
7537
|
}
|
|
7371
|
-
return {
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
poolPending = undefined;
|
|
7382
|
-
const pool = await pending.catch(() => {
|
|
7383
|
-
return;
|
|
7384
|
-
});
|
|
7385
|
-
await pool?.end();
|
|
7386
|
-
}
|
|
7387
|
-
};
|
|
7538
|
+
return createMysqlAdapter(ensurePool, async () => {
|
|
7539
|
+
if (!poolPending)
|
|
7540
|
+
return;
|
|
7541
|
+
const pending = poolPending;
|
|
7542
|
+
poolPending = undefined;
|
|
7543
|
+
const pool = await pending.catch(() => {
|
|
7544
|
+
return;
|
|
7545
|
+
});
|
|
7546
|
+
await pool?.end();
|
|
7547
|
+
});
|
|
7388
7548
|
}
|
|
7389
7549
|
// ../../src/core/database/namedConnections.ts
|
|
7390
7550
|
init_connectionContext();
|
|
@@ -8046,6 +8206,7 @@ async function runSeedersFromDirectory(directory, db, options) {
|
|
|
8046
8206
|
return seeders.length;
|
|
8047
8207
|
}
|
|
8048
8208
|
// ../../src/core/database/sqliteConnection.ts
|
|
8209
|
+
init_connectionContext();
|
|
8049
8210
|
import { Database } from "bun:sqlite";
|
|
8050
8211
|
function isRowReturning(sql) {
|
|
8051
8212
|
const upper = sql.replace(/\s+/g, " ").trim().toUpperCase();
|
|
@@ -8065,20 +8226,70 @@ function createSqliteConnection(filename) {
|
|
|
8065
8226
|
db.exec("PRAGMA journal_mode = WAL");
|
|
8066
8227
|
db.exec("PRAGMA synchronous = NORMAL");
|
|
8067
8228
|
}
|
|
8068
|
-
|
|
8229
|
+
let tail = Promise.resolve();
|
|
8230
|
+
let pending = 0;
|
|
8231
|
+
let closed = false;
|
|
8232
|
+
const transactions = new WeakSet;
|
|
8233
|
+
function execute(query, params) {
|
|
8234
|
+
const statement = db.query(query);
|
|
8235
|
+
const args = [...params];
|
|
8236
|
+
if (isRowReturning(query))
|
|
8237
|
+
return statement.all(...args);
|
|
8238
|
+
statement.run(...args);
|
|
8239
|
+
return [];
|
|
8240
|
+
}
|
|
8241
|
+
function enqueue(operation) {
|
|
8242
|
+
if (closed)
|
|
8243
|
+
return Promise.reject(new Error("SQLite connection is closed."));
|
|
8244
|
+
pending++;
|
|
8245
|
+
const result = tail.then(operation).finally(() => {
|
|
8246
|
+
pending--;
|
|
8247
|
+
});
|
|
8248
|
+
tail = result.then(() => {}, () => {});
|
|
8249
|
+
return result;
|
|
8250
|
+
}
|
|
8251
|
+
const connection = {
|
|
8069
8252
|
async unsafe(query, params = []) {
|
|
8070
|
-
const
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8253
|
+
const active = getActiveDatabaseConnection(connection);
|
|
8254
|
+
if (transactions.has(active))
|
|
8255
|
+
return active.unsafe(query, params);
|
|
8256
|
+
return enqueue(() => execute(query, params));
|
|
8257
|
+
},
|
|
8258
|
+
async begin(callback) {
|
|
8259
|
+
if (transactions.has(getActiveDatabaseConnection(connection))) {
|
|
8260
|
+
throw new Error("Use runInTransaction for nested SQLite transactions.");
|
|
8074
8261
|
}
|
|
8075
|
-
|
|
8076
|
-
|
|
8262
|
+
return enqueue(async () => {
|
|
8263
|
+
db.exec("BEGIN IMMEDIATE");
|
|
8264
|
+
let open = true;
|
|
8265
|
+
const transaction = {
|
|
8266
|
+
async unsafe(query, params = []) {
|
|
8267
|
+
if (!open)
|
|
8268
|
+
throw new Error("SQLite transaction is no longer active.");
|
|
8269
|
+
return execute(query, params);
|
|
8270
|
+
}
|
|
8271
|
+
};
|
|
8272
|
+
transactions.add(transaction);
|
|
8273
|
+
try {
|
|
8274
|
+
const result = await runWithDatabaseConnection(transaction, () => callback(transaction));
|
|
8275
|
+
db.exec("COMMIT");
|
|
8276
|
+
return result;
|
|
8277
|
+
} catch (error) {
|
|
8278
|
+
db.exec("ROLLBACK");
|
|
8279
|
+
throw error;
|
|
8280
|
+
} finally {
|
|
8281
|
+
open = false;
|
|
8282
|
+
}
|
|
8283
|
+
});
|
|
8077
8284
|
},
|
|
8078
8285
|
close() {
|
|
8286
|
+
if (pending > 0)
|
|
8287
|
+
throw new Error("Drain SQLite operations before closing the connection.");
|
|
8288
|
+
closed = true;
|
|
8079
8289
|
db.close();
|
|
8080
8290
|
}
|
|
8081
8291
|
};
|
|
8292
|
+
return connection;
|
|
8082
8293
|
}
|
|
8083
8294
|
// ../../src/core/database/table.ts
|
|
8084
8295
|
function defineTable(definition) {
|
|
@@ -8089,6 +8300,333 @@ function defineTable(definition) {
|
|
|
8089
8300
|
init_transaction();
|
|
8090
8301
|
init_eventBus();
|
|
8091
8302
|
|
|
8303
|
+
// ../../src/core/events/outbox/index.ts
|
|
8304
|
+
init_dialect();
|
|
8305
|
+
init_repositoryConnection();
|
|
8306
|
+
init_transaction();
|
|
8307
|
+
init_databaseTenantContext();
|
|
8308
|
+
init_tenantContext();
|
|
8309
|
+
init_tenantDatabaseScope();
|
|
8310
|
+
|
|
8311
|
+
// ../../src/core/events/outbox/migration.ts
|
|
8312
|
+
function createOutboxMigration(name, driver, options = {}) {
|
|
8313
|
+
const identity = driver === "mysql" ? " CHARACTER SET ascii COLLATE ascii_bin" : "";
|
|
8314
|
+
const payloadType = driver === "mysql" ? "MEDIUMTEXT" : "TEXT";
|
|
8315
|
+
const inlineIndex = driver === "mysql" ? ", INDEX strata_outbox_due (status, available_at, event_id, listener_name), INDEX strata_outbox_expired (status, lease_until, event_id, listener_name)" : "";
|
|
8316
|
+
return {
|
|
8317
|
+
name,
|
|
8318
|
+
async up(db) {
|
|
8319
|
+
await db.unsafe(`CREATE TABLE IF NOT EXISTS strata_outbox_event (
|
|
8320
|
+
id VARCHAR(64)${identity} PRIMARY KEY, name VARCHAR(128)${identity} NOT NULL, version INTEGER NOT NULL CHECK (version > 0),
|
|
8321
|
+
tenant_id BIGINT, payload ${payloadType} NOT NULL, created_at BIGINT NOT NULL,
|
|
8322
|
+
publication_token VARCHAR(64)${identity} NOT NULL)`);
|
|
8323
|
+
await db.unsafe(`CREATE TABLE IF NOT EXISTS strata_outbox_delivery (
|
|
8324
|
+
event_id VARCHAR(64)${identity} NOT NULL,
|
|
8325
|
+
listener_name VARCHAR(128)${identity} NOT NULL, status VARCHAR(16) NOT NULL CHECK (status IN ('pending', 'processing', 'failed', 'completed')),
|
|
8326
|
+
attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0), max_attempts INTEGER NOT NULL CHECK (max_attempts > 0),
|
|
8327
|
+
available_at BIGINT NOT NULL, lease_token VARCHAR(64)${identity}, lease_until BIGINT,
|
|
8328
|
+
completed_at BIGINT, error_code VARCHAR(64), PRIMARY KEY (event_id, listener_name)${inlineIndex},
|
|
8329
|
+
FOREIGN KEY (event_id) REFERENCES strata_outbox_event(id) ON DELETE CASCADE)`);
|
|
8330
|
+
if (driver !== "mysql") {
|
|
8331
|
+
await db.unsafe("CREATE INDEX IF NOT EXISTS strata_outbox_due ON strata_outbox_delivery (status, available_at, event_id, listener_name)");
|
|
8332
|
+
await db.unsafe("CREATE INDEX IF NOT EXISTS strata_outbox_expired ON strata_outbox_delivery (status, lease_until, event_id, listener_name)");
|
|
8333
|
+
}
|
|
8334
|
+
if (driver === "pgsql" && options.rls) {
|
|
8335
|
+
await db.unsafe(`ALTER TABLE strata_outbox_event ENABLE ROW LEVEL SECURITY`);
|
|
8336
|
+
await db.unsafe(`ALTER TABLE strata_outbox_event FORCE ROW LEVEL SECURITY`);
|
|
8337
|
+
await db.unsafe(`DROP POLICY IF EXISTS strata_outbox_scope ON strata_outbox_event`);
|
|
8338
|
+
await db.unsafe(`CREATE POLICY strata_outbox_scope ON strata_outbox_event USING (
|
|
8339
|
+
current_setting('app.bypass_rls', true) = 'true' OR
|
|
8340
|
+
tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::bigint OR
|
|
8341
|
+
(tenant_id IS NULL AND NULLIF(current_setting('app.tenant_id', true), '') IS NULL))`);
|
|
8342
|
+
await db.unsafe(`ALTER TABLE strata_outbox_delivery ENABLE ROW LEVEL SECURITY`);
|
|
8343
|
+
await db.unsafe(`ALTER TABLE strata_outbox_delivery FORCE ROW LEVEL SECURITY`);
|
|
8344
|
+
await db.unsafe(`DROP POLICY IF EXISTS strata_outbox_scope ON strata_outbox_delivery`);
|
|
8345
|
+
await db.unsafe(`CREATE POLICY strata_outbox_scope ON strata_outbox_delivery USING (
|
|
8346
|
+
current_setting('app.bypass_rls', true) = 'true' OR EXISTS (
|
|
8347
|
+
SELECT 1 FROM strata_outbox_event WHERE id = strata_outbox_delivery.event_id))`);
|
|
8348
|
+
}
|
|
8349
|
+
},
|
|
8350
|
+
async down(db) {
|
|
8351
|
+
const [row] = await db.unsafe("SELECT COUNT(*) AS pending FROM strata_outbox_delivery WHERE status <> 'completed'");
|
|
8352
|
+
if (Number(row?.pending) > 0)
|
|
8353
|
+
throw new Error("Cannot drop outbox tables while undelivered events remain.");
|
|
8354
|
+
await db.unsafe("DROP TABLE strata_outbox_delivery");
|
|
8355
|
+
await db.unsafe("DROP TABLE strata_outbox_event");
|
|
8356
|
+
}
|
|
8357
|
+
};
|
|
8358
|
+
}
|
|
8359
|
+
|
|
8360
|
+
// ../../src/core/events/outbox/index.ts
|
|
8361
|
+
function positiveInteger(value, name) {
|
|
8362
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
8363
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
8364
|
+
return value;
|
|
8365
|
+
}
|
|
8366
|
+
function identifier(value, max) {
|
|
8367
|
+
if (!value || value.length > max || !/^[\w.:-]+$/.test(value))
|
|
8368
|
+
throw new Error("Invalid durable event/listener identifier.");
|
|
8369
|
+
return value;
|
|
8370
|
+
}
|
|
8371
|
+
function placeholders(count) {
|
|
8372
|
+
return Array.from({ length: count }, (_, index) => currentSqlDialect().placeholder(index + 1));
|
|
8373
|
+
}
|
|
8374
|
+
async function clock() {
|
|
8375
|
+
const [row] = await repositoryConnection.unsafe(`SELECT ${currentSqlDialect().epochMillisecondsExpression()} AS now_ms`);
|
|
8376
|
+
const now = Number(row?.now_ms);
|
|
8377
|
+
if (!Number.isSafeInteger(now))
|
|
8378
|
+
throw new Error("Database did not return a valid outbox clock.");
|
|
8379
|
+
return now;
|
|
8380
|
+
}
|
|
8381
|
+
async function privileged(work) {
|
|
8382
|
+
if (hasActiveTransaction())
|
|
8383
|
+
throw new Error("Outbox coordination must run outside business transactions.");
|
|
8384
|
+
for (let attempt = 0;; attempt++) {
|
|
8385
|
+
try {
|
|
8386
|
+
return await runWithMigrationBypass(() => runInTransaction(work));
|
|
8387
|
+
} catch (error) {
|
|
8388
|
+
const failure = error !== null && typeof error === "object" ? error : {};
|
|
8389
|
+
const code = String(failure.errno ?? failure.code);
|
|
8390
|
+
if (attempt >= 4 || !["40001", "40P01", "1213"].includes(code))
|
|
8391
|
+
throw error;
|
|
8392
|
+
await Bun.sleep(10 * (attempt + 1));
|
|
8393
|
+
}
|
|
8394
|
+
}
|
|
8395
|
+
}
|
|
8396
|
+
|
|
8397
|
+
class SqlOutbox {
|
|
8398
|
+
listeners = new Map;
|
|
8399
|
+
leaseMs;
|
|
8400
|
+
maxAttempts;
|
|
8401
|
+
retryDelayMs;
|
|
8402
|
+
maxPayloadBytes;
|
|
8403
|
+
tenantResolver;
|
|
8404
|
+
constructor(options) {
|
|
8405
|
+
this.leaseMs = positiveInteger(options.leaseMs ?? 60000, "leaseMs");
|
|
8406
|
+
if (this.leaseMs < 30)
|
|
8407
|
+
throw new Error("leaseMs must be at least 30 milliseconds.");
|
|
8408
|
+
this.maxAttempts = positiveInteger(options.maxAttempts ?? 5, "maxAttempts");
|
|
8409
|
+
this.retryDelayMs = positiveInteger(options.retryDelayMs ?? 1000, "retryDelayMs");
|
|
8410
|
+
this.maxPayloadBytes = positiveInteger(options.maxPayloadBytes ?? 65536, "maxPayloadBytes");
|
|
8411
|
+
this.tenantResolver = options.resolveTenant ?? resolveTenant;
|
|
8412
|
+
for (const listener of options.listeners) {
|
|
8413
|
+
identifier(listener.name, 128);
|
|
8414
|
+
identifier(listener.event, 128);
|
|
8415
|
+
if (this.listeners.has(listener.name))
|
|
8416
|
+
throw new Error("Durable listener names must be unique.");
|
|
8417
|
+
this.listeners.set(listener.name, listener);
|
|
8418
|
+
}
|
|
8419
|
+
}
|
|
8420
|
+
async publish(name, payload, options = {}) {
|
|
8421
|
+
if (!hasActiveTransaction())
|
|
8422
|
+
throw new Error("Durable publication requires an active business transaction.");
|
|
8423
|
+
return runInTransaction(() => this.persist(name, payload, options));
|
|
8424
|
+
}
|
|
8425
|
+
async persist(name, payload, options) {
|
|
8426
|
+
identifier(name, 128);
|
|
8427
|
+
const id = identifier(options.id ?? crypto.randomUUID(), 64);
|
|
8428
|
+
const version = positiveInteger(options.version ?? 1, "version");
|
|
8429
|
+
const serialized = JSON.stringify(payload);
|
|
8430
|
+
if (serialized === undefined || new TextEncoder().encode(serialized).byteLength > this.maxPayloadBytes)
|
|
8431
|
+
throw new Error("Invalid or oversized durable payload.");
|
|
8432
|
+
const listeners = [...this.listeners.values()].filter((listener) => listener.event === name);
|
|
8433
|
+
if (listeners.length === 0)
|
|
8434
|
+
throw new Error("No durable listeners registered for this event.");
|
|
8435
|
+
const tenantId = currentTenant()?.id ?? null;
|
|
8436
|
+
if (tenantId !== null)
|
|
8437
|
+
positiveInteger(tenantId, "tenantId");
|
|
8438
|
+
const token = crypto.randomUUID();
|
|
8439
|
+
const now = await clock();
|
|
8440
|
+
await repositoryConnection.unsafe(`INSERT INTO strata_outbox_event (id, name, version, tenant_id, payload, created_at, publication_token)
|
|
8441
|
+
VALUES (${placeholders(7).join(", ")})${currentSqlDialect().upsertSuffix(["id"], [])}`, [id, name, version, tenantId, serialized, now, token]);
|
|
8442
|
+
const [existing] = await repositoryConnection.unsafe(`SELECT * FROM strata_outbox_event WHERE id = ${placeholders(1)[0]}`, [id]);
|
|
8443
|
+
if (!existing || existing.name !== name || Number(existing.version) !== version || (existing.tenant_id === null ? null : Number(existing.tenant_id)) !== tenantId || existing.payload !== serialized)
|
|
8444
|
+
throw new Error("Conflicting durable event ID.");
|
|
8445
|
+
if (existing.publication_token !== token)
|
|
8446
|
+
return id;
|
|
8447
|
+
for (const listener of listeners) {
|
|
8448
|
+
await repositoryConnection.unsafe(`INSERT INTO strata_outbox_delivery (event_id, listener_name, status, max_attempts, available_at)
|
|
8449
|
+
VALUES (${placeholders(5).join(", ")})`, [id, listener.name, "pending", this.maxAttempts, now]);
|
|
8450
|
+
}
|
|
8451
|
+
return id;
|
|
8452
|
+
}
|
|
8453
|
+
async claim() {
|
|
8454
|
+
return privileged(async () => {
|
|
8455
|
+
const now = await clock();
|
|
8456
|
+
const p = placeholders(2);
|
|
8457
|
+
const columns = "event_id, listener_name, attempts, max_attempts";
|
|
8458
|
+
const [expired] = await repositoryConnection.unsafe(`SELECT ${columns} FROM strata_outbox_delivery WHERE status = 'processing' AND lease_until <= ${p[0]}
|
|
8459
|
+
ORDER BY lease_until, event_id, listener_name LIMIT 1${currentSqlDialect().rowLockClause(true)}`, [now]);
|
|
8460
|
+
const delivery = expired ?? (await repositoryConnection.unsafe(`SELECT ${columns} FROM strata_outbox_delivery WHERE status = 'pending' AND available_at <= ${p[0]}
|
|
8461
|
+
ORDER BY available_at, event_id, listener_name LIMIT 1${currentSqlDialect().rowLockClause(true)}`, [now]))[0];
|
|
8462
|
+
if (!delivery)
|
|
8463
|
+
return null;
|
|
8464
|
+
const [event] = await repositoryConnection.unsafe(`SELECT * FROM strata_outbox_event WHERE id = ${p[0]}`, [delivery.event_id]);
|
|
8465
|
+
if (!event)
|
|
8466
|
+
throw new Error("Outbox event missing for delivery.");
|
|
8467
|
+
const candidate = { ...event, ...delivery };
|
|
8468
|
+
const token = crypto.randomUUID();
|
|
8469
|
+
const exhausted = Number(candidate.attempts) >= Number(candidate.max_attempts);
|
|
8470
|
+
const q = placeholders(9);
|
|
8471
|
+
await repositoryConnection.unsafe(`UPDATE strata_outbox_delivery SET status = ${q[0]}, lease_token = ${q[1]}, lease_until = ${q[2]},
|
|
8472
|
+
attempts = ${q[3]}, error_code = ${exhausted ? "'attempts_exhausted'" : "error_code"} WHERE event_id = ${q[4]} AND listener_name = ${q[5]} AND
|
|
8473
|
+
((status = 'pending' AND available_at <= ${q[6]}) OR (status = 'processing' AND lease_until <= ${q[7]})) AND attempts = ${q[8]}`, [
|
|
8474
|
+
exhausted ? "failed" : "processing",
|
|
8475
|
+
token,
|
|
8476
|
+
exhausted ? null : now + this.leaseMs,
|
|
8477
|
+
Number(candidate.attempts) + (exhausted ? 0 : 1),
|
|
8478
|
+
candidate.id,
|
|
8479
|
+
candidate.listener_name,
|
|
8480
|
+
now,
|
|
8481
|
+
now,
|
|
8482
|
+
candidate.attempts
|
|
8483
|
+
]);
|
|
8484
|
+
const [owner] = await repositoryConnection.unsafe(`SELECT lease_token FROM strata_outbox_delivery WHERE event_id = ${p[0]} AND listener_name = ${p[1]}${currentSqlDialect().rowLockClause()}`, [candidate.id, candidate.listener_name]);
|
|
8485
|
+
if (owner?.lease_token !== token)
|
|
8486
|
+
return null;
|
|
8487
|
+
if (exhausted)
|
|
8488
|
+
return "exhausted";
|
|
8489
|
+
return { ...candidate, attempts: Number(candidate.attempts) + 1, lease_token: token };
|
|
8490
|
+
});
|
|
8491
|
+
}
|
|
8492
|
+
async renew(claim) {
|
|
8493
|
+
return privileged(async () => {
|
|
8494
|
+
const now = await clock();
|
|
8495
|
+
const p = placeholders(5);
|
|
8496
|
+
await repositoryConnection.unsafe(`UPDATE strata_outbox_delivery SET lease_until = ${p[0]} WHERE event_id = ${p[1]}
|
|
8497
|
+
AND listener_name = ${p[2]} AND lease_token = ${p[3]} AND status = 'processing' AND lease_until > ${p[4]}`, [now + this.leaseMs, claim.id, claim.listener_name, claim.lease_token, now]);
|
|
8498
|
+
const q = placeholders(3);
|
|
8499
|
+
const [owner] = await repositoryConnection.unsafe(`SELECT lease_until FROM strata_outbox_delivery WHERE event_id = ${q[0]} AND listener_name = ${q[1]} AND lease_token = ${q[2]} AND status = 'processing'${currentSqlDialect().rowLockClause()}`, [claim.id, claim.listener_name, claim.lease_token]);
|
|
8500
|
+
return Number(owner?.lease_until) > now;
|
|
8501
|
+
});
|
|
8502
|
+
}
|
|
8503
|
+
async finish(claim, success) {
|
|
8504
|
+
await privileged(async () => {
|
|
8505
|
+
const now = await clock();
|
|
8506
|
+
const status = success ? "completed" : claim.attempts >= Number(claim.max_attempts) ? "failed" : "pending";
|
|
8507
|
+
const delay = Math.min(this.retryDelayMs * 2 ** Math.min(claim.attempts - 1, 16), 300000);
|
|
8508
|
+
const p = placeholders(9);
|
|
8509
|
+
await repositoryConnection.unsafe(`UPDATE strata_outbox_delivery SET status = ${p[0]}, available_at = ${p[1]}, completed_at = ${p[2]},
|
|
8510
|
+
error_code = ${p[3]}, lease_until = NULL WHERE event_id = ${p[4]} AND listener_name = ${p[5]}
|
|
8511
|
+
AND lease_token = ${p[6]} AND status = 'processing' AND lease_until > ${p[7]} AND attempts = ${p[8]}`, [
|
|
8512
|
+
status,
|
|
8513
|
+
now + delay,
|
|
8514
|
+
success ? now : null,
|
|
8515
|
+
success ? null : "listener_failed",
|
|
8516
|
+
claim.id,
|
|
8517
|
+
claim.listener_name,
|
|
8518
|
+
claim.lease_token,
|
|
8519
|
+
now,
|
|
8520
|
+
claim.attempts
|
|
8521
|
+
]);
|
|
8522
|
+
});
|
|
8523
|
+
}
|
|
8524
|
+
async processNext(options = {}) {
|
|
8525
|
+
if (hasActiveTransaction())
|
|
8526
|
+
throw new Error("Outbox workers must run outside business transactions.");
|
|
8527
|
+
options.signal?.throwIfAborted();
|
|
8528
|
+
const claim = await this.claim();
|
|
8529
|
+
if (!claim)
|
|
8530
|
+
return false;
|
|
8531
|
+
if (claim === "exhausted")
|
|
8532
|
+
return true;
|
|
8533
|
+
const controller = new AbortController;
|
|
8534
|
+
const forward = () => controller.abort(options.signal?.reason);
|
|
8535
|
+
options.signal?.addEventListener("abort", forward, { once: true });
|
|
8536
|
+
if (options.signal?.aborted)
|
|
8537
|
+
forward();
|
|
8538
|
+
let renewal;
|
|
8539
|
+
let watchdog = setTimeout(() => controller.abort(new Error("Outbox lease expired.")), this.leaseMs);
|
|
8540
|
+
const heartbeat = setInterval(() => {
|
|
8541
|
+
if (renewal || controller.signal.aborted)
|
|
8542
|
+
return;
|
|
8543
|
+
renewal = (async () => {
|
|
8544
|
+
try {
|
|
8545
|
+
if (!await this.renew(claim))
|
|
8546
|
+
controller.abort(new Error("Outbox ownership lost."));
|
|
8547
|
+
else {
|
|
8548
|
+
clearTimeout(watchdog);
|
|
8549
|
+
watchdog = setTimeout(() => controller.abort(new Error("Outbox lease expired.")), this.leaseMs);
|
|
8550
|
+
}
|
|
8551
|
+
} catch {
|
|
8552
|
+
controller.abort(new Error("Outbox lease renewal failed."));
|
|
8553
|
+
} finally {
|
|
8554
|
+
renewal = undefined;
|
|
8555
|
+
}
|
|
8556
|
+
})();
|
|
8557
|
+
}, Math.max(10, Math.floor(this.leaseMs / 3)));
|
|
8558
|
+
let success = false;
|
|
8559
|
+
try {
|
|
8560
|
+
const listener = this.listeners.get(claim.listener_name);
|
|
8561
|
+
if (!listener || listener.event !== claim.name)
|
|
8562
|
+
throw new Error("Durable listener is not registered.");
|
|
8563
|
+
const event = {
|
|
8564
|
+
id: claim.id,
|
|
8565
|
+
name: claim.name,
|
|
8566
|
+
version: Number(claim.version),
|
|
8567
|
+
tenantId: claim.tenant_id === null ? null : Number(claim.tenant_id),
|
|
8568
|
+
payload: JSON.parse(claim.payload)
|
|
8569
|
+
};
|
|
8570
|
+
const tenant = event.tenantId === null ? null : await privileged(() => this.tenantResolver(Number(event.tenantId)));
|
|
8571
|
+
if (event.tenantId !== null && (!tenant || tenant.id !== event.tenantId))
|
|
8572
|
+
throw new Error("Outbox tenant could not be resolved.");
|
|
8573
|
+
const handle = async () => {
|
|
8574
|
+
controller.signal.throwIfAborted();
|
|
8575
|
+
await listener.handle(event, { signal: controller.signal });
|
|
8576
|
+
controller.signal.throwIfAborted();
|
|
8577
|
+
};
|
|
8578
|
+
if (tenant)
|
|
8579
|
+
await runWithTenantDatabase(tenant, handle);
|
|
8580
|
+
else
|
|
8581
|
+
await handle();
|
|
8582
|
+
success = true;
|
|
8583
|
+
} catch {
|
|
8584
|
+
success = false;
|
|
8585
|
+
} finally {
|
|
8586
|
+
clearInterval(heartbeat);
|
|
8587
|
+
await renewal;
|
|
8588
|
+
clearTimeout(watchdog);
|
|
8589
|
+
options.signal?.removeEventListener("abort", forward);
|
|
8590
|
+
}
|
|
8591
|
+
await this.finish(claim, success && !controller.signal.aborted);
|
|
8592
|
+
return true;
|
|
8593
|
+
}
|
|
8594
|
+
async work(options) {
|
|
8595
|
+
const pollMs = positiveInteger(options.pollMs ?? 1000, "pollMs");
|
|
8596
|
+
while (!options.signal.aborted) {
|
|
8597
|
+
try {
|
|
8598
|
+
if (await this.processNext({ signal: options.signal }))
|
|
8599
|
+
continue;
|
|
8600
|
+
} catch (error) {
|
|
8601
|
+
if (options.signal.aborted)
|
|
8602
|
+
return;
|
|
8603
|
+
throw error;
|
|
8604
|
+
}
|
|
8605
|
+
await new Promise((resolve) => {
|
|
8606
|
+
const done = () => {
|
|
8607
|
+
clearTimeout(timer);
|
|
8608
|
+
options.signal.removeEventListener("abort", done);
|
|
8609
|
+
resolve();
|
|
8610
|
+
};
|
|
8611
|
+
const timer = setTimeout(done, pollMs);
|
|
8612
|
+
options.signal.addEventListener("abort", done, { once: true });
|
|
8613
|
+
if (options.signal.aborted)
|
|
8614
|
+
done();
|
|
8615
|
+
});
|
|
8616
|
+
}
|
|
8617
|
+
}
|
|
8618
|
+
async replay(eventId, listenerName) {
|
|
8619
|
+
identifier(eventId, 64);
|
|
8620
|
+
identifier(listenerName, 128);
|
|
8621
|
+
await privileged(async () => {
|
|
8622
|
+
const now = await clock();
|
|
8623
|
+
const p = placeholders(3);
|
|
8624
|
+
await repositoryConnection.unsafe(`UPDATE strata_outbox_delivery SET status = 'pending', attempts = 0, available_at = ${p[0]},
|
|
8625
|
+
lease_token = NULL, lease_until = NULL, completed_at = NULL, error_code = NULL
|
|
8626
|
+
WHERE event_id = ${p[1]} AND listener_name = ${p[2]} AND status = 'failed'`, [now, eventId, listenerName]);
|
|
8627
|
+
});
|
|
8628
|
+
}
|
|
8629
|
+
}
|
|
8092
8630
|
// ../../src/core/mail/mailer.ts
|
|
8093
8631
|
function resolveSmtpConfig() {
|
|
8094
8632
|
const host = process.env.MAIL_HOST?.trim();
|
|
@@ -9895,7 +10433,7 @@ function logServerError(error, mappedError) {
|
|
|
9895
10433
|
});
|
|
9896
10434
|
}
|
|
9897
10435
|
async function webErrorResponse(error, request) {
|
|
9898
|
-
if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
10436
|
+
if (currentRequestMeta().errorFormat === "json" || !request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
9899
10437
|
return null;
|
|
9900
10438
|
}
|
|
9901
10439
|
const mappedError = toHttpError(error) ?? mapDatabaseError(error);
|
|
@@ -12408,6 +12946,7 @@ export {
|
|
|
12408
12946
|
Schedule,
|
|
12409
12947
|
Schema,
|
|
12410
12948
|
ServiceContainer,
|
|
12949
|
+
SqlOutbox,
|
|
12411
12950
|
SqliteGrammar,
|
|
12412
12951
|
StorageManager,
|
|
12413
12952
|
SyncQueue,
|
|
@@ -12502,6 +13041,7 @@ export {
|
|
|
12502
13041
|
createMysqlPool,
|
|
12503
13042
|
createNotificationDispatcher,
|
|
12504
13043
|
createOidcHandshake,
|
|
13044
|
+
createOutboxMigration,
|
|
12505
13045
|
createProductionQueue,
|
|
12506
13046
|
createQueue,
|
|
12507
13047
|
createQueueWorker,
|
|
@@ -12567,6 +13107,7 @@ export {
|
|
|
12567
13107
|
grammarForDriver,
|
|
12568
13108
|
guestCanViewResource,
|
|
12569
13109
|
hasActiveDatabaseConnection,
|
|
13110
|
+
hasActiveTransaction,
|
|
12570
13111
|
hasMany,
|
|
12571
13112
|
hasManyThrough,
|
|
12572
13113
|
hasMinimumOrgRole2 as hasMinimumOrgRole,
|