@getstrata/bootstrap 0.2.26 → 0.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/listeners/invalidateCacheOnModelWrite.d.ts +1 -1
- package/dist/entries/buildModuleRoutes.js +3 -0
- package/dist/entries/buildWebModuleRoutes.js +3 -0
- package/dist/entries/context.js +148 -124
- package/dist/entries/createSpaRoutes.js +62 -0
- package/dist/entries/createWebRoutes.js +3 -0
- package/dist/entries/dependencies.js +148 -124
- package/dist/entries/health.js +212 -0
- package/dist/entries/httpKernel.js +3 -0
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +118 -0
- package/dist/entries/metricsRoutes.js +16 -0
- package/dist/entries/providers/view.js +22 -0
- package/dist/entries/providers.js +226 -202
- package/dist/entries/web/routing.js +3 -0
- package/dist/index.js +441 -555
- package/package.json +22 -2
|
@@ -284,6 +284,25 @@ function getDb() {
|
|
|
284
284
|
getDatabase();
|
|
285
285
|
return getDefaultDatabaseQuery();
|
|
286
286
|
}
|
|
287
|
+
async function pingDatabase(connection = getDatabase()) {
|
|
288
|
+
try {
|
|
289
|
+
await connection`SELECT 1`;
|
|
290
|
+
return true;
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function ensureDatabaseConnection() {
|
|
296
|
+
if (await pingDatabase()) {
|
|
297
|
+
return getDatabase();
|
|
298
|
+
}
|
|
299
|
+
await getDatabase().close().catch(() => {
|
|
300
|
+
return;
|
|
301
|
+
});
|
|
302
|
+
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
303
|
+
registerDefaultDatabasePool(connectionHolder.connection);
|
|
304
|
+
return getDatabase();
|
|
305
|
+
}
|
|
287
306
|
var db = new Proxy(function database() {}, {
|
|
288
307
|
apply(_target, _thisArg, args) {
|
|
289
308
|
return getDb()(...args);
|
|
@@ -1393,25 +1412,117 @@ function discoverListeners() {
|
|
|
1393
1412
|
return appListeners;
|
|
1394
1413
|
}
|
|
1395
1414
|
|
|
1396
|
-
// ../../src/
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1415
|
+
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
1416
|
+
import { eventBus as eventBus2, modelEventName as modelEventName2 } from "@getstrata/core/events";
|
|
1417
|
+
import InvalidateCacheTagsJob from "@getstrata/core/jobs/invalidateCacheTagsJob";
|
|
1418
|
+
import { createTrackedJob } from "@getstrata/core/queue/createAppQueue";
|
|
1419
|
+
|
|
1420
|
+
// ../../src/bootstrap/cache/modelCacheTags.ts
|
|
1421
|
+
function cacheTagsForModelWrite(tableName, action) {
|
|
1422
|
+
const module = discoverModules().find((entry) => entry.tableName === tableName);
|
|
1423
|
+
const baseTags = module?.cacheTags ?? [`${tableName}s`];
|
|
1424
|
+
const isDelete = action === "deleted" || action === "force-deleted";
|
|
1425
|
+
const extraTags = isDelete ? module?.cacheDeleteExtraTags ?? [] : [];
|
|
1426
|
+
return [...new Set([...baseTags, ...extraTags])];
|
|
1427
|
+
}
|
|
1428
|
+
function discoverModelTableNames() {
|
|
1429
|
+
return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
|
|
1401
1430
|
}
|
|
1402
1431
|
|
|
1403
|
-
// ../../src/
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1432
|
+
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
1433
|
+
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
1434
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
|
|
1435
|
+
for (const tableName of discoverModelTableNames()) {
|
|
1436
|
+
for (const action of MODEL_WRITE_ACTIONS) {
|
|
1437
|
+
bus.listen(modelEventName2(tableName, action), async () => {
|
|
1438
|
+
const tags = cacheTagsForModelWrite(tableName, action);
|
|
1439
|
+
if (tags.length === 0) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
let cache;
|
|
1443
|
+
let queue;
|
|
1444
|
+
try {
|
|
1445
|
+
cache = resolveApplicationCache();
|
|
1446
|
+
queue = resolveApplicationQueue();
|
|
1447
|
+
} catch {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
1451
|
+
await queue.dispatch(job, { tags });
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1409
1454
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// ../../src/bootstrap/providers/listeners.ts
|
|
1458
|
+
var registeredListenerGroups = new Set;
|
|
1459
|
+
function registerListenerGroup(name, register) {
|
|
1460
|
+
if (registeredListenerGroups.has(name)) {
|
|
1461
|
+
return;
|
|
1412
1462
|
}
|
|
1463
|
+
registeredListenerGroups.add(name);
|
|
1464
|
+
register();
|
|
1413
1465
|
}
|
|
1414
|
-
var
|
|
1466
|
+
var listenersProvider = {
|
|
1467
|
+
name: "core.listeners",
|
|
1468
|
+
boot() {
|
|
1469
|
+
registerListenerGroup("cache.invalidate-on-model-write", () => {
|
|
1470
|
+
registerInvalidateCacheOnModelWriteListeners();
|
|
1471
|
+
});
|
|
1472
|
+
for (const [index, registerListener] of discoverListeners().entries()) {
|
|
1473
|
+
registerListenerGroup(`app.listener.${index}`, registerListener);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
var listeners_default = listenersProvider;
|
|
1478
|
+
|
|
1479
|
+
// ../../src/core/auth/policy.ts
|
|
1480
|
+
var BLOCKED_POLICY_ACTIONS = new Set([
|
|
1481
|
+
"constructor",
|
|
1482
|
+
"toString",
|
|
1483
|
+
"valueOf",
|
|
1484
|
+
"hasOwnProperty",
|
|
1485
|
+
"isPrototypeOf",
|
|
1486
|
+
"propertyIsEnumerable",
|
|
1487
|
+
"__proto__"
|
|
1488
|
+
]);
|
|
1489
|
+
|
|
1490
|
+
class PolicyGate {
|
|
1491
|
+
constructor() {}
|
|
1492
|
+
policies = new Map;
|
|
1493
|
+
register(resource, policy) {
|
|
1494
|
+
this.policies.set(resource, policy);
|
|
1495
|
+
}
|
|
1496
|
+
allows(resource, action, user, model) {
|
|
1497
|
+
const policy = this.policies.get(resource);
|
|
1498
|
+
if (!policy) {
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
const handler = policy[action];
|
|
1505
|
+
if (typeof handler !== "function") {
|
|
1506
|
+
return false;
|
|
1507
|
+
}
|
|
1508
|
+
const resolvedUser = user === undefined ? currentAuthUser() : user;
|
|
1509
|
+
return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
|
|
1510
|
+
}
|
|
1511
|
+
authorize(resource, action, user, model) {
|
|
1512
|
+
if (!this.allows(resource, action, user, model)) {
|
|
1513
|
+
throw new ForbiddenError2;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// ../../src/bootstrap/providers/policy.ts
|
|
1519
|
+
var policyProvider = {
|
|
1520
|
+
name: "core.policy",
|
|
1521
|
+
register({ container }) {
|
|
1522
|
+
container.set(CORE_POLICY_GATE_TOKEN, new PolicyGate);
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
var policy_default = policyProvider;
|
|
1415
1526
|
|
|
1416
1527
|
// ../../src/core/pagination/index.ts
|
|
1417
1528
|
function buildPaginationMeta(input) {
|
|
@@ -3299,9 +3410,6 @@ class ResilientQueue {
|
|
|
3299
3410
|
function createFailedJobService() {
|
|
3300
3411
|
return new failedJobService_default(new failedJobRepository_default);
|
|
3301
3412
|
}
|
|
3302
|
-
function createTrackedJob(name, job) {
|
|
3303
|
-
return jobRegistry.track(name, job);
|
|
3304
|
-
}
|
|
3305
3413
|
function createProductionQueue(driver, options = {}) {
|
|
3306
3414
|
options.registerJobs?.();
|
|
3307
3415
|
const failedJobs = options.failedJobs ?? createFailedJobService();
|
|
@@ -3324,116 +3432,16 @@ function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(),
|
|
|
3324
3432
|
});
|
|
3325
3433
|
}
|
|
3326
3434
|
|
|
3327
|
-
// ../../src/bootstrap/cache/modelCacheTags.ts
|
|
3328
|
-
function cacheTagsForModelWrite(tableName, action) {
|
|
3329
|
-
const module = discoverModules().find((entry) => entry.tableName === tableName);
|
|
3330
|
-
const baseTags = module?.cacheTags ?? [`${tableName}s`];
|
|
3331
|
-
const isDelete = action === "deleted" || action === "force-deleted";
|
|
3332
|
-
const extraTags = isDelete ? module?.cacheDeleteExtraTags ?? [] : [];
|
|
3333
|
-
return [...new Set([...baseTags, ...extraTags])];
|
|
3334
|
-
}
|
|
3335
|
-
function discoverModelTableNames() {
|
|
3336
|
-
return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
|
|
3337
|
-
}
|
|
3338
|
-
|
|
3339
|
-
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
3340
|
-
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
3341
|
-
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
3342
|
-
for (const tableName of discoverModelTableNames()) {
|
|
3343
|
-
for (const action of MODEL_WRITE_ACTIONS) {
|
|
3344
|
-
bus.listen(modelEventName(tableName, action), async () => {
|
|
3345
|
-
const tags = cacheTagsForModelWrite(tableName, action);
|
|
3346
|
-
if (tags.length === 0) {
|
|
3347
|
-
return;
|
|
3348
|
-
}
|
|
3349
|
-
let cache;
|
|
3350
|
-
let queue;
|
|
3351
|
-
try {
|
|
3352
|
-
cache = resolveApplicationCache();
|
|
3353
|
-
queue = resolveApplicationQueue();
|
|
3354
|
-
} catch {
|
|
3355
|
-
return;
|
|
3356
|
-
}
|
|
3357
|
-
const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
|
|
3358
|
-
await queue.dispatch(job, { tags });
|
|
3359
|
-
});
|
|
3360
|
-
}
|
|
3361
|
-
}
|
|
3362
|
-
}
|
|
3363
|
-
|
|
3364
|
-
// ../../src/bootstrap/providers/listeners.ts
|
|
3365
|
-
var registeredListenerGroups = new Set;
|
|
3366
|
-
function registerListenerGroup(name, register) {
|
|
3367
|
-
if (registeredListenerGroups.has(name)) {
|
|
3368
|
-
return;
|
|
3369
|
-
}
|
|
3370
|
-
registeredListenerGroups.add(name);
|
|
3371
|
-
register();
|
|
3372
|
-
}
|
|
3373
|
-
var listenersProvider = {
|
|
3374
|
-
name: "core.listeners",
|
|
3375
|
-
boot() {
|
|
3376
|
-
registerListenerGroup("cache.invalidate-on-model-write", () => {
|
|
3377
|
-
registerInvalidateCacheOnModelWriteListeners();
|
|
3378
|
-
});
|
|
3379
|
-
for (const [index, registerListener] of discoverListeners().entries()) {
|
|
3380
|
-
registerListenerGroup(`app.listener.${index}`, registerListener);
|
|
3381
|
-
}
|
|
3382
|
-
}
|
|
3383
|
-
};
|
|
3384
|
-
var listeners_default = listenersProvider;
|
|
3385
|
-
|
|
3386
|
-
// ../../src/core/auth/policy.ts
|
|
3387
|
-
var BLOCKED_POLICY_ACTIONS = new Set([
|
|
3388
|
-
"constructor",
|
|
3389
|
-
"toString",
|
|
3390
|
-
"valueOf",
|
|
3391
|
-
"hasOwnProperty",
|
|
3392
|
-
"isPrototypeOf",
|
|
3393
|
-
"propertyIsEnumerable",
|
|
3394
|
-
"__proto__"
|
|
3395
|
-
]);
|
|
3396
|
-
|
|
3397
|
-
class PolicyGate {
|
|
3398
|
-
constructor() {}
|
|
3399
|
-
policies = new Map;
|
|
3400
|
-
register(resource, policy) {
|
|
3401
|
-
this.policies.set(resource, policy);
|
|
3402
|
-
}
|
|
3403
|
-
allows(resource, action, user, model) {
|
|
3404
|
-
const policy = this.policies.get(resource);
|
|
3405
|
-
if (!policy) {
|
|
3406
|
-
return false;
|
|
3407
|
-
}
|
|
3408
|
-
if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
|
|
3409
|
-
return false;
|
|
3410
|
-
}
|
|
3411
|
-
const handler = policy[action];
|
|
3412
|
-
if (typeof handler !== "function") {
|
|
3413
|
-
return false;
|
|
3414
|
-
}
|
|
3415
|
-
const resolvedUser = user === undefined ? currentAuthUser() : user;
|
|
3416
|
-
return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
|
|
3417
|
-
}
|
|
3418
|
-
authorize(resource, action, user, model) {
|
|
3419
|
-
if (!this.allows(resource, action, user, model)) {
|
|
3420
|
-
throw new ForbiddenError2;
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
|
|
3425
|
-
// ../../src/bootstrap/providers/policy.ts
|
|
3426
|
-
var policyProvider = {
|
|
3427
|
-
name: "core.policy",
|
|
3428
|
-
register({ container }) {
|
|
3429
|
-
container.set(CORE_POLICY_GATE_TOKEN, new PolicyGate);
|
|
3430
|
-
}
|
|
3431
|
-
};
|
|
3432
|
-
var policy_default = policyProvider;
|
|
3433
|
-
|
|
3434
3435
|
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3435
3436
|
import { createHmac as createHmac2 } from "crypto";
|
|
3436
3437
|
|
|
3438
|
+
// ../../src/core/queue/index.ts
|
|
3439
|
+
class Job {
|
|
3440
|
+
maxAttempts;
|
|
3441
|
+
backoffMs;
|
|
3442
|
+
priority;
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3437
3445
|
// ../../src/core/security/safeUrl.ts
|
|
3438
3446
|
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
3439
3447
|
var dnsLookup = dnsLookupImpl;
|
|
@@ -3618,6 +3626,19 @@ class DispatchWebhookJob extends Job {
|
|
|
3618
3626
|
}
|
|
3619
3627
|
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
3620
3628
|
|
|
3629
|
+
// ../../src/core/jobs/invalidateCacheTagsJob.ts
|
|
3630
|
+
class InvalidateCacheTagsJob2 extends Job {
|
|
3631
|
+
cache;
|
|
3632
|
+
constructor(cache) {
|
|
3633
|
+
super();
|
|
3634
|
+
this.cache = cache;
|
|
3635
|
+
}
|
|
3636
|
+
async handle(payload) {
|
|
3637
|
+
await this.cache.tags(...payload.tags).flush();
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob2;
|
|
3641
|
+
|
|
3621
3642
|
// ../../src/core/contracts/di.ts
|
|
3622
3643
|
function getRequiredDependency2(dependencies, key) {
|
|
3623
3644
|
const dependency = dependencies[key];
|
|
@@ -3855,6 +3876,9 @@ function readFrontendMode() {
|
|
|
3855
3876
|
function isViewsEnabled() {
|
|
3856
3877
|
return readFrontendMode() === "server-htmx";
|
|
3857
3878
|
}
|
|
3879
|
+
function isSpaEnabled() {
|
|
3880
|
+
return readFrontendMode() === "spa-react";
|
|
3881
|
+
}
|
|
3858
3882
|
|
|
3859
3883
|
// ../../src/core/http/requestMetaContext.ts
|
|
3860
3884
|
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/bootstrap/health.ts
|
|
3
|
+
import { jsonResponse } from "@getstrata/core/http";
|
|
4
|
+
var {RedisClient } = globalThis.Bun;
|
|
5
|
+
|
|
6
|
+
// ../../src/config/database.ts
|
|
7
|
+
function readInteger(name, fallback) {
|
|
8
|
+
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
9
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
10
|
+
}
|
|
11
|
+
var databaseConfig = {
|
|
12
|
+
url: process.env.DATABASE_URL ?? "",
|
|
13
|
+
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
14
|
+
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
15
|
+
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
16
|
+
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// ../../src/core/runtime/asyncContextStore.ts
|
|
20
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
21
|
+
function createAsyncContextStore(key) {
|
|
22
|
+
const symbol = Symbol.for(key);
|
|
23
|
+
const globalRecord = globalThis;
|
|
24
|
+
const existing = globalRecord[symbol];
|
|
25
|
+
if (existing) {
|
|
26
|
+
return existing;
|
|
27
|
+
}
|
|
28
|
+
const store = new AsyncLocalStorage;
|
|
29
|
+
globalRecord[symbol] = store;
|
|
30
|
+
return store;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ../../src/core/database/connectionContext.ts
|
|
34
|
+
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
35
|
+
function getActiveDatabaseConnection(fallback) {
|
|
36
|
+
return activeConnection.getStore() ?? fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ../../src/core/database/queryProxy.ts
|
|
40
|
+
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
41
|
+
function createDatabaseQueryProxy(pool) {
|
|
42
|
+
function resolveDatabase() {
|
|
43
|
+
return getActiveDatabaseConnection(pool);
|
|
44
|
+
}
|
|
45
|
+
function resolveDatabaseForProperty(property) {
|
|
46
|
+
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
47
|
+
return pool;
|
|
48
|
+
}
|
|
49
|
+
return resolveDatabase();
|
|
50
|
+
}
|
|
51
|
+
return new Proxy(function database() {}, {
|
|
52
|
+
apply(_target, _thisArg, args) {
|
|
53
|
+
return resolveDatabase()(...args);
|
|
54
|
+
},
|
|
55
|
+
get(_target, property) {
|
|
56
|
+
const connection = resolveDatabaseForProperty(property);
|
|
57
|
+
const value = connection[property];
|
|
58
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ../../src/core/database/defaultConnection.ts
|
|
64
|
+
var defaultPool = {
|
|
65
|
+
connection: null
|
|
66
|
+
};
|
|
67
|
+
var defaultQuery = {
|
|
68
|
+
connection: null
|
|
69
|
+
};
|
|
70
|
+
function registerDefaultDatabasePool(connection) {
|
|
71
|
+
defaultPool.connection = connection;
|
|
72
|
+
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
73
|
+
}
|
|
74
|
+
function getDefaultDatabaseQuery() {
|
|
75
|
+
if (!defaultQuery.connection) {
|
|
76
|
+
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
77
|
+
}
|
|
78
|
+
return defaultQuery.connection;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ../../src/db/connection/createConnection.ts
|
|
82
|
+
var {SQL } = globalThis.Bun;
|
|
83
|
+
function createDatabaseConnection(config) {
|
|
84
|
+
if (!config.url) {
|
|
85
|
+
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
86
|
+
}
|
|
87
|
+
return new SQL({
|
|
88
|
+
url: config.url,
|
|
89
|
+
max: config.poolMax,
|
|
90
|
+
idleTimeout: config.idleTimeoutSeconds,
|
|
91
|
+
maxLifetime: config.maxLifetimeSeconds,
|
|
92
|
+
connectionTimeout: config.connectionTimeoutSeconds
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ../../src/db/connection/index.ts
|
|
97
|
+
var connectionHolder = {
|
|
98
|
+
connection: null
|
|
99
|
+
};
|
|
100
|
+
function getDatabase() {
|
|
101
|
+
if (!connectionHolder.connection) {
|
|
102
|
+
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
103
|
+
registerDefaultDatabasePool(connectionHolder.connection);
|
|
104
|
+
}
|
|
105
|
+
return connectionHolder.connection;
|
|
106
|
+
}
|
|
107
|
+
function getDb() {
|
|
108
|
+
getDatabase();
|
|
109
|
+
return getDefaultDatabaseQuery();
|
|
110
|
+
}
|
|
111
|
+
async function pingDatabase(connection = getDatabase()) {
|
|
112
|
+
try {
|
|
113
|
+
await connection`SELECT 1`;
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function ensureDatabaseConnection() {
|
|
120
|
+
if (await pingDatabase()) {
|
|
121
|
+
return getDatabase();
|
|
122
|
+
}
|
|
123
|
+
await getDatabase().close().catch(() => {
|
|
124
|
+
return;
|
|
125
|
+
});
|
|
126
|
+
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
127
|
+
registerDefaultDatabasePool(connectionHolder.connection);
|
|
128
|
+
return getDatabase();
|
|
129
|
+
}
|
|
130
|
+
var db = new Proxy(function database() {}, {
|
|
131
|
+
apply(_target, _thisArg, args) {
|
|
132
|
+
return getDb()(...args);
|
|
133
|
+
},
|
|
134
|
+
get(_target, property) {
|
|
135
|
+
const connection = getDb();
|
|
136
|
+
const value = connection[property];
|
|
137
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ../../src/bootstrap/config.ts
|
|
142
|
+
import {
|
|
143
|
+
CORE_AUTH_TOKEN,
|
|
144
|
+
CORE_CACHE_TOKEN,
|
|
145
|
+
CORE_CONFIG_TOKEN,
|
|
146
|
+
CORE_EVENT_BUS_TOKEN,
|
|
147
|
+
CORE_POLICY_GATE_TOKEN,
|
|
148
|
+
CORE_QUEUE_TOKEN,
|
|
149
|
+
CORE_TOKEN_SERVICE_TOKEN
|
|
150
|
+
} from "@getstrata/core/contracts/serviceTokens";
|
|
151
|
+
var APP_PORT_CONFIG_KEY = "app.port";
|
|
152
|
+
var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
|
|
153
|
+
var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
|
|
154
|
+
var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
|
|
155
|
+
var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
|
|
156
|
+
var DATABASE_URL_CONFIG_KEY = "database.url";
|
|
157
|
+
var AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
|
|
158
|
+
var DEFAULT_APP_PORT = 3000;
|
|
159
|
+
var DEFAULT_CACHE_TTL_MS = 3600000;
|
|
160
|
+
var DEFAULT_CACHE_MAX_ENTRIES = 100;
|
|
161
|
+
var DEFAULT_CACHE_DRIVER = "array";
|
|
162
|
+
var DEFAULT_API_TOKEN = "";
|
|
163
|
+
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
164
|
+
|
|
165
|
+
// ../../src/bootstrap/health.ts
|
|
166
|
+
function resolveRedisUrl(dependencies) {
|
|
167
|
+
if (!dependencies.container.has(CORE_CONFIG_TOKEN)) {
|
|
168
|
+
return process.env.REDIS_URL?.trim() || undefined;
|
|
169
|
+
}
|
|
170
|
+
const config = dependencies.container.resolve(CORE_CONFIG_TOKEN);
|
|
171
|
+
const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim();
|
|
172
|
+
return redisUrl || undefined;
|
|
173
|
+
}
|
|
174
|
+
async function checkDatabase() {
|
|
175
|
+
await ensureDatabaseConnection();
|
|
176
|
+
return await pingDatabase();
|
|
177
|
+
}
|
|
178
|
+
async function checkRedis(redisUrl) {
|
|
179
|
+
try {
|
|
180
|
+
const client = new RedisClient(redisUrl);
|
|
181
|
+
const response = await client.ping();
|
|
182
|
+
return response === "PONG";
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function createHealthRoutes(dependencies) {
|
|
188
|
+
return {
|
|
189
|
+
"/health": async () => jsonResponse({ status: "ok" }),
|
|
190
|
+
"/ready": async () => {
|
|
191
|
+
const checks = {
|
|
192
|
+
database: await checkDatabase() ? "ok" : "error"
|
|
193
|
+
};
|
|
194
|
+
const redisUrl = resolveRedisUrl(dependencies);
|
|
195
|
+
if (redisUrl) {
|
|
196
|
+
checks.redis = await checkRedis(redisUrl) ? "ok" : "error";
|
|
197
|
+
} else {
|
|
198
|
+
checks.redis = "skipped";
|
|
199
|
+
}
|
|
200
|
+
const ready = checks.database === "ok" && (checks.redis === "ok" || checks.redis === "skipped");
|
|
201
|
+
return jsonResponse({
|
|
202
|
+
status: ready ? "ready" : "not_ready",
|
|
203
|
+
checks
|
|
204
|
+
}, { status: ready ? 200 : 503 });
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export {
|
|
209
|
+
createHealthRoutes,
|
|
210
|
+
checkRedis,
|
|
211
|
+
checkDatabase
|
|
212
|
+
};
|
|
@@ -39,6 +39,9 @@ function readFrontendMode() {
|
|
|
39
39
|
function isViewsEnabled() {
|
|
40
40
|
return readFrontendMode() === "server-htmx";
|
|
41
41
|
}
|
|
42
|
+
function isSpaEnabled() {
|
|
43
|
+
return readFrontendMode() === "spa-react";
|
|
44
|
+
}
|
|
42
45
|
|
|
43
46
|
// ../../src/config/rateLimit.ts
|
|
44
47
|
var LOCAL_LOGIN_RATE_LIMIT = {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
3
|
+
import { eventBus, modelEventName } from "@getstrata/core/events";
|
|
4
|
+
import InvalidateCacheTagsJob from "@getstrata/core/jobs/invalidateCacheTagsJob";
|
|
5
|
+
import { createTrackedJob } from "@getstrata/core/queue/createAppQueue";
|
|
6
|
+
|
|
7
|
+
// ../../src/bootstrap/applicationRegistry.ts
|
|
8
|
+
import {
|
|
9
|
+
resolveApplicationAuth,
|
|
10
|
+
resolveApplicationCache,
|
|
11
|
+
resolveApplicationConfig,
|
|
12
|
+
resolveApplicationDependencies,
|
|
13
|
+
resolveApplicationEventBus,
|
|
14
|
+
resolveApplicationLogger,
|
|
15
|
+
resolveApplicationPolicyGate,
|
|
16
|
+
resolveApplicationQueue,
|
|
17
|
+
setActiveApplicationContext
|
|
18
|
+
} from "@getstrata/core/runtime/applicationRegistry";
|
|
19
|
+
|
|
20
|
+
// ../../src/bootstrap/discoverModules.ts
|
|
21
|
+
import { readdirSync } from "fs";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
import { pathToFileURL } from "url";
|
|
24
|
+
var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
|
|
25
|
+
function readDiscoverModulesState() {
|
|
26
|
+
const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
|
|
27
|
+
if (existing) {
|
|
28
|
+
return existing;
|
|
29
|
+
}
|
|
30
|
+
const state = { appModules: [] };
|
|
31
|
+
globalThis[DISCOVER_MODULES_STATE_KEY] = state;
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
function configureModulesDirectory(modulesDir) {
|
|
35
|
+
readDiscoverModulesState().configuredModulesDir = modulesDir;
|
|
36
|
+
}
|
|
37
|
+
function resolveModulesDirectory(options) {
|
|
38
|
+
const state = readDiscoverModulesState();
|
|
39
|
+
if (options?.modulesDir) {
|
|
40
|
+
return options.modulesDir;
|
|
41
|
+
}
|
|
42
|
+
if (state.configuredModulesDir) {
|
|
43
|
+
return state.configuredModulesDir;
|
|
44
|
+
}
|
|
45
|
+
return join(import.meta.dir, "../modules");
|
|
46
|
+
}
|
|
47
|
+
async function loadDiscoveredModules(options) {
|
|
48
|
+
const modulesDirectory = resolveModulesDirectory(options);
|
|
49
|
+
let moduleNames;
|
|
50
|
+
try {
|
|
51
|
+
moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.code === "ENOENT") {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const modules = await Promise.all(moduleNames.map(async (moduleName) => {
|
|
59
|
+
const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
|
|
60
|
+
const loaded = await import(moduleUrl);
|
|
61
|
+
return loaded.default;
|
|
62
|
+
}));
|
|
63
|
+
return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
64
|
+
}
|
|
65
|
+
async function ensureModulesLoaded(options) {
|
|
66
|
+
const state = readDiscoverModulesState();
|
|
67
|
+
if (state.appModules.length > 0) {
|
|
68
|
+
return state.appModules;
|
|
69
|
+
}
|
|
70
|
+
state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
|
|
71
|
+
state.appModules.splice(0, state.appModules.length, ...modules);
|
|
72
|
+
return state.appModules;
|
|
73
|
+
});
|
|
74
|
+
return state.modulesReady;
|
|
75
|
+
}
|
|
76
|
+
function discoverModules() {
|
|
77
|
+
return readDiscoverModulesState().appModules;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ../../src/bootstrap/cache/modelCacheTags.ts
|
|
81
|
+
function cacheTagsForModelWrite(tableName, action) {
|
|
82
|
+
const module = discoverModules().find((entry) => entry.tableName === tableName);
|
|
83
|
+
const baseTags = module?.cacheTags ?? [`${tableName}s`];
|
|
84
|
+
const isDelete = action === "deleted" || action === "force-deleted";
|
|
85
|
+
const extraTags = isDelete ? module?.cacheDeleteExtraTags ?? [] : [];
|
|
86
|
+
return [...new Set([...baseTags, ...extraTags])];
|
|
87
|
+
}
|
|
88
|
+
function discoverModelTableNames() {
|
|
89
|
+
return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
93
|
+
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
94
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
95
|
+
for (const tableName of discoverModelTableNames()) {
|
|
96
|
+
for (const action of MODEL_WRITE_ACTIONS) {
|
|
97
|
+
bus.listen(modelEventName(tableName, action), async () => {
|
|
98
|
+
const tags = cacheTagsForModelWrite(tableName, action);
|
|
99
|
+
if (tags.length === 0) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let cache;
|
|
103
|
+
let queue;
|
|
104
|
+
try {
|
|
105
|
+
cache = resolveApplicationCache();
|
|
106
|
+
queue = resolveApplicationQueue();
|
|
107
|
+
} catch {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
111
|
+
await queue.dispatch(job, { tags });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export {
|
|
117
|
+
registerInvalidateCacheOnModelWriteListeners
|
|
118
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/bootstrap/metricsRoutes.ts
|
|
3
|
+
import { prometheusRegistry } from "@getstrata/core/metrics/prometheus";
|
|
4
|
+
function createMetricsRoutes() {
|
|
5
|
+
return {
|
|
6
|
+
"/metrics": async () => new Response(prometheusRegistry.renderMetrics(), {
|
|
7
|
+
status: 200,
|
|
8
|
+
headers: {
|
|
9
|
+
"content-type": "text/plain; version=0.0.4; charset=utf-8"
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
createMetricsRoutes
|
|
16
|
+
};
|