@getstrata/core 1.1.8 → 2.0.0
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 +23 -0
- package/dist/core/database/transaction.d.ts +2 -1
- package/dist/core/database/transactionControl.d.ts +5 -0
- package/dist/core/events/deferredModelEvents.d.ts +3 -0
- package/dist/core/events/index.d.ts +1 -0
- package/dist/core/http/loginThrottleMiddleware.d.ts +3 -0
- package/dist/core/http/metricsMiddleware.d.ts +1 -2
- package/dist/core/http/requestMetaContext.d.ts +2 -0
- package/dist/core/http/throttleMiddleware.d.ts +11 -2
- package/dist/core/metrics/prometheus.d.ts +11 -1
- package/dist/core/queue/jobRunner.d.ts +3 -1
- package/dist/core/queue/redisQueue.d.ts +20 -3
- package/dist/core/tenant/databaseTenantContext.d.ts +5 -1
- package/dist/entries/audit/exportAuditLogs.js +751 -97
- package/dist/entries/auth/accessControl.js +16 -2
- package/dist/entries/auth/basicAuthGuard.js +271 -224
- package/dist/entries/auth/membershipMiddleware.js +17 -0
- package/dist/entries/auth/membershipScope.js +21 -6
- package/dist/entries/auth/membershipService.js +20 -5
- package/dist/entries/auth/oauth/samlProvider.js +187 -142
- package/dist/entries/auth/oneTimeToken.js +142 -123
- package/dist/entries/auth/passwordLogin.js +212 -191
- package/dist/entries/auth/policy.js +16 -2
- package/dist/entries/auth/saml/samlServiceProvider.js +187 -142
- package/dist/entries/auth/scimAuthMiddleware.js +732 -64
- package/dist/entries/database/migrations.js +144 -125
- package/dist/entries/database/schema.js +58 -39
- package/dist/entries/http/csrfMiddleware.js +28 -14
- package/dist/entries/http/csrfToken.js +28 -14
- package/dist/entries/http/flashMiddleware.js +28 -14
- package/dist/entries/http/metricsMiddleware.js +124 -27
- package/dist/entries/http/response.js +107 -0
- package/dist/entries/http/routeMiddleware.js +40 -0
- package/dist/entries/http/throttleMiddleware.js +124 -11
- package/dist/entries/jobs/exportAuditLogsJob.js +751 -97
- package/dist/entries/metrics/prometheus.js +71 -11
- package/dist/entries/queue/createAppQueue.js +215 -21
- package/dist/entries/queue/jobRunner.js +3 -2
- package/dist/entries/queue/publicQueue.js +215 -21
- package/dist/entries/queue/queueMetrics.js +215 -21
- package/dist/entries/queue/redisQueue.js +223 -22
- package/dist/entries/runtime/asyncContextStore.js +16 -0
- package/dist/entries/tenant/databaseTenantContext.js +666 -11
- package/dist/entries/tenant/tenancyConfig.js +19 -1
- package/dist/entries/tenant/tenantDatabaseScope.js +688 -18
- package/dist/entries/tracing/tracingMiddleware.js +28 -14
- package/dist/framework/public-api.d.ts +3 -3
- package/dist/index.js +1344 -651
- package/package.json +1 -1
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
var __esm = (fn, res, err) => () => {
|
|
3
|
+
if (fn)
|
|
4
|
+
try {
|
|
5
|
+
res = fn(fn = 0);
|
|
6
|
+
} catch (e) {
|
|
7
|
+
err = [e];
|
|
8
|
+
}
|
|
9
|
+
if (err)
|
|
10
|
+
throw err[0];
|
|
11
|
+
return res;
|
|
12
|
+
};
|
|
4
13
|
|
|
5
14
|
// ../../src/core/runtime/asyncContextStore.ts
|
|
6
15
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -15,9 +24,9 @@ function createAsyncContextStore(key) {
|
|
|
15
24
|
globalRecord[symbol] = store;
|
|
16
25
|
return store;
|
|
17
26
|
}
|
|
27
|
+
var init_asyncContextStore = () => {};
|
|
18
28
|
|
|
19
29
|
// ../../src/core/database/connectionContext.ts
|
|
20
|
-
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
21
30
|
function runWithDatabaseConnection(connection, callback) {
|
|
22
31
|
return activeConnection.run(connection, callback);
|
|
23
32
|
}
|
|
@@ -27,9 +36,579 @@ function getActiveDatabaseConnection(fallback) {
|
|
|
27
36
|
function hasActiveDatabaseConnection() {
|
|
28
37
|
return activeConnection.getStore() !== undefined;
|
|
29
38
|
}
|
|
39
|
+
var activeConnection;
|
|
40
|
+
var init_connectionContext = __esm(() => {
|
|
41
|
+
init_asyncContextStore();
|
|
42
|
+
activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ../../src/core/database/transactionControl.ts
|
|
46
|
+
function unwrapTransactionRollback(error, depth = 0) {
|
|
47
|
+
if (error instanceof TransactionRollback) {
|
|
48
|
+
return error;
|
|
49
|
+
}
|
|
50
|
+
if (depth >= 5 || !(error instanceof Error) || error.cause === undefined) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return unwrapTransactionRollback(error.cause, depth + 1);
|
|
54
|
+
}
|
|
55
|
+
function requestTransactionRollback() {
|
|
56
|
+
const state = transactionScopes.getStore();
|
|
57
|
+
const index = state ? state.rollback.length - 1 : -1;
|
|
58
|
+
if (!state || index < 0) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const flags = state.rollback;
|
|
62
|
+
const current = flags[index];
|
|
63
|
+
if (current === undefined) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
flags[index] = true;
|
|
67
|
+
}
|
|
68
|
+
function scopeRequestsRollback() {
|
|
69
|
+
const state = transactionScopes.getStore();
|
|
70
|
+
if (!state || state.rollback.length === 0) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return state.rollback[state.rollback.length - 1] === true;
|
|
74
|
+
}
|
|
75
|
+
async function commitOrRollbackScope(result) {
|
|
76
|
+
if (scopeRequestsRollback()) {
|
|
77
|
+
throw new TransactionRollback(result);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
async function settleTransaction(work) {
|
|
82
|
+
try {
|
|
83
|
+
return await work();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const rollback = unwrapTransactionRollback(error);
|
|
86
|
+
if (rollback) {
|
|
87
|
+
return rollback.result;
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function runWithTransactionScope(callback) {
|
|
93
|
+
const existing = transactionScopes.getStore();
|
|
94
|
+
if (existing) {
|
|
95
|
+
existing.rollback.push(false);
|
|
96
|
+
try {
|
|
97
|
+
return await callback();
|
|
98
|
+
} finally {
|
|
99
|
+
existing.rollback.pop();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const state = { rollback: [false] };
|
|
103
|
+
return await transactionScopes.run(state, callback);
|
|
104
|
+
}
|
|
105
|
+
var TransactionRollback, transactionScopes;
|
|
106
|
+
var init_transactionControl = __esm(() => {
|
|
107
|
+
init_asyncContextStore();
|
|
108
|
+
TransactionRollback = class TransactionRollback extends Error {
|
|
109
|
+
result;
|
|
110
|
+
constructor(result) {
|
|
111
|
+
super("Transaction rolled back");
|
|
112
|
+
this.name = "TransactionRollback";
|
|
113
|
+
this.result = result;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
transactionScopes = createAsyncContextStore("@getstrata/transactionScopes");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
120
|
+
function runWithTenant(tenant, callback) {
|
|
121
|
+
return tenantContext.run(tenant, callback);
|
|
122
|
+
}
|
|
123
|
+
function currentTenant() {
|
|
124
|
+
return tenantContext.getStore() ?? null;
|
|
125
|
+
}
|
|
126
|
+
var tenantContext;
|
|
127
|
+
var init_tenantContext = __esm(() => {
|
|
128
|
+
init_asyncContextStore();
|
|
129
|
+
tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ../../src/core/events/eventBus.ts
|
|
133
|
+
class EventBus {
|
|
134
|
+
constructor() {}
|
|
135
|
+
listeners = new Map;
|
|
136
|
+
on(event, listener) {
|
|
137
|
+
return this.listen(event, listener);
|
|
138
|
+
}
|
|
139
|
+
listen(event, listener) {
|
|
140
|
+
const handlers = this.listeners.get(event) ?? new Set;
|
|
141
|
+
handlers.add(listener);
|
|
142
|
+
this.listeners.set(event, handlers);
|
|
143
|
+
return () => {
|
|
144
|
+
handlers.delete(listener);
|
|
145
|
+
if (handlers.size === 0) {
|
|
146
|
+
this.listeners.delete(event);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async emit(event, payload) {
|
|
151
|
+
await this.dispatch(event, payload);
|
|
152
|
+
}
|
|
153
|
+
async dispatch(event, payload) {
|
|
154
|
+
const handlers = this.listeners.get(event);
|
|
155
|
+
if (!handlers || handlers.size === 0) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
for (const handler of handlers) {
|
|
159
|
+
await handler(payload);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function readSharedEventBus() {
|
|
164
|
+
const globalBus = globalThis[EVENT_BUS_KEY];
|
|
165
|
+
if (globalBus) {
|
|
166
|
+
return globalBus;
|
|
167
|
+
}
|
|
168
|
+
const bus = new EventBus;
|
|
169
|
+
globalThis[EVENT_BUS_KEY] = bus;
|
|
170
|
+
return bus;
|
|
171
|
+
}
|
|
172
|
+
var EVENT_BUS_KEY, eventBus;
|
|
173
|
+
var init_eventBus = __esm(() => {
|
|
174
|
+
EVENT_BUS_KEY = Symbol.for("@getstrata/eventBus");
|
|
175
|
+
eventBus = readSharedEventBus();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ../../src/core/events/deferredModelEvents.ts
|
|
179
|
+
function sameTenant(left, right) {
|
|
180
|
+
if (left === null && right === null) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
if (left === null || right === null) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return left.id === right.id;
|
|
187
|
+
}
|
|
188
|
+
function groupQueuedModelEvents(queued) {
|
|
189
|
+
const groups = [];
|
|
190
|
+
for (const item of queued) {
|
|
191
|
+
const lastGroup = groups[groups.length - 1];
|
|
192
|
+
const last = lastGroup?.[lastGroup.length - 1];
|
|
193
|
+
if (!lastGroup || !last || !sameTenant(last.tenant, item.tenant)) {
|
|
194
|
+
groups.push([item]);
|
|
195
|
+
} else {
|
|
196
|
+
lastGroup.push(item);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return groups;
|
|
200
|
+
}
|
|
201
|
+
async function dispatchEventBatch(batch) {
|
|
202
|
+
for (const item of batch) {
|
|
203
|
+
await eventBus.dispatch(item.event, item.payload);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function dispatchQueuedModelEvents(queued) {
|
|
207
|
+
if (queued.length === 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
await Promise.resolve().then(() => init_tenantDatabaseScope());
|
|
211
|
+
for (const batch of groupQueuedModelEvents(queued)) {
|
|
212
|
+
const tenant = batch[0]?.tenant ?? null;
|
|
213
|
+
if (tenant) {
|
|
214
|
+
await runWithTenantDatabase(tenant, () => dispatchEventBatch(batch));
|
|
215
|
+
} else {
|
|
216
|
+
await dispatchEventBatch(batch);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function runWithDeferredModelEvents(callback) {
|
|
221
|
+
const existing = deferredModelEvents.getStore();
|
|
222
|
+
if (existing) {
|
|
223
|
+
existing.frames.push([]);
|
|
224
|
+
try {
|
|
225
|
+
const result = await callback();
|
|
226
|
+
const nested = existing.frames.pop() ?? [];
|
|
227
|
+
const parent = existing.frames[existing.frames.length - 1];
|
|
228
|
+
if (parent) {
|
|
229
|
+
parent.push(...nested);
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
existing.frames.pop();
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const state = { frames: [[]] };
|
|
238
|
+
const result = await deferredModelEvents.run(state, callback);
|
|
239
|
+
await dispatchQueuedModelEvents(state.frames[0] ?? []);
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
var deferredModelEvents;
|
|
243
|
+
var init_deferredModelEvents = __esm(() => {
|
|
244
|
+
init_asyncContextStore();
|
|
245
|
+
init_tenantContext();
|
|
246
|
+
init_eventBus();
|
|
247
|
+
deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ../../src/core/database/connection.ts
|
|
251
|
+
function createDatabaseConnection(source) {
|
|
252
|
+
return {
|
|
253
|
+
async unsafe(query, params = []) {
|
|
254
|
+
return await source.unsafe(query, params);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
var init_connection = () => {};
|
|
259
|
+
|
|
260
|
+
// ../../src/core/database/boundConnection.ts
|
|
261
|
+
function boundConnectionState() {
|
|
262
|
+
const globalRecord = globalThis;
|
|
263
|
+
const existing = globalRecord[BOUND_CONNECTION_KEY];
|
|
264
|
+
if (existing) {
|
|
265
|
+
return existing;
|
|
266
|
+
}
|
|
267
|
+
const created = { connection: null };
|
|
268
|
+
globalRecord[BOUND_CONNECTION_KEY] = created;
|
|
269
|
+
return created;
|
|
270
|
+
}
|
|
271
|
+
function getBoundDatabaseConnection() {
|
|
272
|
+
return boundConnectionState().connection;
|
|
273
|
+
}
|
|
274
|
+
var BOUND_CONNECTION_KEY;
|
|
275
|
+
var init_boundConnection = __esm(() => {
|
|
276
|
+
BOUND_CONNECTION_KEY = Symbol.for("@getstrata/boundDatabaseConnection");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ../../src/core/database/queryProxy.ts
|
|
280
|
+
var POOL_CONNECTION_METHODS;
|
|
281
|
+
var init_queryProxy = __esm(() => {
|
|
282
|
+
init_connectionContext();
|
|
283
|
+
POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ../../src/core/database/defaultConnection.ts
|
|
287
|
+
function defaultConnectionState() {
|
|
288
|
+
const globalRecord = globalThis;
|
|
289
|
+
const existing = globalRecord[DEFAULT_CONNECTION_KEY];
|
|
290
|
+
if (existing) {
|
|
291
|
+
return existing;
|
|
292
|
+
}
|
|
293
|
+
const created = { pool: null, query: null };
|
|
294
|
+
globalRecord[DEFAULT_CONNECTION_KEY] = created;
|
|
295
|
+
return created;
|
|
296
|
+
}
|
|
297
|
+
function getDefaultDatabasePool() {
|
|
298
|
+
const pool = defaultConnectionState().pool;
|
|
299
|
+
if (!pool) {
|
|
300
|
+
throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
301
|
+
}
|
|
302
|
+
return pool;
|
|
303
|
+
}
|
|
304
|
+
function getDefaultDatabaseQuery() {
|
|
305
|
+
const query = defaultConnectionState().query;
|
|
306
|
+
if (!query) {
|
|
307
|
+
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
308
|
+
}
|
|
309
|
+
return query;
|
|
310
|
+
}
|
|
311
|
+
var DEFAULT_CONNECTION_KEY;
|
|
312
|
+
var init_defaultConnection = __esm(() => {
|
|
313
|
+
init_queryProxy();
|
|
314
|
+
DEFAULT_CONNECTION_KEY = Symbol.for("@getstrata/defaultDatabaseConnection");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ../../src/core/database/schema/driver.ts
|
|
318
|
+
function normalizeConnectionName(connection) {
|
|
319
|
+
const normalized = connection.trim().toLowerCase();
|
|
320
|
+
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
321
|
+
return "pgsql";
|
|
322
|
+
}
|
|
323
|
+
if (normalized === "mysql" || normalized === "mariadb") {
|
|
324
|
+
return "mysql";
|
|
325
|
+
}
|
|
326
|
+
if (normalized === "sqlite") {
|
|
327
|
+
return "sqlite";
|
|
328
|
+
}
|
|
329
|
+
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
330
|
+
}
|
|
331
|
+
function resolveDriverFromUrl(url) {
|
|
332
|
+
const normalized = url.trim().toLowerCase();
|
|
333
|
+
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
334
|
+
return "pgsql";
|
|
335
|
+
}
|
|
336
|
+
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
337
|
+
return "mysql";
|
|
338
|
+
}
|
|
339
|
+
if (normalized.startsWith("sqlite:")) {
|
|
340
|
+
return "sqlite";
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
function resolveDatabaseDriver(options = {}) {
|
|
345
|
+
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
346
|
+
if (connection) {
|
|
347
|
+
return normalizeConnectionName(connection);
|
|
348
|
+
}
|
|
349
|
+
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
350
|
+
const fromUrl = resolveDriverFromUrl(url);
|
|
351
|
+
if (fromUrl) {
|
|
352
|
+
return fromUrl;
|
|
353
|
+
}
|
|
354
|
+
return "pgsql";
|
|
355
|
+
}
|
|
356
|
+
var init_driver = () => {};
|
|
357
|
+
|
|
358
|
+
// ../../src/core/database/dialect.ts
|
|
359
|
+
function assertSafeIdentifier(identifier) {
|
|
360
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
361
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
362
|
+
}
|
|
363
|
+
return identifier;
|
|
364
|
+
}
|
|
365
|
+
function quoteIdentifierFor(dialect, column) {
|
|
366
|
+
return dialect.quoteIdentifier(column);
|
|
367
|
+
}
|
|
368
|
+
function dialectFor(driver) {
|
|
369
|
+
return dialects[driver];
|
|
370
|
+
}
|
|
371
|
+
function currentSqlDialect() {
|
|
372
|
+
return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
|
|
373
|
+
}
|
|
374
|
+
var postgresDialect, mysqlDialect, sqliteDialect, dialects, dialectContext, dialectOverride = null;
|
|
375
|
+
var init_dialect = __esm(() => {
|
|
376
|
+
init_asyncContextStore();
|
|
377
|
+
init_driver();
|
|
378
|
+
postgresDialect = {
|
|
379
|
+
driver: "pgsql",
|
|
380
|
+
placeholder(index) {
|
|
381
|
+
return `$${index}`;
|
|
382
|
+
},
|
|
383
|
+
quoteIdentifier(identifier) {
|
|
384
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
385
|
+
},
|
|
386
|
+
nowExpression() {
|
|
387
|
+
return "NOW()";
|
|
388
|
+
},
|
|
389
|
+
timestampValue(value) {
|
|
390
|
+
return value.toISOString();
|
|
391
|
+
},
|
|
392
|
+
returningClause(columns) {
|
|
393
|
+
return ` RETURNING ${columns}`;
|
|
394
|
+
},
|
|
395
|
+
ilikeOperator() {
|
|
396
|
+
return "ILIKE";
|
|
397
|
+
},
|
|
398
|
+
nullsLastSuffix() {
|
|
399
|
+
return " NULLS LAST";
|
|
400
|
+
},
|
|
401
|
+
castToText(expression) {
|
|
402
|
+
return `${expression}::text`;
|
|
403
|
+
},
|
|
404
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
405
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
406
|
+
if (updateColumns.length === 0) {
|
|
407
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
408
|
+
}
|
|
409
|
+
const assignments = updateColumns.map((column) => {
|
|
410
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
411
|
+
return `${quoted} = excluded.${quoted}`;
|
|
412
|
+
}).join(", ");
|
|
413
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
mysqlDialect = {
|
|
417
|
+
driver: "mysql",
|
|
418
|
+
placeholder() {
|
|
419
|
+
return "?";
|
|
420
|
+
},
|
|
421
|
+
quoteIdentifier(identifier) {
|
|
422
|
+
return `\`${assertSafeIdentifier(identifier)}\``;
|
|
423
|
+
},
|
|
424
|
+
nowExpression() {
|
|
425
|
+
return "CURRENT_TIMESTAMP";
|
|
426
|
+
},
|
|
427
|
+
timestampValue(value) {
|
|
428
|
+
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
429
|
+
},
|
|
430
|
+
returningClause() {
|
|
431
|
+
return "";
|
|
432
|
+
},
|
|
433
|
+
ilikeOperator() {
|
|
434
|
+
return "LIKE";
|
|
435
|
+
},
|
|
436
|
+
nullsLastSuffix() {
|
|
437
|
+
return "";
|
|
438
|
+
},
|
|
439
|
+
castToText(expression) {
|
|
440
|
+
return `CAST(${expression} AS CHAR)`;
|
|
441
|
+
},
|
|
442
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
443
|
+
if (updateColumns.length === 0) {
|
|
444
|
+
const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
|
|
445
|
+
return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
|
|
446
|
+
}
|
|
447
|
+
const assignments = updateColumns.map((column) => {
|
|
448
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
449
|
+
return `${quoted} = VALUES(${quoted})`;
|
|
450
|
+
}).join(", ");
|
|
451
|
+
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
sqliteDialect = {
|
|
455
|
+
driver: "sqlite",
|
|
456
|
+
placeholder() {
|
|
457
|
+
return "?";
|
|
458
|
+
},
|
|
459
|
+
quoteIdentifier(identifier) {
|
|
460
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
461
|
+
},
|
|
462
|
+
nowExpression() {
|
|
463
|
+
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
464
|
+
},
|
|
465
|
+
timestampValue(value) {
|
|
466
|
+
return value.toISOString();
|
|
467
|
+
},
|
|
468
|
+
returningClause(columns) {
|
|
469
|
+
return ` RETURNING ${columns}`;
|
|
470
|
+
},
|
|
471
|
+
ilikeOperator() {
|
|
472
|
+
return "LIKE";
|
|
473
|
+
},
|
|
474
|
+
nullsLastSuffix() {
|
|
475
|
+
return "";
|
|
476
|
+
},
|
|
477
|
+
castToText(expression) {
|
|
478
|
+
return `CAST(${expression} AS TEXT)`;
|
|
479
|
+
},
|
|
480
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
481
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
482
|
+
if (updateColumns.length === 0) {
|
|
483
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
484
|
+
}
|
|
485
|
+
const assignments = updateColumns.map((column) => {
|
|
486
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
487
|
+
return `${quoted} = excluded.${quoted}`;
|
|
488
|
+
}).join(", ");
|
|
489
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
dialects = {
|
|
493
|
+
pgsql: postgresDialect,
|
|
494
|
+
mysql: mysqlDialect,
|
|
495
|
+
sqlite: sqliteDialect
|
|
496
|
+
};
|
|
497
|
+
dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
501
|
+
function resolveRepositoryConnection() {
|
|
502
|
+
return getActiveDatabaseConnection(getBoundDatabaseConnection() ?? getDefaultDatabaseQuery());
|
|
503
|
+
}
|
|
504
|
+
function compileTaggedSql(strings, values) {
|
|
505
|
+
const dialect = currentSqlDialect();
|
|
506
|
+
let query = strings[0] ?? "";
|
|
507
|
+
const params = [];
|
|
508
|
+
for (let index = 0;index < values.length; index += 1) {
|
|
509
|
+
params.push(values[index]);
|
|
510
|
+
query += `${dialect.placeholder(params.length)}${strings[index + 1] ?? ""}`;
|
|
511
|
+
}
|
|
512
|
+
return { query, params };
|
|
513
|
+
}
|
|
514
|
+
function isTaggedStrings(value) {
|
|
515
|
+
if (!Array.isArray(value) || typeof value[0] !== "string") {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
return typeof value.raw !== "undefined";
|
|
519
|
+
}
|
|
520
|
+
function runOnConnection(connection, args) {
|
|
521
|
+
if (typeof connection === "function") {
|
|
522
|
+
return connection(...args);
|
|
523
|
+
}
|
|
524
|
+
if (typeof connection.unsafe !== "function") {
|
|
525
|
+
throw new Error("Database connection cannot run SQL.");
|
|
526
|
+
}
|
|
527
|
+
const strings = args[0];
|
|
528
|
+
if (!isTaggedStrings(strings)) {
|
|
529
|
+
throw new Error("Database connection cannot run tagged SQL.");
|
|
530
|
+
}
|
|
531
|
+
const compiled = compileTaggedSql(strings, args.slice(1));
|
|
532
|
+
return connection.unsafe(compiled.query, compiled.params);
|
|
533
|
+
}
|
|
534
|
+
var repositoryConnection;
|
|
535
|
+
var init_repositoryConnection = __esm(() => {
|
|
536
|
+
init_boundConnection();
|
|
537
|
+
init_connectionContext();
|
|
538
|
+
init_defaultConnection();
|
|
539
|
+
init_dialect();
|
|
540
|
+
repositoryConnection = new Proxy(function repositoryConnection() {}, {
|
|
541
|
+
apply(_target, _thisArg, args) {
|
|
542
|
+
return runOnConnection(resolveRepositoryConnection(), args);
|
|
543
|
+
},
|
|
544
|
+
get(_target, property) {
|
|
545
|
+
const connection = resolveRepositoryConnection();
|
|
546
|
+
const value = connection[property];
|
|
547
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// ../../src/core/database/transaction.ts
|
|
553
|
+
async function runInSavepoint(connection, operation) {
|
|
554
|
+
const inherited = transactionFrame.getStore();
|
|
555
|
+
let parent = inherited?.connection === connection ? inherited : outerFrames.get(connection);
|
|
556
|
+
if (!parent) {
|
|
557
|
+
parent = { connection, childActive: false };
|
|
558
|
+
outerFrames.set(connection, parent);
|
|
559
|
+
}
|
|
560
|
+
if (parent.childActive) {
|
|
561
|
+
throw new Error("Concurrent nested transactions on one connection are not supported. Await each transaction before starting the next.");
|
|
562
|
+
}
|
|
563
|
+
parent.childActive = true;
|
|
564
|
+
const name = `strata_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
565
|
+
try {
|
|
566
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
567
|
+
await connection.unsafe(`SAVEPOINT ${name}`);
|
|
568
|
+
try {
|
|
569
|
+
const result = await transactionFrame.run({ connection, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(connection)))));
|
|
570
|
+
await connection.unsafe(`RELEASE SAVEPOINT ${name}`);
|
|
571
|
+
return result;
|
|
572
|
+
} catch (error) {
|
|
573
|
+
await connection.unsafe(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
574
|
+
await connection.unsafe(`RELEASE SAVEPOINT ${name}`);
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
}));
|
|
578
|
+
} finally {
|
|
579
|
+
parent.childActive = false;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function supportsTransactions(connection) {
|
|
583
|
+
return typeof connection.begin === "function";
|
|
584
|
+
}
|
|
585
|
+
async function runInTransaction(operation) {
|
|
586
|
+
const pool = resolveRepositoryConnection();
|
|
587
|
+
if (hasActiveDatabaseConnection()) {
|
|
588
|
+
return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
|
|
589
|
+
}
|
|
590
|
+
if (!supportsTransactions(pool)) {
|
|
591
|
+
throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
|
|
592
|
+
}
|
|
593
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
594
|
+
return await pool.begin(async (transaction) => {
|
|
595
|
+
return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(transaction))))));
|
|
596
|
+
});
|
|
597
|
+
}));
|
|
598
|
+
}
|
|
599
|
+
var transactionFrame, outerFrames;
|
|
600
|
+
var init_transaction = __esm(() => {
|
|
601
|
+
init_deferredModelEvents();
|
|
602
|
+
init_asyncContextStore();
|
|
603
|
+
init_connection();
|
|
604
|
+
init_connectionContext();
|
|
605
|
+
init_repositoryConnection();
|
|
606
|
+
init_transactionControl();
|
|
607
|
+
transactionFrame = createAsyncContextStore("@getstrata/transactionFrame");
|
|
608
|
+
outerFrames = new WeakMap;
|
|
609
|
+
});
|
|
30
610
|
|
|
31
611
|
// ../../src/core/tenant/tenancyConfig.ts
|
|
32
|
-
var TENANCY_DRIVERS = ["rls", "column", "none"];
|
|
33
612
|
function readTenancyDriver(env = process.env) {
|
|
34
613
|
const raw = env.TENANCY_DRIVER?.trim();
|
|
35
614
|
if (raw === undefined || raw === "") {
|
|
@@ -46,40 +625,131 @@ function isTenancyEnabled(env = process.env) {
|
|
|
46
625
|
function isRlsTenancy(env = process.env) {
|
|
47
626
|
return readTenancyDriver(env) === "rls";
|
|
48
627
|
}
|
|
628
|
+
var TENANCY_DRIVERS;
|
|
629
|
+
var init_tenancyConfig = __esm(() => {
|
|
630
|
+
TENANCY_DRIVERS = ["rls", "column", "none"];
|
|
631
|
+
});
|
|
49
632
|
|
|
50
|
-
// ../../src/core/tenant/
|
|
51
|
-
|
|
52
|
-
function
|
|
53
|
-
|
|
633
|
+
// ../../src/core/tenant/databaseTenantContext.ts
|
|
634
|
+
import { getDefaultDatabasePool as getDefaultDatabasePool2 } from "@getstrata/core/database/defaultConnection";
|
|
635
|
+
async function applyBypassToTransaction(transaction, bypass) {
|
|
636
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, [
|
|
637
|
+
bypass ? "true" : "false"
|
|
638
|
+
]);
|
|
54
639
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
640
|
+
function stringifyBypassIdentifier(identifier) {
|
|
641
|
+
if (typeof identifier === "number") {
|
|
642
|
+
return String(identifier);
|
|
643
|
+
}
|
|
644
|
+
return identifier.trim();
|
|
57
645
|
}
|
|
646
|
+
async function applyIdentifierToTransaction(transaction, identifier) {
|
|
647
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_identifier', $1, true)`, [
|
|
648
|
+
stringifyBypassIdentifier(identifier)
|
|
649
|
+
]);
|
|
650
|
+
}
|
|
651
|
+
function assertBypassIdentifier(identifier) {
|
|
652
|
+
if (typeof identifier === "number") {
|
|
653
|
+
if (!Number.isInteger(identifier) || identifier <= 0) {
|
|
654
|
+
throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (typeof identifier === "string" && identifier.trim() !== "") {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
throw new Error("RLS bypass requires a caller-supplied identifier that pins the row.");
|
|
662
|
+
}
|
|
663
|
+
async function runWithScopedTenantTransaction(apply, callback) {
|
|
664
|
+
if (!isRlsTenancy()) {
|
|
665
|
+
return await callback();
|
|
666
|
+
}
|
|
667
|
+
if (hasActiveDatabaseConnection()) {
|
|
668
|
+
const connection = getActiveDatabaseConnection(getDefaultDatabasePool2());
|
|
669
|
+
return await runInTransaction(async () => {
|
|
670
|
+
const [previous] = await connection.unsafe(`SELECT current_setting('app.tenant_id', true) AS tenant_id,
|
|
671
|
+
current_setting('app.bypass_rls', true) AS bypass_rls,
|
|
672
|
+
current_setting('app.bypass_identifier', true) AS bypass_identifier`);
|
|
673
|
+
await apply(connection);
|
|
674
|
+
const result = await callback();
|
|
675
|
+
await connection.unsafe(`SELECT set_config('app.tenant_id', $1, true),
|
|
676
|
+
set_config('app.bypass_rls', $2, true), set_config('app.bypass_identifier', $3, true)`, [previous?.tenant_id ?? "", previous?.bypass_rls ?? "", previous?.bypass_identifier ?? ""]);
|
|
677
|
+
return result;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
const pool = getDefaultDatabasePool2();
|
|
681
|
+
const begin = pool.begin;
|
|
682
|
+
if (typeof begin !== "function") {
|
|
683
|
+
throw new Error("RLS migration bypass requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
684
|
+
}
|
|
685
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
686
|
+
return await begin.bind(pool)(async (transaction) => {
|
|
687
|
+
await apply(transaction);
|
|
688
|
+
return await runWithDatabaseConnection(transaction, () => runWithTransactionScope(async () => commitOrRollbackScope(await callback())));
|
|
689
|
+
});
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
async function runWithMigrationBypass(callback) {
|
|
693
|
+
return await runWithScopedTenantTransaction((transaction) => applyBypassToTransaction(transaction, true), callback);
|
|
694
|
+
}
|
|
695
|
+
async function runWithMigrationBypassForIdentifier(identifier, callback) {
|
|
696
|
+
assertBypassIdentifier(identifier);
|
|
697
|
+
return await runWithScopedTenantTransaction((transaction) => applyIdentifierToTransaction(transaction, identifier), callback);
|
|
698
|
+
}
|
|
699
|
+
var init_databaseTenantContext = __esm(() => {
|
|
700
|
+
init_connectionContext();
|
|
701
|
+
init_transaction();
|
|
702
|
+
init_transactionControl();
|
|
703
|
+
init_deferredModelEvents();
|
|
704
|
+
init_tenancyConfig();
|
|
705
|
+
});
|
|
58
706
|
|
|
59
707
|
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
708
|
+
import { getDefaultDatabasePool as getDefaultDatabasePool3 } from "@getstrata/core/database/defaultConnection";
|
|
60
709
|
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
61
710
|
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
62
711
|
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
63
712
|
}
|
|
64
713
|
async function runWithTenantDatabase(tenant, callback) {
|
|
714
|
+
const outerTenant = currentTenant();
|
|
715
|
+
if (hasActiveDatabaseConnection() && outerTenant && outerTenant.id !== tenant.id) {
|
|
716
|
+
throw new Error("Cannot switch tenants inside an active database transaction.");
|
|
717
|
+
}
|
|
65
718
|
if (!isTenancyEnabled() || !isRlsTenancy()) {
|
|
66
719
|
return await runWithTenant(tenant, callback);
|
|
67
720
|
}
|
|
68
721
|
if (hasActiveDatabaseConnection()) {
|
|
69
|
-
const activeConnection = getActiveDatabaseConnection(
|
|
70
|
-
await applyTenantContextToTransaction(activeConnection, tenant.id);
|
|
71
|
-
|
|
722
|
+
const activeConnection = getActiveDatabaseConnection(getDefaultDatabasePool3());
|
|
723
|
+
return await runWithScopedTenantTransaction(() => applyTenantContextToTransaction(activeConnection, tenant.id), () => runWithTenant(tenant, callback));
|
|
724
|
+
}
|
|
725
|
+
const pool = getDefaultDatabasePool3();
|
|
726
|
+
const begin = pool.begin;
|
|
727
|
+
if (typeof begin !== "function") {
|
|
728
|
+
throw new Error("RLS tenant scope requires a pool that supports begin(). Session-scoped set_config is not used on pooled connections.");
|
|
72
729
|
}
|
|
73
|
-
return await
|
|
74
|
-
await
|
|
75
|
-
|
|
76
|
-
return await
|
|
730
|
+
return await settleTransaction(() => runWithDeferredModelEvents(async () => {
|
|
731
|
+
return await begin.bind(pool)(async (transaction) => {
|
|
732
|
+
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
733
|
+
return await runWithDatabaseConnection(transaction, async () => {
|
|
734
|
+
return await runWithTransactionScope(async () => commitOrRollbackScope(await runWithTenant(tenant, callback)));
|
|
735
|
+
});
|
|
77
736
|
});
|
|
78
|
-
});
|
|
737
|
+
}));
|
|
79
738
|
}
|
|
80
739
|
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
81
740
|
return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
|
|
82
741
|
}
|
|
742
|
+
var init_tenantDatabaseScope = __esm(() => {
|
|
743
|
+
init_connectionContext();
|
|
744
|
+
init_transactionControl();
|
|
745
|
+
init_deferredModelEvents();
|
|
746
|
+
init_databaseTenantContext();
|
|
747
|
+
init_tenancyConfig();
|
|
748
|
+
init_tenantContext();
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// entries/tenant/tenantDatabaseScope.ts
|
|
752
|
+
init_tenantDatabaseScope();
|
|
83
753
|
export {
|
|
84
754
|
applyTenantContextToTransaction,
|
|
85
755
|
isInsideTenantDatabaseScope,
|