@getstrata/bootstrap 2.1.0 → 2.2.1
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 +32 -0
- package/README.md +1 -1
- package/dist/bootstrap/app.d.ts +7 -1
- package/dist/bootstrap/config.d.ts +1 -1
- package/dist/bootstrap/context.d.ts +8 -2
- package/dist/bootstrap/dependencies.d.ts +1 -1
- package/dist/bootstrap/discoverJobs.d.ts +11 -3
- package/dist/bootstrap/discoverListeners.d.ts +7 -3
- package/dist/bootstrap/discoverModels.d.ts +8 -0
- package/dist/bootstrap/discoverModules.d.ts +4 -1
- package/dist/bootstrap/httpKernel.d.ts +11 -2
- package/dist/bootstrap/inProcessCron.d.ts +2 -1
- package/dist/bootstrap/infrastructureDiscovery.d.ts +9 -0
- package/dist/bootstrap/modules.d.ts +1 -1
- package/dist/bootstrap/providers/tracing.d.ts +3 -0
- package/dist/bootstrap/public-api.d.ts +9 -4
- package/dist/bootstrap/web/index.d.ts +1 -1
- package/dist/bootstrap/web/server.d.ts +15 -7
- package/dist/entries/buildModuleRoutes.js +218 -71
- package/dist/entries/buildWebModuleRoutes.js +218 -71
- package/dist/entries/cache/modelCacheTags.js +157 -51
- package/dist/entries/config.js +8 -0
- package/dist/entries/context.js +427 -188
- package/dist/entries/createWebRoutes.js +220 -73
- package/dist/entries/dependencies.js +429 -190
- package/dist/entries/discoverJobs.js +132 -50
- package/dist/entries/discoverListeners.js +85 -37
- package/dist/entries/discoverModels.js +39 -0
- package/dist/entries/discoverModules.js +158 -51
- package/dist/entries/health.js +4 -0
- package/dist/entries/httpKernel.js +58 -17
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +157 -51
- package/dist/entries/providers.js +308 -164
- package/dist/entries/secretsGuard.js +2 -2
- package/dist/entries/web/routing.js +58 -17
- package/dist/entries/web/server.js +71 -14
- package/dist/index.js +600 -222
- package/package.json +8 -3
|
@@ -53,8 +53,12 @@ import {
|
|
|
53
53
|
CORE_CACHE_TOKEN,
|
|
54
54
|
CORE_CONFIG_TOKEN,
|
|
55
55
|
CORE_EVENT_BUS_TOKEN,
|
|
56
|
+
CORE_HTTP_CLEANUP_TOKEN,
|
|
56
57
|
CORE_POLICY_GATE_TOKEN,
|
|
58
|
+
CORE_PUBLIC_TENANCY_TOKEN,
|
|
57
59
|
CORE_QUEUE_TOKEN,
|
|
60
|
+
CORE_TENANT_RESOLVER_TOKEN,
|
|
61
|
+
CORE_THROTTLE_QUOTA_POLICY_TOKEN,
|
|
58
62
|
CORE_TOKEN_SERVICE_TOKEN
|
|
59
63
|
} from "@getstrata/core/contracts/serviceTokens";
|
|
60
64
|
var APP_PORT_CONFIG_KEY = "app.port";
|
|
@@ -120,8 +124,39 @@ function resolveRegisterRateLimit() {
|
|
|
120
124
|
// ../../src/bootstrap/httpKernel.ts
|
|
121
125
|
class HttpKernel {
|
|
122
126
|
dependencies;
|
|
127
|
+
ownedThrottles = new Set;
|
|
128
|
+
disposed = false;
|
|
123
129
|
constructor(dependencies) {
|
|
124
130
|
this.dependencies = dependencies;
|
|
131
|
+
if (dependencies.container.has(CORE_HTTP_CLEANUP_TOKEN)) {
|
|
132
|
+
dependencies.container.resolve(CORE_HTTP_CLEANUP_TOKEN)(() => this.dispose());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
dispose() {
|
|
136
|
+
this.disposed = true;
|
|
137
|
+
const owned = [...this.ownedThrottles];
|
|
138
|
+
this.ownedThrottles.clear();
|
|
139
|
+
const errors = [];
|
|
140
|
+
for (const throttle of owned.reverse()) {
|
|
141
|
+
try {
|
|
142
|
+
throttle.dispose();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
errors.push(error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (errors.length)
|
|
148
|
+
throw new AggregateError(errors, "HTTP throttle cleanup failed.");
|
|
149
|
+
}
|
|
150
|
+
ownThrottle(throttle) {
|
|
151
|
+
if (this.disposed) {
|
|
152
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function")
|
|
153
|
+
throttle.dispose();
|
|
154
|
+
throw new Error("HTTP kernel is disposed.");
|
|
155
|
+
}
|
|
156
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function") {
|
|
157
|
+
this.ownedThrottles.add(throttle);
|
|
158
|
+
}
|
|
159
|
+
return throttle;
|
|
125
160
|
}
|
|
126
161
|
globalMiddleware(errorFormat = "json") {
|
|
127
162
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
@@ -136,7 +171,10 @@ class HttpKernel {
|
|
|
136
171
|
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
137
172
|
createAuthMiddleware(auth),
|
|
138
173
|
createMembershipMiddleware(),
|
|
139
|
-
createTenantMiddleware(
|
|
174
|
+
createTenantMiddleware({
|
|
175
|
+
publicTenancy: this.dependencies.container.has(CORE_PUBLIC_TENANCY_TOKEN) ? this.dependencies.container.resolve(CORE_PUBLIC_TENANCY_TOKEN) : undefined,
|
|
176
|
+
resolveTenant: this.dependencies.container.has(CORE_TENANT_RESOLVER_TOKEN) ? this.dependencies.container.resolve(CORE_TENANT_RESOLVER_TOKEN) : undefined
|
|
177
|
+
})
|
|
140
178
|
];
|
|
141
179
|
}
|
|
142
180
|
group(name) {
|
|
@@ -151,25 +189,28 @@ class HttpKernel {
|
|
|
151
189
|
if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
|
|
152
190
|
return [csrf];
|
|
153
191
|
}
|
|
192
|
+
const quotaPolicy = this.dependencies.container.has(CORE_THROTTLE_QUOTA_POLICY_TOKEN) ? this.dependencies.container.resolve(CORE_THROTTLE_QUOTA_POLICY_TOKEN) : undefined;
|
|
154
193
|
const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
|
|
155
194
|
const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
|
|
156
195
|
if (!redisUrl) {
|
|
157
196
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
158
197
|
return [
|
|
159
|
-
createMemoryThrottleMiddleware({
|
|
198
|
+
this.ownThrottle(createMemoryThrottleMiddleware({
|
|
160
199
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
161
|
-
decaySeconds: 60
|
|
162
|
-
|
|
200
|
+
decaySeconds: 60,
|
|
201
|
+
quotaPolicy
|
|
202
|
+
})),
|
|
163
203
|
csrf
|
|
164
204
|
];
|
|
165
205
|
}
|
|
166
206
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
167
207
|
return [
|
|
168
|
-
createThrottleMiddleware({
|
|
208
|
+
this.ownThrottle(createThrottleMiddleware({
|
|
169
209
|
redisUrl,
|
|
170
210
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
171
|
-
decaySeconds: 60
|
|
172
|
-
|
|
211
|
+
decaySeconds: 60,
|
|
212
|
+
quotaPolicy
|
|
213
|
+
})),
|
|
173
214
|
csrf
|
|
174
215
|
];
|
|
175
216
|
}
|
|
@@ -206,8 +247,8 @@ class HttpKernel {
|
|
|
206
247
|
return handler(request);
|
|
207
248
|
});
|
|
208
249
|
}
|
|
209
|
-
wrapWebPublicRead(handler) {
|
|
210
|
-
if (isPublicReadsEnabled()) {
|
|
250
|
+
wrapWebPublicRead(handler, options = {}) {
|
|
251
|
+
if (options.allowAnonymous ?? isPublicReadsEnabled()) {
|
|
211
252
|
return this.wrapWeb(handler);
|
|
212
253
|
}
|
|
213
254
|
return this.wrapWebAuthenticated(handler);
|
|
@@ -253,8 +294,8 @@ class HttpKernel {
|
|
|
253
294
|
const middleware = [...this.group("authenticated"), ...this.verifiedMiddleware()];
|
|
254
295
|
return withMiddleware(...middleware)(handler);
|
|
255
296
|
}
|
|
256
|
-
wrapPublicRead(handler) {
|
|
257
|
-
if (isPublicReadsEnabled()) {
|
|
297
|
+
wrapPublicRead(handler, options = {}) {
|
|
298
|
+
if (options.allowAnonymous ?? isPublicReadsEnabled()) {
|
|
258
299
|
return handler;
|
|
259
300
|
}
|
|
260
301
|
return this.wrapAuthenticated(handler);
|
|
@@ -299,25 +340,25 @@ class HttpKernel {
|
|
|
299
340
|
const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
|
|
300
341
|
const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
|
|
301
342
|
if (scope === "login") {
|
|
302
|
-
return withMiddleware(createLoginThrottleMiddleware({
|
|
343
|
+
return withMiddleware(this.ownThrottle(createLoginThrottleMiddleware({
|
|
303
344
|
...redisUrl ? { redisUrl } : {},
|
|
304
345
|
maxAttempts: rateLimit.maxAttempts,
|
|
305
346
|
decaySeconds: rateLimit.decaySeconds
|
|
306
|
-
}))(handler);
|
|
347
|
+
})))(handler);
|
|
307
348
|
}
|
|
308
349
|
if (redisUrl) {
|
|
309
|
-
return withMiddleware(createThrottleMiddleware({
|
|
350
|
+
return withMiddleware(this.ownThrottle(createThrottleMiddleware({
|
|
310
351
|
redisUrl,
|
|
311
352
|
maxAttempts: rateLimit.maxAttempts,
|
|
312
353
|
decaySeconds: rateLimit.decaySeconds,
|
|
313
354
|
keyPrefix: memoryKeyPrefix
|
|
314
|
-
}))(handler);
|
|
355
|
+
})))(handler);
|
|
315
356
|
}
|
|
316
|
-
return withMiddleware(createMemoryThrottleMiddleware({
|
|
357
|
+
return withMiddleware(this.ownThrottle(createMemoryThrottleMiddleware({
|
|
317
358
|
maxAttempts: rateLimit.maxAttempts,
|
|
318
359
|
decaySeconds: rateLimit.decaySeconds,
|
|
319
360
|
keyPrefix: memoryKeyPrefix
|
|
320
|
-
}))(handler);
|
|
361
|
+
})))(handler);
|
|
321
362
|
}
|
|
322
363
|
}
|
|
323
364
|
function createHttpKernel(dependencies) {
|
|
@@ -325,74 +366,180 @@ function createHttpKernel(dependencies) {
|
|
|
325
366
|
}
|
|
326
367
|
|
|
327
368
|
// ../../src/bootstrap/discoverModules.ts
|
|
328
|
-
import {
|
|
329
|
-
import { join } from "path";
|
|
369
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
370
|
+
import { join as join2, resolve } from "path";
|
|
371
|
+
|
|
372
|
+
// ../../src/bootstrap/infrastructureDiscovery.ts
|
|
373
|
+
import { readdir } from "fs/promises";
|
|
374
|
+
import { join, relative } from "path";
|
|
330
375
|
import { pathToFileURL } from "url";
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
376
|
+
async function infrastructureFiles(directory, exclude = []) {
|
|
377
|
+
const excluded = new Set([
|
|
378
|
+
"index.ts",
|
|
379
|
+
"index.js",
|
|
380
|
+
"index.mts",
|
|
381
|
+
"index.mjs",
|
|
382
|
+
"register.ts",
|
|
383
|
+
"register.js",
|
|
384
|
+
"register.mts",
|
|
385
|
+
"register.mjs",
|
|
386
|
+
...exclude
|
|
387
|
+
]);
|
|
388
|
+
const files = [];
|
|
389
|
+
async function visit(path) {
|
|
390
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
391
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
392
|
+
for (const entry of entries) {
|
|
393
|
+
const file = join(path, entry.name);
|
|
394
|
+
const key = relative(directory, file).replaceAll("\\", "/");
|
|
395
|
+
if (excluded.has(entry.name) || excluded.has(key))
|
|
396
|
+
continue;
|
|
397
|
+
if (entry.isDirectory())
|
|
398
|
+
await visit(file);
|
|
399
|
+
else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
|
|
400
|
+
files.push(file);
|
|
401
|
+
}
|
|
353
402
|
}
|
|
354
|
-
|
|
355
|
-
|
|
403
|
+
try {
|
|
404
|
+
await visit(directory);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (error.code !== "ENOENT")
|
|
407
|
+
throw error;
|
|
408
|
+
if (error.path !== directory)
|
|
409
|
+
throw error;
|
|
356
410
|
}
|
|
357
|
-
|
|
411
|
+
return files;
|
|
358
412
|
}
|
|
359
|
-
async function
|
|
360
|
-
const modulesDirectory = resolveModulesDirectory(options);
|
|
361
|
-
let moduleNames;
|
|
413
|
+
async function importInfrastructure(file) {
|
|
362
414
|
try {
|
|
363
|
-
|
|
415
|
+
return await import(pathToFileURL(file).href);
|
|
364
416
|
} catch (error) {
|
|
365
|
-
|
|
366
|
-
|
|
417
|
+
throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ../../src/bootstrap/discoverModules.ts
|
|
422
|
+
var KEY = Symbol.for("@getstrata/discoverModulesState");
|
|
423
|
+
function state() {
|
|
424
|
+
const globals = globalThis;
|
|
425
|
+
globals[KEY] ??= { appModules: [] };
|
|
426
|
+
return globals[KEY];
|
|
427
|
+
}
|
|
428
|
+
function configureModulesDirectory(modulesDir) {
|
|
429
|
+
const current = state();
|
|
430
|
+
const directory = resolve(modulesDir);
|
|
431
|
+
if (current.configuredModulesDir !== directory || current.configuredManifest) {
|
|
432
|
+
current.appModules.length = 0;
|
|
433
|
+
current.modulesReady = undefined;
|
|
434
|
+
current.source = undefined;
|
|
435
|
+
}
|
|
436
|
+
current.configuredManifest = undefined;
|
|
437
|
+
current.configuredModulesDir = directory;
|
|
438
|
+
}
|
|
439
|
+
function configureModulesManifest(manifest) {
|
|
440
|
+
const current = state();
|
|
441
|
+
if (current.configuredManifest !== manifest) {
|
|
442
|
+
current.appModules.length = 0;
|
|
443
|
+
current.modulesReady = undefined;
|
|
444
|
+
current.source = undefined;
|
|
445
|
+
}
|
|
446
|
+
current.configuredManifest = manifest;
|
|
447
|
+
}
|
|
448
|
+
function validateModule(value, location) {
|
|
449
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
450
|
+
throw new TypeError(`Invalid application module at ${location}.`);
|
|
451
|
+
const module = value;
|
|
452
|
+
if (typeof module.name !== "string" || !module.name.trim())
|
|
453
|
+
throw new TypeError(`Invalid module name at ${location}.`);
|
|
454
|
+
if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
|
|
455
|
+
throw new TypeError(`Invalid module order at ${location}.`);
|
|
456
|
+
for (const hook of ["routes", "webRoutes"])
|
|
457
|
+
if (module[hook] !== undefined && typeof module[hook] !== "function")
|
|
458
|
+
throw new TypeError(`Invalid ${hook} at ${location}.`);
|
|
459
|
+
if (module.providers !== undefined) {
|
|
460
|
+
if (!Array.isArray(module.providers))
|
|
461
|
+
throw new TypeError(`Invalid providers at ${location}.`);
|
|
462
|
+
for (const provider of module.providers) {
|
|
463
|
+
if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
|
|
464
|
+
throw new TypeError(`Invalid provider at ${location}.`);
|
|
367
465
|
}
|
|
368
|
-
throw error;
|
|
369
466
|
}
|
|
370
|
-
const modules = await Promise.all(moduleNames.map(async (moduleName) => {
|
|
371
|
-
const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
|
|
372
|
-
const loaded = await import(moduleUrl);
|
|
373
|
-
return loaded.default;
|
|
374
|
-
}));
|
|
375
|
-
return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
376
467
|
}
|
|
377
|
-
async function
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
468
|
+
async function load(options) {
|
|
469
|
+
const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
|
|
470
|
+
const entries = [];
|
|
471
|
+
if (manifest)
|
|
472
|
+
for (const [index, value] of manifest.entries())
|
|
473
|
+
entries.push({ value, location: `module manifest[${index}]` });
|
|
474
|
+
else {
|
|
475
|
+
const directory = options.modulesDir ?? state().configuredModulesDir;
|
|
476
|
+
if (!directory)
|
|
477
|
+
throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
|
|
478
|
+
let folders;
|
|
479
|
+
try {
|
|
480
|
+
folders = await readdir2(directory, { withFileTypes: true });
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error.code === "ENOENT")
|
|
483
|
+
return [];
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
const excluded = new Set(options.exclude ?? []);
|
|
487
|
+
for (const folder of folders.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
488
|
+
const path = join2(directory, folder.name);
|
|
489
|
+
const files = await readdir2(path);
|
|
490
|
+
const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
|
|
491
|
+
if (candidates.length !== 1)
|
|
492
|
+
throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
|
|
493
|
+
const file = join2(path, candidates[0]);
|
|
494
|
+
const exports = await importInfrastructure(file);
|
|
495
|
+
entries.push({ value: exports.default, location: file });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const names = new Map;
|
|
499
|
+
const modules = [];
|
|
500
|
+
for (const { value, location } of entries) {
|
|
501
|
+
validateModule(value, location);
|
|
502
|
+
const previous = names.get(value.name);
|
|
503
|
+
if (previous)
|
|
504
|
+
throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
|
|
505
|
+
names.set(value.name, location);
|
|
506
|
+
modules.push(value);
|
|
507
|
+
}
|
|
508
|
+
return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
509
|
+
}
|
|
510
|
+
function ensureModulesLoaded(options = {}) {
|
|
511
|
+
const current = state();
|
|
512
|
+
const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
|
|
513
|
+
options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
|
|
514
|
+
[...options.exclude ?? []].sort()
|
|
515
|
+
]);
|
|
516
|
+
if (current.modulesReady && current.source === key)
|
|
517
|
+
return current.modulesReady;
|
|
518
|
+
current.appModules.length = 0;
|
|
519
|
+
current.source = key;
|
|
520
|
+
const pending = load(options).then((modules) => {
|
|
521
|
+
if (current.modulesReady !== pending)
|
|
522
|
+
return modules;
|
|
523
|
+
current.appModules.splice(0, current.appModules.length, ...modules);
|
|
524
|
+
return current.appModules;
|
|
525
|
+
}).catch((error) => {
|
|
526
|
+
if (current.modulesReady === pending)
|
|
527
|
+
current.modulesReady = undefined;
|
|
528
|
+
throw error;
|
|
385
529
|
});
|
|
386
|
-
|
|
530
|
+
current.modulesReady = pending;
|
|
531
|
+
return pending;
|
|
387
532
|
}
|
|
388
533
|
function discoverModules() {
|
|
389
|
-
return
|
|
534
|
+
return state().appModules;
|
|
390
535
|
}
|
|
391
536
|
function resetDiscoverModulesForTests() {
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
537
|
+
const current = state();
|
|
538
|
+
current.configuredModulesDir = undefined;
|
|
539
|
+
current.configuredManifest = undefined;
|
|
540
|
+
current.appModules.length = 0;
|
|
541
|
+
current.modulesReady = undefined;
|
|
542
|
+
current.source = undefined;
|
|
396
543
|
}
|
|
397
544
|
// ../../src/bootstrap/prefixRouteMap.ts
|
|
398
545
|
function prefixRouteMap(prefix, routes) {
|
|
@@ -1,73 +1,179 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/bootstrap/discoverModules.ts
|
|
3
|
-
import {
|
|
4
|
-
import { join } from "path";
|
|
3
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
4
|
+
import { join as join2, resolve } from "path";
|
|
5
|
+
|
|
6
|
+
// ../../src/bootstrap/infrastructureDiscovery.ts
|
|
7
|
+
import { readdir } from "fs/promises";
|
|
8
|
+
import { join, relative } from "path";
|
|
5
9
|
import { pathToFileURL } from "url";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
async function infrastructureFiles(directory, exclude = []) {
|
|
11
|
+
const excluded = new Set([
|
|
12
|
+
"index.ts",
|
|
13
|
+
"index.js",
|
|
14
|
+
"index.mts",
|
|
15
|
+
"index.mjs",
|
|
16
|
+
"register.ts",
|
|
17
|
+
"register.js",
|
|
18
|
+
"register.mts",
|
|
19
|
+
"register.mjs",
|
|
20
|
+
...exclude
|
|
21
|
+
]);
|
|
22
|
+
const files = [];
|
|
23
|
+
async function visit(path) {
|
|
24
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
25
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const file = join(path, entry.name);
|
|
28
|
+
const key = relative(directory, file).replaceAll("\\", "/");
|
|
29
|
+
if (excluded.has(entry.name) || excluded.has(key))
|
|
30
|
+
continue;
|
|
31
|
+
if (entry.isDirectory())
|
|
32
|
+
await visit(file);
|
|
33
|
+
else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
|
|
34
|
+
files.push(file);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await visit(directory);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error.code !== "ENOENT")
|
|
41
|
+
throw error;
|
|
42
|
+
if (error.path !== directory)
|
|
43
|
+
throw error;
|
|
11
44
|
}
|
|
12
|
-
|
|
13
|
-
globalThis[DISCOVER_MODULES_STATE_KEY] = state;
|
|
14
|
-
return state;
|
|
45
|
+
return files;
|
|
15
46
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
47
|
+
async function importInfrastructure(file) {
|
|
48
|
+
try {
|
|
49
|
+
return await import(pathToFileURL(file).href);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
|
|
21
52
|
}
|
|
22
|
-
state.configuredModulesDir = modulesDir;
|
|
23
53
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
|
|
55
|
+
// ../../src/bootstrap/discoverModules.ts
|
|
56
|
+
var KEY = Symbol.for("@getstrata/discoverModulesState");
|
|
57
|
+
function state() {
|
|
58
|
+
const globals = globalThis;
|
|
59
|
+
globals[KEY] ??= { appModules: [] };
|
|
60
|
+
return globals[KEY];
|
|
61
|
+
}
|
|
62
|
+
function configureModulesDirectory(modulesDir) {
|
|
63
|
+
const current = state();
|
|
64
|
+
const directory = resolve(modulesDir);
|
|
65
|
+
if (current.configuredModulesDir !== directory || current.configuredManifest) {
|
|
66
|
+
current.appModules.length = 0;
|
|
67
|
+
current.modulesReady = undefined;
|
|
68
|
+
current.source = undefined;
|
|
28
69
|
}
|
|
29
|
-
|
|
30
|
-
|
|
70
|
+
current.configuredManifest = undefined;
|
|
71
|
+
current.configuredModulesDir = directory;
|
|
72
|
+
}
|
|
73
|
+
function configureModulesManifest(manifest) {
|
|
74
|
+
const current = state();
|
|
75
|
+
if (current.configuredManifest !== manifest) {
|
|
76
|
+
current.appModules.length = 0;
|
|
77
|
+
current.modulesReady = undefined;
|
|
78
|
+
current.source = undefined;
|
|
31
79
|
}
|
|
32
|
-
|
|
80
|
+
current.configuredManifest = manifest;
|
|
33
81
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
82
|
+
function validateModule(value, location) {
|
|
83
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
84
|
+
throw new TypeError(`Invalid application module at ${location}.`);
|
|
85
|
+
const module = value;
|
|
86
|
+
if (typeof module.name !== "string" || !module.name.trim())
|
|
87
|
+
throw new TypeError(`Invalid module name at ${location}.`);
|
|
88
|
+
if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
|
|
89
|
+
throw new TypeError(`Invalid module order at ${location}.`);
|
|
90
|
+
for (const hook of ["routes", "webRoutes"])
|
|
91
|
+
if (module[hook] !== undefined && typeof module[hook] !== "function")
|
|
92
|
+
throw new TypeError(`Invalid ${hook} at ${location}.`);
|
|
93
|
+
if (module.providers !== undefined) {
|
|
94
|
+
if (!Array.isArray(module.providers))
|
|
95
|
+
throw new TypeError(`Invalid providers at ${location}.`);
|
|
96
|
+
for (const provider of module.providers) {
|
|
97
|
+
if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
|
|
98
|
+
throw new TypeError(`Invalid provider at ${location}.`);
|
|
42
99
|
}
|
|
43
|
-
throw error;
|
|
44
100
|
}
|
|
45
|
-
const modules = await Promise.all(moduleNames.map(async (moduleName) => {
|
|
46
|
-
const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
|
|
47
|
-
const loaded = await import(moduleUrl);
|
|
48
|
-
return loaded.default;
|
|
49
|
-
}));
|
|
50
|
-
return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
51
101
|
}
|
|
52
|
-
async function
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
102
|
+
async function load(options) {
|
|
103
|
+
const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
|
|
104
|
+
const entries = [];
|
|
105
|
+
if (manifest)
|
|
106
|
+
for (const [index, value] of manifest.entries())
|
|
107
|
+
entries.push({ value, location: `module manifest[${index}]` });
|
|
108
|
+
else {
|
|
109
|
+
const directory = options.modulesDir ?? state().configuredModulesDir;
|
|
110
|
+
if (!directory)
|
|
111
|
+
throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
|
|
112
|
+
let folders;
|
|
113
|
+
try {
|
|
114
|
+
folders = await readdir2(directory, { withFileTypes: true });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.code === "ENOENT")
|
|
117
|
+
return [];
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
const excluded = new Set(options.exclude ?? []);
|
|
121
|
+
for (const folder of folders.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
122
|
+
const path = join2(directory, folder.name);
|
|
123
|
+
const files = await readdir2(path);
|
|
124
|
+
const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
|
|
125
|
+
if (candidates.length !== 1)
|
|
126
|
+
throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
|
|
127
|
+
const file = join2(path, candidates[0]);
|
|
128
|
+
const exports = await importInfrastructure(file);
|
|
129
|
+
entries.push({ value: exports.default, location: file });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const names = new Map;
|
|
133
|
+
const modules = [];
|
|
134
|
+
for (const { value, location } of entries) {
|
|
135
|
+
validateModule(value, location);
|
|
136
|
+
const previous = names.get(value.name);
|
|
137
|
+
if (previous)
|
|
138
|
+
throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
|
|
139
|
+
names.set(value.name, location);
|
|
140
|
+
modules.push(value);
|
|
56
141
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
142
|
+
return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
143
|
+
}
|
|
144
|
+
function ensureModulesLoaded(options = {}) {
|
|
145
|
+
const current = state();
|
|
146
|
+
const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
|
|
147
|
+
options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
|
|
148
|
+
[...options.exclude ?? []].sort()
|
|
149
|
+
]);
|
|
150
|
+
if (current.modulesReady && current.source === key)
|
|
151
|
+
return current.modulesReady;
|
|
152
|
+
current.appModules.length = 0;
|
|
153
|
+
current.source = key;
|
|
154
|
+
const pending = load(options).then((modules) => {
|
|
155
|
+
if (current.modulesReady !== pending)
|
|
156
|
+
return modules;
|
|
157
|
+
current.appModules.splice(0, current.appModules.length, ...modules);
|
|
158
|
+
return current.appModules;
|
|
159
|
+
}).catch((error) => {
|
|
160
|
+
if (current.modulesReady === pending)
|
|
161
|
+
current.modulesReady = undefined;
|
|
162
|
+
throw error;
|
|
60
163
|
});
|
|
61
|
-
|
|
164
|
+
current.modulesReady = pending;
|
|
165
|
+
return pending;
|
|
62
166
|
}
|
|
63
167
|
function discoverModules() {
|
|
64
|
-
return
|
|
168
|
+
return state().appModules;
|
|
65
169
|
}
|
|
66
170
|
function resetDiscoverModulesForTests() {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
171
|
+
const current = state();
|
|
172
|
+
current.configuredModulesDir = undefined;
|
|
173
|
+
current.configuredManifest = undefined;
|
|
174
|
+
current.appModules.length = 0;
|
|
175
|
+
current.modulesReady = undefined;
|
|
176
|
+
current.source = undefined;
|
|
71
177
|
}
|
|
72
178
|
|
|
73
179
|
// ../../src/bootstrap/cache/modelCacheTags.ts
|
package/dist/entries/config.js
CHANGED
|
@@ -7,8 +7,12 @@ import {
|
|
|
7
7
|
CORE_CACHE_TOKEN,
|
|
8
8
|
CORE_CONFIG_TOKEN,
|
|
9
9
|
CORE_EVENT_BUS_TOKEN,
|
|
10
|
+
CORE_HTTP_CLEANUP_TOKEN,
|
|
10
11
|
CORE_POLICY_GATE_TOKEN,
|
|
12
|
+
CORE_PUBLIC_TENANCY_TOKEN,
|
|
11
13
|
CORE_QUEUE_TOKEN,
|
|
14
|
+
CORE_TENANT_RESOLVER_TOKEN,
|
|
15
|
+
CORE_THROTTLE_QUOTA_POLICY_TOKEN,
|
|
12
16
|
CORE_TOKEN_SERVICE_TOKEN
|
|
13
17
|
} from "@getstrata/core/contracts/serviceTokens";
|
|
14
18
|
var APP_PORT_CONFIG_KEY = "app.port";
|
|
@@ -36,8 +40,12 @@ export {
|
|
|
36
40
|
CORE_CACHE_TOKEN,
|
|
37
41
|
CORE_CONFIG_TOKEN,
|
|
38
42
|
CORE_EVENT_BUS_TOKEN,
|
|
43
|
+
CORE_HTTP_CLEANUP_TOKEN,
|
|
39
44
|
CORE_POLICY_GATE_TOKEN,
|
|
45
|
+
CORE_PUBLIC_TENANCY_TOKEN,
|
|
40
46
|
CORE_QUEUE_TOKEN,
|
|
47
|
+
CORE_TENANT_RESOLVER_TOKEN,
|
|
48
|
+
CORE_THROTTLE_QUOTA_POLICY_TOKEN,
|
|
41
49
|
CORE_TOKEN_SERVICE_TOKEN,
|
|
42
50
|
DATABASE_URL_CONFIG_KEY,
|
|
43
51
|
DEFAULT_API_TOKEN,
|