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