@getstrata/core 2.0.2 → 2.0.4
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 +8 -0
- package/dist/core/database/baseRepository.d.ts +2 -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/eventBus.d.ts +4 -0
- package/dist/core/events/outbox/index.d.ts +55 -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/openapi/registeredRoute.d.ts +40 -1
- package/dist/core/security/safeFetch.d.ts +1 -0
- package/dist/entries/audit/exportAuditLogs.js +137 -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 +37 -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 +137 -17
- package/dist/entries/openapi/generator.js +42 -12
- package/dist/entries/openapi/validate.js +10 -0
- package/dist/entries/security/safeFetch.js +100 -17
- package/dist/entries/tenant/databaseTenantContext.js +37 -0
- package/dist/entries/tenant/tenantDatabaseScope.js +37 -0
- package/dist/entries/tracing/tracingMiddleware.js +100 -17
- package/dist/framework/public-api.d.ts +3 -1
- package/dist/index.js +649 -60
- package/package.json +6 -1
|
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
|
|
|
58
58
|
// ../../src/core/events/eventBus.ts
|
|
59
59
|
class EventBus {
|
|
60
60
|
constructor() {}
|
|
61
|
+
transactionalListeners = new Map;
|
|
62
|
+
listenTransactional(event, listener) {
|
|
63
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
64
|
+
handlers.add(listener);
|
|
65
|
+
this.transactionalListeners.set(event, handlers);
|
|
66
|
+
return () => {
|
|
67
|
+
handlers.delete(listener);
|
|
68
|
+
if (!handlers.size)
|
|
69
|
+
this.transactionalListeners.delete(event);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
hasTransactionalListeners(event) {
|
|
73
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
74
|
+
}
|
|
75
|
+
async dispatchTransactional(event, payload) {
|
|
76
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
77
|
+
await listener(payload);
|
|
78
|
+
}
|
|
61
79
|
listeners = new Map;
|
|
62
80
|
on(event, listener) {
|
|
63
81
|
return this.listen(event, listener);
|
|
@@ -164,6 +182,7 @@ async function runWithTransactionScope(callback) {
|
|
|
164
182
|
var TransactionRollback, transactionScopes;
|
|
165
183
|
var init_transactionControl = __esm(() => {
|
|
166
184
|
init_asyncContextStore();
|
|
185
|
+
init_connectionContext();
|
|
167
186
|
TransactionRollback = class TransactionRollback extends Error {
|
|
168
187
|
result;
|
|
169
188
|
constructor(result) {
|
|
@@ -452,6 +471,12 @@ var init_dialect = __esm(() => {
|
|
|
452
471
|
nowExpression() {
|
|
453
472
|
return "NOW()";
|
|
454
473
|
},
|
|
474
|
+
epochMillisecondsExpression() {
|
|
475
|
+
return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
|
|
476
|
+
},
|
|
477
|
+
rowLockClause(skipLocked = false) {
|
|
478
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
479
|
+
},
|
|
455
480
|
timestampValue(value) {
|
|
456
481
|
return value.toISOString();
|
|
457
482
|
},
|
|
@@ -490,6 +515,12 @@ var init_dialect = __esm(() => {
|
|
|
490
515
|
nowExpression() {
|
|
491
516
|
return "CURRENT_TIMESTAMP";
|
|
492
517
|
},
|
|
518
|
+
epochMillisecondsExpression() {
|
|
519
|
+
return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
|
|
520
|
+
},
|
|
521
|
+
rowLockClause(skipLocked = false) {
|
|
522
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
523
|
+
},
|
|
493
524
|
timestampValue(value) {
|
|
494
525
|
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
495
526
|
},
|
|
@@ -528,6 +559,12 @@ var init_dialect = __esm(() => {
|
|
|
528
559
|
nowExpression() {
|
|
529
560
|
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
530
561
|
},
|
|
562
|
+
epochMillisecondsExpression() {
|
|
563
|
+
return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
|
|
564
|
+
},
|
|
565
|
+
rowLockClause() {
|
|
566
|
+
return "";
|
|
567
|
+
},
|
|
531
568
|
timestampValue(value) {
|
|
532
569
|
return value.toISOString();
|
|
533
570
|
},
|
|
@@ -1077,50 +1114,133 @@ async function resolveSafeOutboundTarget(rawUrl, options = {}) {
|
|
|
1077
1114
|
|
|
1078
1115
|
// ../../src/core/security/safeFetch.ts
|
|
1079
1116
|
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
1117
|
+
function untilAborted(work, signal) {
|
|
1118
|
+
signal.throwIfAborted();
|
|
1119
|
+
return new Promise((resolve, reject) => {
|
|
1120
|
+
const onAbort = () => reject(signal.reason);
|
|
1121
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1122
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
function preserveResponseMetadata(response, original) {
|
|
1126
|
+
Object.defineProperties(response, {
|
|
1127
|
+
url: { value: original.url },
|
|
1128
|
+
redirected: { value: original.redirected },
|
|
1129
|
+
type: { value: original.type }
|
|
1130
|
+
});
|
|
1131
|
+
const clone = response.clone.bind(response);
|
|
1132
|
+
response.clone = () => preserveResponseMetadata(clone(), original);
|
|
1133
|
+
return response;
|
|
1134
|
+
}
|
|
1135
|
+
function withBodyDeadline(response, signal, release) {
|
|
1136
|
+
if (!response.body) {
|
|
1137
|
+
release();
|
|
1138
|
+
return response;
|
|
1139
|
+
}
|
|
1140
|
+
const reader = response.body.getReader();
|
|
1141
|
+
let stopped = false;
|
|
1142
|
+
let bodyController;
|
|
1143
|
+
const stop = (reason, errorBody) => {
|
|
1144
|
+
if (stopped)
|
|
1145
|
+
return;
|
|
1146
|
+
stopped = true;
|
|
1147
|
+
release();
|
|
1148
|
+
signal.removeEventListener("abort", onAbort);
|
|
1149
|
+
if (errorBody)
|
|
1150
|
+
bodyController.error(reason);
|
|
1151
|
+
reader.cancel(reason).catch(() => {}).finally(() => reader.releaseLock());
|
|
1152
|
+
};
|
|
1153
|
+
const onAbort = () => stop(signal.reason, true);
|
|
1154
|
+
const body = new ReadableStream({
|
|
1155
|
+
start(controller) {
|
|
1156
|
+
bodyController = controller;
|
|
1157
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1158
|
+
if (signal.aborted)
|
|
1159
|
+
onAbort();
|
|
1160
|
+
},
|
|
1161
|
+
async pull(controller) {
|
|
1162
|
+
try {
|
|
1163
|
+
const result = await untilAborted(reader.read(), signal);
|
|
1164
|
+
if (stopped)
|
|
1165
|
+
return;
|
|
1166
|
+
if (result.done) {
|
|
1167
|
+
stopped = true;
|
|
1168
|
+
release();
|
|
1169
|
+
signal.removeEventListener("abort", onAbort);
|
|
1170
|
+
reader.releaseLock();
|
|
1171
|
+
controller.close();
|
|
1172
|
+
} else {
|
|
1173
|
+
controller.enqueue(result.value);
|
|
1174
|
+
}
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
stop(error, true);
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
cancel(reason) {
|
|
1180
|
+
stop(reason, false);
|
|
1181
|
+
}
|
|
1182
|
+
}, { highWaterMark: 0 });
|
|
1183
|
+
return preserveResponseMetadata(new Response(body, {
|
|
1184
|
+
status: response.status,
|
|
1185
|
+
statusText: response.statusText,
|
|
1186
|
+
headers: response.headers
|
|
1187
|
+
}), response);
|
|
1188
|
+
}
|
|
1080
1189
|
async function safeFetch(input, init = {}, options = {}) {
|
|
1190
|
+
init.signal?.throwIfAborted();
|
|
1081
1191
|
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
1192
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1193
|
+
throw new RangeError("safeFetch timeoutMs must be a positive finite number.");
|
|
1194
|
+
}
|
|
1082
1195
|
const maxRedirects = options.maxRedirects ?? 0;
|
|
1083
|
-
const resolveDns = options.resolveDns ?? true;
|
|
1084
1196
|
const urlOptions = {
|
|
1085
1197
|
allowHttp: options.allowHttp,
|
|
1086
|
-
resolveDns,
|
|
1198
|
+
resolveDns: options.resolveDns ?? true,
|
|
1087
1199
|
allowPrivate: options.allowPrivate
|
|
1088
1200
|
};
|
|
1089
1201
|
const controller = new AbortController;
|
|
1090
|
-
const
|
|
1202
|
+
const forwardAbort = () => controller.abort(init.signal?.reason);
|
|
1203
|
+
init.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
1204
|
+
const timeout = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
|
|
1205
|
+
const release = () => {
|
|
1206
|
+
clearTimeout(timeout);
|
|
1207
|
+
init.signal?.removeEventListener("abort", forwardAbort);
|
|
1208
|
+
};
|
|
1091
1209
|
try {
|
|
1092
|
-
let current = await resolveSafeOutboundTarget(input, urlOptions);
|
|
1210
|
+
let current = await untilAborted(resolveSafeOutboundTarget(input, urlOptions), controller.signal);
|
|
1093
1211
|
let redirectCount = 0;
|
|
1094
1212
|
while (true) {
|
|
1213
|
+
controller.signal.throwIfAborted();
|
|
1095
1214
|
const address = current.addresses[0];
|
|
1096
1215
|
const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
|
|
1097
1216
|
const headers = new Headers(init.headers);
|
|
1098
|
-
if (address && !headers.has("host"))
|
|
1217
|
+
if (address && !headers.has("host"))
|
|
1099
1218
|
headers.set("Host", current.url.host);
|
|
1100
|
-
}
|
|
1101
1219
|
const fetchInit = {
|
|
1102
1220
|
...init,
|
|
1103
1221
|
headers,
|
|
1104
1222
|
signal: controller.signal,
|
|
1105
1223
|
redirect: "manual"
|
|
1106
1224
|
};
|
|
1107
|
-
if (address)
|
|
1225
|
+
if (address)
|
|
1108
1226
|
fetchInit.tls = { serverName: current.url.hostname };
|
|
1109
|
-
|
|
1110
|
-
const response = await fetch(fetchUrl, fetchInit);
|
|
1227
|
+
const response = await untilAborted(fetch(fetchUrl, fetchInit), controller.signal);
|
|
1111
1228
|
if (response.status >= 300 && response.status < 400) {
|
|
1112
1229
|
const location = response.headers.get("location");
|
|
1113
|
-
if (
|
|
1114
|
-
|
|
1230
|
+
if (location && redirectCount < maxRedirects) {
|
|
1231
|
+
if (response.body)
|
|
1232
|
+
await untilAborted(response.body.cancel(), controller.signal);
|
|
1233
|
+
current = await untilAborted(resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions), controller.signal);
|
|
1234
|
+
redirectCount++;
|
|
1235
|
+
continue;
|
|
1115
1236
|
}
|
|
1116
|
-
current = await resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions);
|
|
1117
|
-
redirectCount += 1;
|
|
1118
|
-
continue;
|
|
1119
1237
|
}
|
|
1120
|
-
return response;
|
|
1238
|
+
return withBodyDeadline(response, controller.signal, release);
|
|
1121
1239
|
}
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
controller.abort(error);
|
|
1242
|
+
release();
|
|
1243
|
+
throw error;
|
|
1124
1244
|
}
|
|
1125
1245
|
}
|
|
1126
1246
|
|
|
@@ -168,7 +168,7 @@ var PUBLIC_ROUTE_DESCRIPTIONS = {
|
|
|
168
168
|
"GET /scim/v2/Groups": "SCIM list groups (departments)"
|
|
169
169
|
};
|
|
170
170
|
function toOpenApiPath(path) {
|
|
171
|
-
return path.replace(/:([A-Za-z_]
|
|
171
|
+
return path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}");
|
|
172
172
|
}
|
|
173
173
|
function toRelativeApiPath(path) {
|
|
174
174
|
const prefix = apiPrefix();
|
|
@@ -196,6 +196,16 @@ function requiresBearerAuth(path, method) {
|
|
|
196
196
|
}
|
|
197
197
|
return relative.startsWith("/auth/") || relative.startsWith("/users/me") || path.startsWith("/users/me") || relative === "/user" || relative.startsWith("/integrations/") || relative.startsWith("/audit-logs") || ["POST", "PATCH", "PUT", "DELETE"].includes(method);
|
|
198
198
|
}
|
|
199
|
+
var DEFAULT_RESPONSES = {
|
|
200
|
+
"200": { description: "OK" },
|
|
201
|
+
"201": { description: "Created" },
|
|
202
|
+
"204": { description: "No Content" },
|
|
203
|
+
"400": { description: "Bad Request" },
|
|
204
|
+
"401": { description: "Unauthorized" },
|
|
205
|
+
"403": { description: "Forbidden" },
|
|
206
|
+
"404": { description: "Not Found" },
|
|
207
|
+
"422": { description: "Validation Error" }
|
|
208
|
+
};
|
|
199
209
|
function generateOpenApiSpec(routes) {
|
|
200
210
|
const paths = {};
|
|
201
211
|
for (const route of routes) {
|
|
@@ -206,16 +216,8 @@ function generateOpenApiSpec(routes) {
|
|
|
206
216
|
paths[openApiPath][method] = {
|
|
207
217
|
summary: description,
|
|
208
218
|
...requiresBearerAuth(route.path, route.method) ? { security: [{ bearerAuth: [] }] } : {},
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"201": { description: "Created" },
|
|
212
|
-
"204": { description: "No Content" },
|
|
213
|
-
"400": { description: "Bad Request" },
|
|
214
|
-
"401": { description: "Unauthorized" },
|
|
215
|
-
"403": { description: "Forbidden" },
|
|
216
|
-
"404": { description: "Not Found" },
|
|
217
|
-
"422": { description: "Validation Error" }
|
|
218
|
-
}
|
|
219
|
+
...route.openApi,
|
|
220
|
+
responses: route.openApi?.responses ?? structuredClone(DEFAULT_RESPONSES)
|
|
219
221
|
};
|
|
220
222
|
}
|
|
221
223
|
return {
|
|
@@ -297,10 +299,38 @@ function renderTypeScriptSdk(spec, prefix = apiPrefix()) {
|
|
|
297
299
|
" }",
|
|
298
300
|
""
|
|
299
301
|
];
|
|
302
|
+
const contracts = Object.fromEntries(Object.entries(spec.paths).map(([path, methods]) => [
|
|
303
|
+
path,
|
|
304
|
+
Object.fromEntries(Object.entries(methods).filter(([, value]) => {
|
|
305
|
+
const operation = value;
|
|
306
|
+
return operation.parameters || operation.requestBody || operation.operationId || JSON.stringify(operation.responses) !== JSON.stringify(DEFAULT_RESPONSES);
|
|
307
|
+
}))
|
|
308
|
+
]).filter(([, methods]) => Object.keys(methods).length));
|
|
309
|
+
if (Object.keys(contracts).length)
|
|
310
|
+
lines.splice(1, 0, ` static readonly operationContracts = ${JSON.stringify(contracts)} as const;`);
|
|
300
311
|
for (const [path, methods] of Object.entries(spec.paths)) {
|
|
301
312
|
const requestPath = toRequestPath(path, prefix);
|
|
302
|
-
for (const method of Object.
|
|
313
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
314
|
+
const metadata = operation;
|
|
315
|
+
const headers = metadata.parameters?.filter((parameter) => parameter.in === "header") ?? [];
|
|
303
316
|
const functionName = toMethodName(method, path, prefix);
|
|
317
|
+
const pathParameters = metadata.parameters?.filter((parameter) => parameter.in === "path") ?? [];
|
|
318
|
+
const queryParameters = metadata.parameters?.filter((parameter) => parameter.in === "query") ?? [];
|
|
319
|
+
const groups = [
|
|
320
|
+
{ field: "operationHeaders", parameters: headers },
|
|
321
|
+
{ field: "operationPath", parameters: pathParameters },
|
|
322
|
+
{ field: "operationQuery", parameters: queryParameters }
|
|
323
|
+
].filter((group) => group.parameters.length);
|
|
324
|
+
if (groups.length) {
|
|
325
|
+
const type = groups.map((group) => `${group.field}${group.parameters.some((parameter) => parameter.required) ? "" : "?"}: { ${group.parameters.map((parameter) => `${JSON.stringify(parameter.name)}${parameter.required ? "" : "?"}: ${parameter.schema?.type === "integer" || parameter.schema?.type === "number" ? "number" : parameter.schema?.type === "boolean" ? "boolean" : "string"}`).join("; ")} }`).join("; ");
|
|
326
|
+
const required = groups.some((group) => group.parameters.some((parameter) => parameter.required));
|
|
327
|
+
lines.push(` async ${functionName}(init: RequestInit & { ${type} }${required ? "" : " = {}"}): Promise<Response> {`, " const headers = new Headers(init.headers);", ` let path = ${JSON.stringify(requestPath)};`, ...groups.flatMap((group) => group.parameters.filter((parameter) => parameter.required).map((parameter) => ` if (init.${group.field}?.[${JSON.stringify(parameter.name)}] === undefined) throw new TypeError(${JSON.stringify(`Required operation ${group.field === "operationHeaders" ? "header" : group.field === "operationPath" ? "path parameter" : "query parameter"}: ${parameter.name}`)});`)), ...headers.map((parameter) => ` if (init.operationHeaders?.[${JSON.stringify(parameter.name)}] !== undefined) headers.set(${JSON.stringify(parameter.name)}, String(init.operationHeaders[${JSON.stringify(parameter.name)}]));`), ...pathParameters.map((parameter) => ` if (init.operationPath?.[${JSON.stringify(parameter.name)}] !== undefined) path = path.replaceAll(${JSON.stringify(`{${parameter.name}}`)}, encodeURIComponent(String(init.operationPath[${JSON.stringify(parameter.name)}])));`), ...queryParameters.length ? [
|
|
328
|
+
" const query = new URLSearchParams();",
|
|
329
|
+
...queryParameters.map((parameter) => ` if (init.operationQuery?.[${JSON.stringify(parameter.name)}] !== undefined) query.set(${JSON.stringify(parameter.name)}, String(init.operationQuery[${JSON.stringify(parameter.name)}]));`),
|
|
330
|
+
" if (query.size) path += '?' + query.toString();"
|
|
331
|
+
] : [], ` const { ${groups.map((group) => `${group.field}: _${group.field}`).join(", ")}, ...requestInit } = init;`, ` return await this.request(path, { ...requestInit, headers, method: ${JSON.stringify(method.toUpperCase())} });`, " }", "");
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
304
334
|
lines.push(` async ${functionName}(init: RequestInit = {}): Promise<Response> {`, ` return await this.request("${requestPath}", { ...init, method: "${method.toUpperCase()}" });`, " }", "");
|
|
305
335
|
}
|
|
306
336
|
}
|
|
@@ -13,6 +13,16 @@ function validateOpenApiSpec(spec) {
|
|
|
13
13
|
errors.push(`Path must start with '/': ${path}`);
|
|
14
14
|
}
|
|
15
15
|
for (const [method, operation] of Object.entries(methods)) {
|
|
16
|
+
const details = operation;
|
|
17
|
+
const seen = new Set;
|
|
18
|
+
for (const parameter of details.parameters ?? []) {
|
|
19
|
+
const identity = `${parameter.in}:${parameter.in === "header" ? parameter.name.toLowerCase() : parameter.name}`;
|
|
20
|
+
if (seen.has(identity))
|
|
21
|
+
errors.push(`Duplicate parameter ${parameter.name} on ${method.toUpperCase()} ${path}.`);
|
|
22
|
+
seen.add(identity);
|
|
23
|
+
if (parameter.in === "path" && (!parameter.required || !path.includes(`{${parameter.name}}`)))
|
|
24
|
+
errors.push(`Path parameter ${parameter.name} must be required and match ${path}.`);
|
|
25
|
+
}
|
|
16
26
|
if (!("responses" in operation)) {
|
|
17
27
|
errors.push(`Operation ${method.toUpperCase()} ${path} is missing responses.`);
|
|
18
28
|
}
|
|
@@ -261,50 +261,133 @@ async function resolveSafeOutboundTarget(rawUrl, options = {}) {
|
|
|
261
261
|
|
|
262
262
|
// ../../src/core/security/safeFetch.ts
|
|
263
263
|
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
264
|
+
function untilAborted(work, signal) {
|
|
265
|
+
signal.throwIfAborted();
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
const onAbort = () => reject(signal.reason);
|
|
268
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
269
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function preserveResponseMetadata(response, original) {
|
|
273
|
+
Object.defineProperties(response, {
|
|
274
|
+
url: { value: original.url },
|
|
275
|
+
redirected: { value: original.redirected },
|
|
276
|
+
type: { value: original.type }
|
|
277
|
+
});
|
|
278
|
+
const clone = response.clone.bind(response);
|
|
279
|
+
response.clone = () => preserveResponseMetadata(clone(), original);
|
|
280
|
+
return response;
|
|
281
|
+
}
|
|
282
|
+
function withBodyDeadline(response, signal, release) {
|
|
283
|
+
if (!response.body) {
|
|
284
|
+
release();
|
|
285
|
+
return response;
|
|
286
|
+
}
|
|
287
|
+
const reader = response.body.getReader();
|
|
288
|
+
let stopped = false;
|
|
289
|
+
let bodyController;
|
|
290
|
+
const stop = (reason, errorBody) => {
|
|
291
|
+
if (stopped)
|
|
292
|
+
return;
|
|
293
|
+
stopped = true;
|
|
294
|
+
release();
|
|
295
|
+
signal.removeEventListener("abort", onAbort);
|
|
296
|
+
if (errorBody)
|
|
297
|
+
bodyController.error(reason);
|
|
298
|
+
reader.cancel(reason).catch(() => {}).finally(() => reader.releaseLock());
|
|
299
|
+
};
|
|
300
|
+
const onAbort = () => stop(signal.reason, true);
|
|
301
|
+
const body = new ReadableStream({
|
|
302
|
+
start(controller) {
|
|
303
|
+
bodyController = controller;
|
|
304
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
305
|
+
if (signal.aborted)
|
|
306
|
+
onAbort();
|
|
307
|
+
},
|
|
308
|
+
async pull(controller) {
|
|
309
|
+
try {
|
|
310
|
+
const result = await untilAborted(reader.read(), signal);
|
|
311
|
+
if (stopped)
|
|
312
|
+
return;
|
|
313
|
+
if (result.done) {
|
|
314
|
+
stopped = true;
|
|
315
|
+
release();
|
|
316
|
+
signal.removeEventListener("abort", onAbort);
|
|
317
|
+
reader.releaseLock();
|
|
318
|
+
controller.close();
|
|
319
|
+
} else {
|
|
320
|
+
controller.enqueue(result.value);
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
stop(error, true);
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
cancel(reason) {
|
|
327
|
+
stop(reason, false);
|
|
328
|
+
}
|
|
329
|
+
}, { highWaterMark: 0 });
|
|
330
|
+
return preserveResponseMetadata(new Response(body, {
|
|
331
|
+
status: response.status,
|
|
332
|
+
statusText: response.statusText,
|
|
333
|
+
headers: response.headers
|
|
334
|
+
}), response);
|
|
335
|
+
}
|
|
264
336
|
async function safeFetch(input, init = {}, options = {}) {
|
|
337
|
+
init.signal?.throwIfAborted();
|
|
265
338
|
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
339
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
340
|
+
throw new RangeError("safeFetch timeoutMs must be a positive finite number.");
|
|
341
|
+
}
|
|
266
342
|
const maxRedirects = options.maxRedirects ?? 0;
|
|
267
|
-
const resolveDns = options.resolveDns ?? true;
|
|
268
343
|
const urlOptions = {
|
|
269
344
|
allowHttp: options.allowHttp,
|
|
270
|
-
resolveDns,
|
|
345
|
+
resolveDns: options.resolveDns ?? true,
|
|
271
346
|
allowPrivate: options.allowPrivate
|
|
272
347
|
};
|
|
273
348
|
const controller = new AbortController;
|
|
274
|
-
const
|
|
349
|
+
const forwardAbort = () => controller.abort(init.signal?.reason);
|
|
350
|
+
init.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
351
|
+
const timeout = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
|
|
352
|
+
const release = () => {
|
|
353
|
+
clearTimeout(timeout);
|
|
354
|
+
init.signal?.removeEventListener("abort", forwardAbort);
|
|
355
|
+
};
|
|
275
356
|
try {
|
|
276
|
-
let current = await resolveSafeOutboundTarget(input, urlOptions);
|
|
357
|
+
let current = await untilAborted(resolveSafeOutboundTarget(input, urlOptions), controller.signal);
|
|
277
358
|
let redirectCount = 0;
|
|
278
359
|
while (true) {
|
|
360
|
+
controller.signal.throwIfAborted();
|
|
279
361
|
const address = current.addresses[0];
|
|
280
362
|
const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
|
|
281
363
|
const headers = new Headers(init.headers);
|
|
282
|
-
if (address && !headers.has("host"))
|
|
364
|
+
if (address && !headers.has("host"))
|
|
283
365
|
headers.set("Host", current.url.host);
|
|
284
|
-
}
|
|
285
366
|
const fetchInit = {
|
|
286
367
|
...init,
|
|
287
368
|
headers,
|
|
288
369
|
signal: controller.signal,
|
|
289
370
|
redirect: "manual"
|
|
290
371
|
};
|
|
291
|
-
if (address)
|
|
372
|
+
if (address)
|
|
292
373
|
fetchInit.tls = { serverName: current.url.hostname };
|
|
293
|
-
|
|
294
|
-
const response = await fetch(fetchUrl, fetchInit);
|
|
374
|
+
const response = await untilAborted(fetch(fetchUrl, fetchInit), controller.signal);
|
|
295
375
|
if (response.status >= 300 && response.status < 400) {
|
|
296
376
|
const location = response.headers.get("location");
|
|
297
|
-
if (
|
|
298
|
-
|
|
377
|
+
if (location && redirectCount < maxRedirects) {
|
|
378
|
+
if (response.body)
|
|
379
|
+
await untilAborted(response.body.cancel(), controller.signal);
|
|
380
|
+
current = await untilAborted(resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions), controller.signal);
|
|
381
|
+
redirectCount++;
|
|
382
|
+
continue;
|
|
299
383
|
}
|
|
300
|
-
current = await resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions);
|
|
301
|
-
redirectCount += 1;
|
|
302
|
-
continue;
|
|
303
384
|
}
|
|
304
|
-
return response;
|
|
385
|
+
return withBodyDeadline(response, controller.signal, release);
|
|
305
386
|
}
|
|
306
|
-
}
|
|
307
|
-
|
|
387
|
+
} catch (error) {
|
|
388
|
+
controller.abort(error);
|
|
389
|
+
release();
|
|
390
|
+
throw error;
|
|
308
391
|
}
|
|
309
392
|
}
|
|
310
393
|
export {
|
|
@@ -58,6 +58,24 @@ var init_tenantContext = __esm(() => {
|
|
|
58
58
|
// ../../src/core/events/eventBus.ts
|
|
59
59
|
class EventBus {
|
|
60
60
|
constructor() {}
|
|
61
|
+
transactionalListeners = new Map;
|
|
62
|
+
listenTransactional(event, listener) {
|
|
63
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
64
|
+
handlers.add(listener);
|
|
65
|
+
this.transactionalListeners.set(event, handlers);
|
|
66
|
+
return () => {
|
|
67
|
+
handlers.delete(listener);
|
|
68
|
+
if (!handlers.size)
|
|
69
|
+
this.transactionalListeners.delete(event);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
hasTransactionalListeners(event) {
|
|
73
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
74
|
+
}
|
|
75
|
+
async dispatchTransactional(event, payload) {
|
|
76
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
77
|
+
await listener(payload);
|
|
78
|
+
}
|
|
61
79
|
listeners = new Map;
|
|
62
80
|
on(event, listener) {
|
|
63
81
|
return this.listen(event, listener);
|
|
@@ -164,6 +182,7 @@ async function runWithTransactionScope(callback) {
|
|
|
164
182
|
var TransactionRollback, transactionScopes;
|
|
165
183
|
var init_transactionControl = __esm(() => {
|
|
166
184
|
init_asyncContextStore();
|
|
185
|
+
init_connectionContext();
|
|
167
186
|
TransactionRollback = class TransactionRollback extends Error {
|
|
168
187
|
result;
|
|
169
188
|
constructor(result) {
|
|
@@ -452,6 +471,12 @@ var init_dialect = __esm(() => {
|
|
|
452
471
|
nowExpression() {
|
|
453
472
|
return "NOW()";
|
|
454
473
|
},
|
|
474
|
+
epochMillisecondsExpression() {
|
|
475
|
+
return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
|
|
476
|
+
},
|
|
477
|
+
rowLockClause(skipLocked = false) {
|
|
478
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
479
|
+
},
|
|
455
480
|
timestampValue(value) {
|
|
456
481
|
return value.toISOString();
|
|
457
482
|
},
|
|
@@ -490,6 +515,12 @@ var init_dialect = __esm(() => {
|
|
|
490
515
|
nowExpression() {
|
|
491
516
|
return "CURRENT_TIMESTAMP";
|
|
492
517
|
},
|
|
518
|
+
epochMillisecondsExpression() {
|
|
519
|
+
return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
|
|
520
|
+
},
|
|
521
|
+
rowLockClause(skipLocked = false) {
|
|
522
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
523
|
+
},
|
|
493
524
|
timestampValue(value) {
|
|
494
525
|
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
495
526
|
},
|
|
@@ -528,6 +559,12 @@ var init_dialect = __esm(() => {
|
|
|
528
559
|
nowExpression() {
|
|
529
560
|
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
530
561
|
},
|
|
562
|
+
epochMillisecondsExpression() {
|
|
563
|
+
return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
|
|
564
|
+
},
|
|
565
|
+
rowLockClause() {
|
|
566
|
+
return "";
|
|
567
|
+
},
|
|
531
568
|
timestampValue(value) {
|
|
532
569
|
return value.toISOString();
|
|
533
570
|
},
|
|
@@ -105,6 +105,7 @@ async function runWithTransactionScope(callback) {
|
|
|
105
105
|
var TransactionRollback, transactionScopes;
|
|
106
106
|
var init_transactionControl = __esm(() => {
|
|
107
107
|
init_asyncContextStore();
|
|
108
|
+
init_connectionContext();
|
|
108
109
|
TransactionRollback = class TransactionRollback extends Error {
|
|
109
110
|
result;
|
|
110
111
|
constructor(result) {
|
|
@@ -132,6 +133,24 @@ var init_tenantContext = __esm(() => {
|
|
|
132
133
|
// ../../src/core/events/eventBus.ts
|
|
133
134
|
class EventBus {
|
|
134
135
|
constructor() {}
|
|
136
|
+
transactionalListeners = new Map;
|
|
137
|
+
listenTransactional(event, listener) {
|
|
138
|
+
const handlers = this.transactionalListeners.get(event) ?? new Set;
|
|
139
|
+
handlers.add(listener);
|
|
140
|
+
this.transactionalListeners.set(event, handlers);
|
|
141
|
+
return () => {
|
|
142
|
+
handlers.delete(listener);
|
|
143
|
+
if (!handlers.size)
|
|
144
|
+
this.transactionalListeners.delete(event);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
hasTransactionalListeners(event) {
|
|
148
|
+
return (this.transactionalListeners.get(event)?.size ?? 0) > 0;
|
|
149
|
+
}
|
|
150
|
+
async dispatchTransactional(event, payload) {
|
|
151
|
+
for (const listener of this.transactionalListeners.get(event) ?? [])
|
|
152
|
+
await listener(payload);
|
|
153
|
+
}
|
|
135
154
|
listeners = new Map;
|
|
136
155
|
on(event, listener) {
|
|
137
156
|
return this.listen(event, listener);
|
|
@@ -386,6 +405,12 @@ var init_dialect = __esm(() => {
|
|
|
386
405
|
nowExpression() {
|
|
387
406
|
return "NOW()";
|
|
388
407
|
},
|
|
408
|
+
epochMillisecondsExpression() {
|
|
409
|
+
return "CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)";
|
|
410
|
+
},
|
|
411
|
+
rowLockClause(skipLocked = false) {
|
|
412
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
413
|
+
},
|
|
389
414
|
timestampValue(value) {
|
|
390
415
|
return value.toISOString();
|
|
391
416
|
},
|
|
@@ -424,6 +449,12 @@ var init_dialect = __esm(() => {
|
|
|
424
449
|
nowExpression() {
|
|
425
450
|
return "CURRENT_TIMESTAMP";
|
|
426
451
|
},
|
|
452
|
+
epochMillisecondsExpression() {
|
|
453
|
+
return "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED)";
|
|
454
|
+
},
|
|
455
|
+
rowLockClause(skipLocked = false) {
|
|
456
|
+
return ` FOR UPDATE${skipLocked ? " SKIP LOCKED" : ""}`;
|
|
457
|
+
},
|
|
427
458
|
timestampValue(value) {
|
|
428
459
|
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
429
460
|
},
|
|
@@ -462,6 +493,12 @@ var init_dialect = __esm(() => {
|
|
|
462
493
|
nowExpression() {
|
|
463
494
|
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
464
495
|
},
|
|
496
|
+
epochMillisecondsExpression() {
|
|
497
|
+
return "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
|
|
498
|
+
},
|
|
499
|
+
rowLockClause() {
|
|
500
|
+
return "";
|
|
501
|
+
},
|
|
465
502
|
timestampValue(value) {
|
|
466
503
|
return value.toISOString();
|
|
467
504
|
},
|