@zerotal/core 1.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 +79 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/package.json +72 -0
- package/src/application/Application.ts +1671 -0
- package/src/application/BootDoctor.ts +108 -0
- package/src/application/DevErrorPage.ts +567 -0
- package/src/application/ExceptionHandler.ts +183 -0
- package/src/application/currentApp.ts +73 -0
- package/src/assets/assets.ts +79 -0
- package/src/assets/index.ts +16 -0
- package/src/auth/AuthenticatedUser.ts +18 -0
- package/src/build/PackageLinter.ts +146 -0
- package/src/build/PackageScaffold.ts +127 -0
- package/src/build/codemod.ts +64 -0
- package/src/build/index.ts +12 -0
- package/src/command/Command.ts +254 -0
- package/src/command/CommandRunner.ts +593 -0
- package/src/command/OutputWriter.ts +61 -0
- package/src/command/builtin/CompileCommand.ts +46 -0
- package/src/command/builtin/CssBuildCommand.ts +71 -0
- package/src/command/builtin/KeyGenerateCommand.ts +58 -0
- package/src/command/builtin/LintPackagesCommand.ts +72 -0
- package/src/command/builtin/MakeCommandCommand.ts +85 -0
- package/src/command/builtin/MakeControllerCommand.ts +95 -0
- package/src/command/builtin/MakeEventCommand.ts +85 -0
- package/src/command/builtin/MakeJobCommand.ts +53 -0
- package/src/command/builtin/MakeListenerCommand.ts +35 -0
- package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
- package/src/command/builtin/MakeNotificationCommand.ts +48 -0
- package/src/command/builtin/MakeObserverCommand.ts +78 -0
- package/src/command/builtin/MakePackageCommand.ts +45 -0
- package/src/command/builtin/MakePolicyCommand.ts +66 -0
- package/src/command/builtin/MakeProviderCommand.ts +75 -0
- package/src/command/builtin/MakeRequestCommand.ts +47 -0
- package/src/command/builtin/MakeResourceCommand.ts +61 -0
- package/src/command/builtin/MakeTestCommand.ts +120 -0
- package/src/command/builtin/ReloadCommand.ts +52 -0
- package/src/command/builtin/ReplCommand.ts +174 -0
- package/src/command/builtin/RouteListCommand.ts +188 -0
- package/src/command/builtin/ServeCommand.ts +321 -0
- package/src/command/builtin/StartCommand.ts +3 -0
- package/src/command/builtin/StatusCommand.ts +71 -0
- package/src/command/builtin/TestCommand.ts +172 -0
- package/src/command/builtin/WorkerCommand.ts +27 -0
- package/src/command/builtin/index.ts +53 -0
- package/src/command/scaffold/worker.ts.txt +12 -0
- package/src/command/scaffold/zerotal.ts.txt +26 -0
- package/src/command/startZerotal.ts +55 -0
- package/src/config/AppConfig.ts +253 -0
- package/src/config/ConfigLoader.ts +117 -0
- package/src/config/ConfigManager.ts +169 -0
- package/src/config/index.ts +46 -0
- package/src/config/registry.ts +59 -0
- package/src/config/validation.ts +117 -0
- package/src/container/Container.ts +606 -0
- package/src/container/ContextualBindingBuilder.ts +57 -0
- package/src/container/ScopedResolver.ts +117 -0
- package/src/container/index.ts +32 -0
- package/src/container/inject.ts +55 -0
- package/src/container/types.ts +71 -0
- package/src/context/RequestContext.ts +91 -0
- package/src/contracts/auth.ts +24 -0
- package/src/contracts/index.ts +23 -0
- package/src/contracts/session.ts +70 -0
- package/src/contracts/transaction.ts +26 -0
- package/src/conventions/ConventionLoader.ts +128 -0
- package/src/conventions/builtinConcerns.ts +131 -0
- package/src/crypt/Crypt.ts +141 -0
- package/src/crypt/URLSigner.ts +96 -0
- package/src/datetime/Carbon.ts +1396 -0
- package/src/datetime/CarbonInterval.ts +421 -0
- package/src/datetime/clock.ts +28 -0
- package/src/datetime/index.ts +23 -0
- package/src/datetime/temporal-shim.ts +1 -0
- package/src/dev/BuildOutput.ts +131 -0
- package/src/dev/CssPlugins.ts +184 -0
- package/src/dev/DevBuildHook.ts +74 -0
- package/src/dev/DevOrchestrator.ts +213 -0
- package/src/dev/DevReloadMiddleware.ts +101 -0
- package/src/dev/DevReloadServer.ts +85 -0
- package/src/dev/DevWsServer.ts +45 -0
- package/src/dev/index.ts +19 -0
- package/src/dev/reloadClient.ts +39 -0
- package/src/env/Def.ts +232 -0
- package/src/env/EnvSchema.ts +105 -0
- package/src/env/index.ts +34 -0
- package/src/env/t.ts +128 -0
- package/src/errors/ConfigError.ts +12 -0
- package/src/errors/ContainerErrors.ts +143 -0
- package/src/errors/HttpError.ts +127 -0
- package/src/errors/ValidationError.ts +19 -0
- package/src/errors/ZerotalError.ts +25 -0
- package/src/errors/index.ts +46 -0
- package/src/events/CallQueuedListener.ts +66 -0
- package/src/events/Emitter.ts +280 -0
- package/src/events/EventFake.ts +160 -0
- package/src/events/FrameworkEvents.ts +252 -0
- package/src/facade/Facade.ts +101 -0
- package/src/facade/facades/App.ts +155 -0
- package/src/facade/facades/Artisan.ts +63 -0
- package/src/facade/facades/Config.ts +21 -0
- package/src/facade/facades/Events.ts +19 -0
- package/src/facade/facades/index.ts +28 -0
- package/src/global.d.ts +9 -0
- package/src/hash/Hash.ts +60 -0
- package/src/health/Health.ts +221 -0
- package/src/health/index.ts +27 -0
- package/src/helpers/Collection.ts +435 -0
- package/src/helpers/config.ts +59 -0
- package/src/helpers/fluent.ts +52 -0
- package/src/helpers/html.ts +11 -0
- package/src/helpers/index.ts +266 -0
- package/src/helpers/make.ts +35 -0
- package/src/helpers/markdown.ts +73 -0
- package/src/helpers/pageElements.ts +27 -0
- package/src/helpers/request.ts +62 -0
- package/src/helpers/response.ts +411 -0
- package/src/helpers/str.ts +208 -0
- package/src/http/Http.ts +298 -0
- package/src/http/HttpClient.ts +289 -0
- package/src/http/Resource.ts +171 -0
- package/src/http/UploadedFile.ts +204 -0
- package/src/http/Uri.ts +490 -0
- package/src/http/index.ts +46 -0
- package/src/http/negotiate.ts +213 -0
- package/src/http/originGuard.ts +76 -0
- package/src/http/sniffContentType.ts +105 -0
- package/src/http/url.ts +204 -0
- package/src/http/withHeaders.ts +24 -0
- package/src/index.ts +250 -0
- package/src/lock/LockManager.ts +228 -0
- package/src/lock/config.ts +49 -0
- package/src/lock/drivers/LockDriver.ts +32 -0
- package/src/lock/drivers/MemoryLockDriver.ts +52 -0
- package/src/lock/drivers/RedisLockDriver.ts +58 -0
- package/src/lock/drivers/SqliteLockDriver.ts +85 -0
- package/src/lock/errors.ts +20 -0
- package/src/lock/facades/Lock.ts +114 -0
- package/src/lock/index.ts +53 -0
- package/src/logger/Log.ts +35 -0
- package/src/logger/LogManager.ts +430 -0
- package/src/logger/LoggerMiddleware.ts +125 -0
- package/src/logger/channels/ConsoleChannel.ts +139 -0
- package/src/logger/channels/DailyChannel.ts +74 -0
- package/src/logger/channels/NullChannel.ts +17 -0
- package/src/logger/channels/SingleChannel.ts +34 -0
- package/src/logger/channels/StackChannel.ts +29 -0
- package/src/logger/config.ts +90 -0
- package/src/logger/format.ts +96 -0
- package/src/logger/frameworkLog.ts +93 -0
- package/src/logger/index.ts +68 -0
- package/src/logger/renderTable.ts +111 -0
- package/src/logger/types.ts +212 -0
- package/src/macros/config.macro.ts +50 -0
- package/src/metrics/HttpMetrics.ts +114 -0
- package/src/metrics/index.ts +18 -0
- package/src/middleware/BaseMiddleware.ts +72 -0
- package/src/middleware/CorsMiddleware.ts +152 -0
- package/src/middleware/RateLimiter.ts +255 -0
- package/src/middleware/SecureHeadersMiddleware.ts +127 -0
- package/src/middleware/ThrottleMiddleware.ts +252 -0
- package/src/middleware/WebhookMiddleware.ts +204 -0
- package/src/pipeline/ContextRegistry.ts +42 -0
- package/src/pipeline/HttpContext.ts +865 -0
- package/src/pipeline/Pipeline.ts +150 -0
- package/src/pipeline/currentPage.ts +46 -0
- package/src/pipeline/types.ts +80 -0
- package/src/provider/LockProvider.ts +64 -0
- package/src/provider/LogProvider.ts +137 -0
- package/src/provider/ServiceProvider.ts +84 -0
- package/src/provider/StorageProvider.ts +45 -0
- package/src/router/FileRouter.ts +526 -0
- package/src/router/Route.ts +76 -0
- package/src/router/RouteHandler.ts +335 -0
- package/src/router/Router.ts +1247 -0
- package/src/router/domain.ts +65 -0
- package/src/security/index.ts +22 -0
- package/src/storage/FakeDisk.ts +233 -0
- package/src/storage/StorageFilesMiddleware.ts +150 -0
- package/src/storage/StorageManager.ts +173 -0
- package/src/storage/config.ts +47 -0
- package/src/storage/drivers/LocalDriver.ts +138 -0
- package/src/storage/drivers/S3Driver.ts +169 -0
- package/src/storage/errors.ts +135 -0
- package/src/storage/facades/Storage.ts +3 -0
- package/src/storage/global.d.ts +7 -0
- package/src/storage/index.ts +22 -0
- package/src/storage/root.ts +59 -0
- package/src/storage/types.ts +104 -0
- package/src/support/appKey.ts +38 -0
- package/src/support/cookie.ts +72 -0
- package/src/support/crypto.ts +52 -0
- package/src/support/deepMerge.ts +117 -0
- package/src/support/env.ts +71 -0
- package/src/support/network.ts +79 -0
- package/src/support/port.ts +197 -0
- package/src/support/str.ts +122 -0
- package/src/view/FileRouteResolver.ts +59 -0
- package/src/view/index.ts +144 -0
- package/src/view/jsx-runtime.ts +233 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request-scoped resolver: caches scoped bindings for the lifetime of a
|
|
3
|
+
* single request and reference-counts them so the cache is flushed only once
|
|
4
|
+
* every outstanding reference (including pending afterResponse hooks) is gone.
|
|
5
|
+
*/
|
|
6
|
+
import type { Factory } from "./types.ts";
|
|
7
|
+
import type { Container } from "./Container.ts";
|
|
8
|
+
import { ScopedAfterFlushError } from "../errors/ContainerErrors.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Per-request cache of scoped bindings, flushed when the last reference is released.
|
|
12
|
+
*/
|
|
13
|
+
export class ScopedResolver {
|
|
14
|
+
private _cache = new Map<unknown, unknown>();
|
|
15
|
+
private _pending = new Map<unknown, Promise<unknown>>();
|
|
16
|
+
private _refCount = 1; // starts at 1 — the request itself holds a reference
|
|
17
|
+
private _flushed = false;
|
|
18
|
+
|
|
19
|
+
constructor(private readonly _container?: Container) {}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a scoped binding, caching the instance for the rest of the request.
|
|
23
|
+
* Concurrent calls for the same token share a single in-flight promise.
|
|
24
|
+
*
|
|
25
|
+
* @throws {ScopedAfterFlushError} If the scope has already been flushed.
|
|
26
|
+
*/
|
|
27
|
+
async resolve<T>(token: unknown, factory: Factory<T>): Promise<T> {
|
|
28
|
+
if (this._flushed) {
|
|
29
|
+
throw new ScopedAfterFlushError(
|
|
30
|
+
`Attempted to resolve a scoped binding after the request scope was flushed. ` +
|
|
31
|
+
`Ensure afterResponse() is called before the response pipeline resolves.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (this._cache.has(token)) {
|
|
36
|
+
return this._cache.get(token) as T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (this._pending.has(token)) {
|
|
40
|
+
return this._pending.get(token) as Promise<T>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const promise = Promise.resolve(factory(this._container!)).then((instance) => {
|
|
44
|
+
this._cache.set(token, instance);
|
|
45
|
+
this._pending.delete(token);
|
|
46
|
+
return instance;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this._pending.set(token, promise);
|
|
50
|
+
return promise;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Reference counting ────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Acquire a reference. Called synchronously by HttpContext.afterResponse()
|
|
57
|
+
* at registration time — before any await — so the request's finally block
|
|
58
|
+
* cannot flush the scope before the callback has a chance to run.
|
|
59
|
+
*/
|
|
60
|
+
acquire(): void {
|
|
61
|
+
if (this._flushed) {
|
|
62
|
+
throw new ScopedAfterFlushError(
|
|
63
|
+
`Cannot acquire a reference on a flushed ScopedResolver. ` +
|
|
64
|
+
`afterResponse() must be called before the response pipeline resolves.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
this._refCount++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Release a reference. When _refCount reaches 0, _doFlush() runs.
|
|
72
|
+
* Called by afterResponse() callback finally blocks and by flush().
|
|
73
|
+
*/
|
|
74
|
+
release(): void {
|
|
75
|
+
this._refCount = Math.max(0, this._refCount - 1);
|
|
76
|
+
if (this._refCount === 0) this._doFlush();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Called by Application.ts in the fetch handler's finally block.
|
|
81
|
+
* Releases the request's own reference (the initial 1).
|
|
82
|
+
* If afterResponse() callbacks are pending they hold their own references,
|
|
83
|
+
* so _doFlush() will not run until all of them have released.
|
|
84
|
+
*/
|
|
85
|
+
flush(): void {
|
|
86
|
+
this.release();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Internal cleanup ──────────────────────────────────────────────────
|
|
90
|
+
private _doFlush(): void {
|
|
91
|
+
if (this._flushed) return;
|
|
92
|
+
this._flushed = true;
|
|
93
|
+
this._cache.clear();
|
|
94
|
+
this._pending.clear();
|
|
95
|
+
// The AsyncLocalStorage entry in Container is garbage-collected automatically
|
|
96
|
+
// when the runScoped() async context exits — no manual unregistration needed.
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Introspection (for tests and debug logging) ───────────────────────
|
|
100
|
+
|
|
101
|
+
/** @internal The current number of outstanding references on this scope. */
|
|
102
|
+
get refCount(): number {
|
|
103
|
+
return this._refCount;
|
|
104
|
+
}
|
|
105
|
+
/** @internal Whether the scope has been flushed and can no longer resolve. */
|
|
106
|
+
get isFlushed(): boolean {
|
|
107
|
+
return this._flushed;
|
|
108
|
+
}
|
|
109
|
+
/** @internal The number of cached scoped instances. */
|
|
110
|
+
get cacheSize(): number {
|
|
111
|
+
return this._cache.size;
|
|
112
|
+
}
|
|
113
|
+
/** The root container — allows middleware to resolve non-scoped bindings. */
|
|
114
|
+
get container(): Container | undefined {
|
|
115
|
+
return this._container;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The IoC container and its supporting types: the {@link Container} that
|
|
3
|
+
* registers and resolves bindings (transient, singleton, scoped, value) with
|
|
4
|
+
* auto-wiring and cycle detection, the per-request {@link ScopedResolver}, the
|
|
5
|
+
* `@inject`/`deps` helpers for declaring constructor dependencies, and the
|
|
6
|
+
* {@link ContextualBindingBuilder} for consumer-specific overrides.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Container, inject } from "@zerotal/core";
|
|
11
|
+
*
|
|
12
|
+
* class Logger {}
|
|
13
|
+
*
|
|
14
|
+
* @inject(Logger)
|
|
15
|
+
* class UserService {
|
|
16
|
+
* constructor(private log: Logger) {}
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* const container = Container.createEmpty();
|
|
20
|
+
* container.singleton(Logger, () => new Logger());
|
|
21
|
+
*
|
|
22
|
+
* // UserService is auto-wired from its @inject() dependencies.
|
|
23
|
+
* const users = await container.make(UserService);
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @packageDocumentation
|
|
27
|
+
*/
|
|
28
|
+
export { Container } from "./Container.ts";
|
|
29
|
+
export { ScopedResolver } from "./ScopedResolver.ts";
|
|
30
|
+
export { inject, injectRegistry } from "./inject.ts";
|
|
31
|
+
export { ContextualBindingBuilder } from "./ContextualBindingBuilder.ts";
|
|
32
|
+
export type { ContainerBindings, BindingToken, Binding, Factory, BindingKind } from "./types.ts";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TC39 standard ES decorators for dependency injection.
|
|
3
|
+
* No reflect-metadata. No external packages.
|
|
4
|
+
*
|
|
5
|
+
* Pass the dependency tokens straight to the decorator — they are auto-wired
|
|
6
|
+
* into the constructor in order:
|
|
7
|
+
*
|
|
8
|
+
* @inject(Db, Cache)
|
|
9
|
+
* class UserService {
|
|
10
|
+
* constructor(private db: SQL, private cache: CacheManager) {}
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Tokens may be classes, abstract classes, or registry keys. Because standard
|
|
14
|
+
* decorators expose no runtime parameter types, the tokens must be listed
|
|
15
|
+
* explicitly (there is no reflect-metadata inference).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { BindingToken } from "./types.ts";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Module-level registry: constructor → ordered dependency tokens.
|
|
22
|
+
*
|
|
23
|
+
* @internal Populated by {@link inject} and read by the container during
|
|
24
|
+
* auto-wiring; not part of the public API.
|
|
25
|
+
*/
|
|
26
|
+
export const injectRegistry = new Map<Function, BindingToken[]>();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mark a class for auto-wiring by the container, declaring its constructor
|
|
30
|
+
* dependencies in order. `@inject()` with no tokens marks a class with a
|
|
31
|
+
* no-argument constructor for auto-wiring.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* @inject(Db, Cache)
|
|
36
|
+
* class UserService {
|
|
37
|
+
* constructor(private db: SQL, private cache: CacheManager) {}
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* // The container builds the dependencies and injects them in order:
|
|
41
|
+
* const users = await container.make(UserService);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function inject(...tokens: BindingToken[]) {
|
|
45
|
+
// `args: any[]` (the codebase's standard Constructor shape) keeps the target
|
|
46
|
+
// assignable from classes with typed constructors — `unknown[]` would fail the
|
|
47
|
+
// constructor-parameter contravariance check at the decoration site.
|
|
48
|
+
return function (
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any — decorator target must accept any class shape
|
|
50
|
+
target: new (...args: any[]) => unknown,
|
|
51
|
+
_context: ClassDecoratorContext,
|
|
52
|
+
): void {
|
|
53
|
+
injectRegistry.set(target, tokens);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The IoC container's type vocabulary: binding tokens, factory functions, the
|
|
3
|
+
* discriminated union of binding descriptors, and the typed binding registry
|
|
4
|
+
* that other packages extend through declaration merging.
|
|
5
|
+
*/
|
|
6
|
+
import type { Container } from "./Container.ts";
|
|
7
|
+
|
|
8
|
+
// ── Binding token types ───────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
// `args: any[]` (the codebase's standard Constructor shape) lets classes with
|
|
11
|
+
// typed constructors be used as tokens — `unknown[]` fails constructor-parameter
|
|
12
|
+
// contravariance at the call site (e.g. `App.make(ServiceWithCtorDeps)`).
|
|
13
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- token types must accept any class shape */
|
|
14
|
+
/** A concrete class usable as a container token. */
|
|
15
|
+
export type ClassToken<T> = new (...args: any[]) => T;
|
|
16
|
+
/** An abstract class usable as a container token. */
|
|
17
|
+
export type AbstractToken<T> = abstract new (...args: any[]) => T;
|
|
18
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
19
|
+
|
|
20
|
+
/** Any value that can identify a binding: a class, an abstract class, or a registry key. */
|
|
21
|
+
export type BindingToken<T = unknown> = ClassToken<T> | AbstractToken<T> | keyof ContainerBindings;
|
|
22
|
+
|
|
23
|
+
/** Builds an instance for a binding; may resolve asynchronously. */
|
|
24
|
+
export type Factory<T> = (container: Container) => T | Promise<T>;
|
|
25
|
+
|
|
26
|
+
/** The four binding lifetimes the container supports. */
|
|
27
|
+
export type BindingKind = "transient" | "singleton" | "scoped" | "value";
|
|
28
|
+
|
|
29
|
+
/** A binding that runs its factory on every resolution. */
|
|
30
|
+
export interface TransientBinding<T> {
|
|
31
|
+
kind: "transient";
|
|
32
|
+
factory: Factory<T>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A binding resolved once and cached for the process lifetime. */
|
|
36
|
+
export interface SingletonBinding<T> {
|
|
37
|
+
kind: "singleton";
|
|
38
|
+
factory: Factory<T>;
|
|
39
|
+
instance: T | undefined;
|
|
40
|
+
resolved: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A binding resolved once per request scope. */
|
|
44
|
+
export interface ScopedBinding<T> {
|
|
45
|
+
kind: "scoped";
|
|
46
|
+
factory: Factory<T>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A binding backed by an already-constructed value. */
|
|
50
|
+
export interface ValueBinding<T> {
|
|
51
|
+
kind: "value";
|
|
52
|
+
instance: T;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The discriminated union of all binding descriptors, keyed by `kind`. */
|
|
56
|
+
export type Binding<T> =
|
|
57
|
+
TransientBinding<T> | SingletonBinding<T> | ScopedBinding<T> | ValueBinding<T>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The typed registry of well-known container bindings.
|
|
61
|
+
*
|
|
62
|
+
* Third-party packages extend this via declaration merging:
|
|
63
|
+
* declare module '@zerotal/core' { interface ContainerBindings { 'db': SQL } }
|
|
64
|
+
*/
|
|
65
|
+
export interface ContainerBindings {
|
|
66
|
+
config: import("../config/ConfigManager.ts").ConfigManager;
|
|
67
|
+
events: import("../events/Emitter.ts").Emitter;
|
|
68
|
+
commands: import("../command/CommandRunner.ts").CommandRunner;
|
|
69
|
+
lock: import("../lock/LockManager.ts").LockManager;
|
|
70
|
+
log: import("../logger/LogManager.ts").LogManager;
|
|
71
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient access to the current request's `HttpContext` through
|
|
3
|
+
* AsyncLocalStorage, so deeply nested code can reach the request without it
|
|
4
|
+
* being threaded through every call.
|
|
5
|
+
*/
|
|
6
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
import type { HttpContext } from "../pipeline/HttpContext.ts";
|
|
8
|
+
import type { TransactionContext } from "../contracts/index.ts";
|
|
9
|
+
import { ContextOutsideRequestError } from "../errors/index.ts";
|
|
10
|
+
|
|
11
|
+
// One ALS instance for the entire process lifetime.
|
|
12
|
+
const storage = new AsyncLocalStorage<HttpContext>();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Static accessor for the request-scoped `HttpContext` stored in AsyncLocalStorage.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* A boundary is opened once per request via {@link RequestContext.run} (in the
|
|
19
|
+
* `Bun.serve()` fetch handler). Inside it, any code — however deeply nested —
|
|
20
|
+
* reaches the current request with {@link RequestContext.get} (throws off-request)
|
|
21
|
+
* or {@link RequestContext.tryGet} (returns `undefined` off-request).
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const ctx = RequestContext.tryGet();
|
|
26
|
+
* if (ctx) console.log(`handling ${ctx.request.url}`);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export class RequestContext {
|
|
30
|
+
// ── Entry point ───────────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* Establish a per-request ALS boundary.
|
|
33
|
+
*
|
|
34
|
+
* MUST be called at the very top of the Bun.serve() fetch handler —
|
|
35
|
+
* synchronously, before any await — to avoid the Bun #24199 edge case
|
|
36
|
+
* where ALS context is lost if await happens before run() is called.
|
|
37
|
+
*
|
|
38
|
+
* The callback runs inside the boundary. Everything called from within
|
|
39
|
+
* the callback (including deeply nested async code) can access ctx via
|
|
40
|
+
* RequestContext.get() with no argument passing.
|
|
41
|
+
*/
|
|
42
|
+
static run<T>(ctx: HttpContext, callback: () => T): T {
|
|
43
|
+
return storage.run(ctx, callback);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Accessors ─────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns the current HttpContext. Throws if called outside a request.
|
|
50
|
+
*
|
|
51
|
+
* @throws {ContextOutsideRequestError} When called outside a request boundary opened by {@link RequestContext.run}.
|
|
52
|
+
*/
|
|
53
|
+
static get(): HttpContext {
|
|
54
|
+
const ctx = storage.getStore();
|
|
55
|
+
if (!ctx) throw new ContextOutsideRequestError();
|
|
56
|
+
return ctx;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns the current HttpContext or undefined.
|
|
61
|
+
* Use this for code that runs in both request and non-request contexts
|
|
62
|
+
* (CLI commands, queue workers, scheduled jobs).
|
|
63
|
+
*/
|
|
64
|
+
static tryGet(): HttpContext | undefined {
|
|
65
|
+
return storage.getStore();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Typed shortcuts — facades use these ──────────────────────────────
|
|
69
|
+
|
|
70
|
+
/** The current request. Throws if called outside a request. */
|
|
71
|
+
static request(): Request {
|
|
72
|
+
return this.get().request;
|
|
73
|
+
}
|
|
74
|
+
/** The current request's unique id. Throws if called outside a request. */
|
|
75
|
+
static requestId(): string {
|
|
76
|
+
return this.get().requestId;
|
|
77
|
+
}
|
|
78
|
+
/** Elapsed milliseconds since the current request started. Throws if outside a request. */
|
|
79
|
+
static took(): number {
|
|
80
|
+
return this.get().took;
|
|
81
|
+
}
|
|
82
|
+
/** The current request's locale, defaulting to `"en"`. Throws if outside a request. */
|
|
83
|
+
static locale(): string {
|
|
84
|
+
return this.get().locale ?? "en";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The active database transaction for the current request, or `undefined`. */
|
|
88
|
+
static transaction(): TransactionContext | undefined {
|
|
89
|
+
return this.tryGet()?._transaction;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The authenticatable-user contract — the identity of "the current user" as the
|
|
3
|
+
* kernel and first-party packages refer to it, with **no** commitment to any
|
|
4
|
+
* particular User model or ORM.
|
|
5
|
+
*
|
|
6
|
+
* It is intentionally empty. An application makes it concrete by merging its own
|
|
7
|
+
* User type in, which flows through every core surface that speaks of the
|
|
8
|
+
* authenticated user (`ctx.user`, the `Auth` facade, policies):
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* // app/models/User.ts
|
|
13
|
+
* declare module "@zerotal/core/contracts" {
|
|
14
|
+
* interface AuthenticatableUser extends User {}
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* The `AuthenticatedUser` interface exported from `@zerotal/core` extends this,
|
|
20
|
+
* so augmenting either interface resolves to the same shape.
|
|
21
|
+
*
|
|
22
|
+
* @category Contracts
|
|
23
|
+
*/
|
|
24
|
+
export interface AuthenticatableUser {}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@zerotal/core/contracts` — the framework's interface layer.
|
|
3
|
+
*
|
|
4
|
+
* Every type here is **pure surface**: zero runtime code, and this subtree
|
|
5
|
+
* imports nothing from the rest of core. Its job is to break the duck-typing at
|
|
6
|
+
* the seams between the kernel and its satellite packages. Core consumes a
|
|
7
|
+
* contract; a package (or a third-party alternative) implements it; neither
|
|
8
|
+
* needs to import the other.
|
|
9
|
+
*
|
|
10
|
+
* | Contract | Kernel use | Implemented by |
|
|
11
|
+
* | --- | --- | --- |
|
|
12
|
+
* | {@link SessionContract} | `ctx.session`, `ctx.flash()` / `flashed()` | `@zerotal/session` |
|
|
13
|
+
* | {@link TransactionContext} | `ctx._transaction`, `RequestContext.transaction()` | `@zerotal/orm` |
|
|
14
|
+
* | {@link AuthenticatableUser} | the current-user identity | the app's User model |
|
|
15
|
+
*
|
|
16
|
+
* A package wires in by implementing the contract and merging into a registry —
|
|
17
|
+
* never by reaching into an untyped side channel on `HttpContext`.
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
export type { SessionContract } from "./session.ts";
|
|
22
|
+
export type { TransactionContext } from "./transaction.ts";
|
|
23
|
+
export type { AuthenticatableUser } from "./auth.ts";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session contract — the interface the kernel consumes so that
|
|
3
|
+
* `HttpContext.flash()` / `flashed()` and any session-aware code depend on a
|
|
4
|
+
* *shape*, never on `@zerotal/session`.
|
|
5
|
+
*
|
|
6
|
+
* `@zerotal/session` provides the implementation (`SessionManager` is what
|
|
7
|
+
* lands on `ctx.session`; `SessionAccessor` backs the `Session` facade). A third
|
|
8
|
+
* party can ship an alternative store and satisfy the same contract, and core is
|
|
9
|
+
* none the wiser.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* This file has **zero** runtime code and imports nothing — it is pure type
|
|
13
|
+
* surface. The extension point is an interface the compiler enforces, so a
|
|
14
|
+
* miswired implementation is a build error rather than a runtime surprise.
|
|
15
|
+
*
|
|
16
|
+
* @category Contracts
|
|
17
|
+
*/
|
|
18
|
+
export interface SessionContract {
|
|
19
|
+
/**
|
|
20
|
+
* The current session ID.
|
|
21
|
+
*/
|
|
22
|
+
id(): string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read the value stored under `key`, or `undefined` if absent.
|
|
26
|
+
*
|
|
27
|
+
* Returns `unknown` — the caller asserts the stored shape. Higher-level
|
|
28
|
+
* surfaces (the `Session` facade, `ctx.session` helpers) layer a generic
|
|
29
|
+
* `<T>` cast on top.
|
|
30
|
+
*/
|
|
31
|
+
get(key: string): unknown;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read the value under `key` and remove it in one step ("read once").
|
|
35
|
+
* Returns the stored value, or `undefined` if absent.
|
|
36
|
+
*/
|
|
37
|
+
pull(key: string): unknown;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Store `value` under `key`, overwriting any existing entry.
|
|
41
|
+
*/
|
|
42
|
+
set(key: string, value: unknown): void;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Report whether `key` is present in the session.
|
|
46
|
+
*/
|
|
47
|
+
has(key: string): boolean;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Remove a single value from the session.
|
|
51
|
+
*/
|
|
52
|
+
forget(key: string): void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Remove every value from the session. The session ID is left unchanged.
|
|
56
|
+
*/
|
|
57
|
+
flush(): void;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Store a value that survives for exactly one subsequent request.
|
|
61
|
+
* Backs `ctx.flash()`.
|
|
62
|
+
*/
|
|
63
|
+
flash(key: string, value: unknown): void;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Issue a fresh session ID while preserving the current session data.
|
|
67
|
+
* Call on any privilege change (login) to defend against session fixation.
|
|
68
|
+
*/
|
|
69
|
+
regenerate(): void;
|
|
70
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The database-transaction contract — the opaque handle the kernel *carries*
|
|
3
|
+
* for a request or scope without depending on `@zerotal/orm`.
|
|
4
|
+
*
|
|
5
|
+
* `HttpContext._transaction` and `RequestContext.transaction()` are typed as
|
|
6
|
+
* this so the ORM can stash the active connection on the request and read it
|
|
7
|
+
* back, while core never learns what a `Bun.sql` connection actually is. The
|
|
8
|
+
* ORM's `SQLInstance` is a structural supertype of this — it adds `begin()` /
|
|
9
|
+
* `end()` — so assigning one here is checked, not cast.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* Core only ever *holds* the value; it never calls it. The single call
|
|
13
|
+
* signature is enough to make the carried value type-safe while leaving the
|
|
14
|
+
* full query API to the ORM, which narrows back to `SQLInstance` at its own
|
|
15
|
+
* boundary.
|
|
16
|
+
*
|
|
17
|
+
* @category Contracts
|
|
18
|
+
*/
|
|
19
|
+
export interface TransactionContext {
|
|
20
|
+
/**
|
|
21
|
+
* Run a query via a tagged template. Present so the contract is a genuine
|
|
22
|
+
* SQL-connection shape rather than an empty marker; the ORM's connection type
|
|
23
|
+
* is assignable to it.
|
|
24
|
+
*/
|
|
25
|
+
<T = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convention-based auto-registration: at boot it scans well-known `app/*`
|
|
3
|
+
* directories, imports each file, and lets a "concern descriptor" register the
|
|
4
|
+
* relevant exports (models, observers, listeners, …). Packages contribute
|
|
5
|
+
* descriptors via `app.registerConcern(...)`, so core depends on neither orm nor auth.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Dev/test glob the filesystem; production can ship a generated manifest (see
|
|
9
|
+
// `manifest` in config) to avoid runtime scanning — same idea as the jobs barrel.
|
|
10
|
+
|
|
11
|
+
import type { Application } from "../application/Application.ts";
|
|
12
|
+
import type { AppEnvironment } from "../provider/ServiceProvider.ts";
|
|
13
|
+
import { frameworkLog } from "../logger/frameworkLog.ts";
|
|
14
|
+
|
|
15
|
+
/** The context a concern receives while registering discovered modules. */
|
|
16
|
+
export interface ConcernContext {
|
|
17
|
+
app: Application;
|
|
18
|
+
env: AppEnvironment;
|
|
19
|
+
/** Resolve a container binding synchronously, or undefined if not bound. */
|
|
20
|
+
resolve<T = unknown>(token: string): T | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Describes one auto-registration concern: where to scan and how to register what it finds. */
|
|
24
|
+
export interface ConcernDescriptor {
|
|
25
|
+
/** Identifier, e.g. "models". Used as the config key for path overrides. */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Lower runs first (models=10, observers=20, policies=30, listeners=40, …). */
|
|
28
|
+
order: number;
|
|
29
|
+
/** App-relative directory to scan, e.g. "app/models". Omit for one-shot concerns. */
|
|
30
|
+
dir?: string;
|
|
31
|
+
/** Environments this concern runs in. Default: all. */
|
|
32
|
+
envs?: AppEnvironment[];
|
|
33
|
+
/** Register the relevant exports from one discovered module. */
|
|
34
|
+
register?(exports: Record<string, unknown>, ctx: ConcernContext): void | Promise<void>;
|
|
35
|
+
/** One-shot hook run once after this concern's files (or instead of scanning). */
|
|
36
|
+
run?(ctx: ConcernContext): void | Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Options controlling a single {@link runConventions} pass. */
|
|
40
|
+
export interface RunConventionsOptions {
|
|
41
|
+
root: string;
|
|
42
|
+
env: AppEnvironment;
|
|
43
|
+
/** Per-concern directory overrides (config `discovery.paths`). */
|
|
44
|
+
paths?: Record<string, string>;
|
|
45
|
+
ctx: ConcernContext;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _shouldSkip(relativePath: string): boolean {
|
|
49
|
+
const file = relativePath.split("/").at(-1) ?? "";
|
|
50
|
+
return (
|
|
51
|
+
file.startsWith("_") ||
|
|
52
|
+
file.endsWith(".test.ts") ||
|
|
53
|
+
file.endsWith(".test.tsx") ||
|
|
54
|
+
file.endsWith(".spec.ts") ||
|
|
55
|
+
file.endsWith(".d.ts")
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function _scanDir(absoluteDirectory: string): Promise<string[]> {
|
|
60
|
+
// glob.scan yields nothing for a missing directory; the try/catch is belt-and-suspenders.
|
|
61
|
+
try {
|
|
62
|
+
const glob = new Bun.Glob("**/*.{ts,tsx}");
|
|
63
|
+
return await Array.fromAsync(glob.scan({ cwd: absoluteDirectory, onlyFiles: true }));
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Scan a directory, import each non-skipped file, and hand the module to `onModule`. Shared by the
|
|
71
|
+
* concern loop and by the early provider/middleware discovery in Application. Import/handler
|
|
72
|
+
* errors are logged and skipped so one bad file can't abort boot.
|
|
73
|
+
*/
|
|
74
|
+
export async function importConventionModules(
|
|
75
|
+
absoluteDirectory: string,
|
|
76
|
+
label: string,
|
|
77
|
+
onModule: (module: Record<string, unknown>, file: string) => void | Promise<void>,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
for (const file of await _scanDir(absoluteDirectory)) {
|
|
80
|
+
if (_shouldSkip(file)) continue;
|
|
81
|
+
try {
|
|
82
|
+
const loadedModule = (await import(`${absoluteDirectory}/${file}`)) as Record<
|
|
83
|
+
string,
|
|
84
|
+
unknown
|
|
85
|
+
>;
|
|
86
|
+
await onModule(loadedModule, file);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
frameworkLog("app").error(`${label} discovery failed for ${file}`, { file }, error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run all concern descriptors in `order`. For each concern with a `dir`, scan + import each
|
|
95
|
+
* file and call `register(module)`. For each concern with `run`, call it once afterward.
|
|
96
|
+
* Import/registration errors are logged and skipped so one bad file can't abort boot.
|
|
97
|
+
*/
|
|
98
|
+
export async function runConventions(
|
|
99
|
+
concerns: ConcernDescriptor[],
|
|
100
|
+
options: RunConventionsOptions,
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
const orderedConcerns = [...concerns].sort((first, second) => first.order - second.order);
|
|
103
|
+
for (const concern of orderedConcerns) {
|
|
104
|
+
if (concern.envs && !concern.envs.includes(options.env)) continue;
|
|
105
|
+
|
|
106
|
+
if (concern.dir && concern.register) {
|
|
107
|
+
const relativeDirectory = options.paths?.[concern.name] ?? concern.dir;
|
|
108
|
+
const register = concern.register;
|
|
109
|
+
await importConventionModules(
|
|
110
|
+
`${options.root}/${relativeDirectory}`,
|
|
111
|
+
`convention "${concern.name}"`,
|
|
112
|
+
(loadedModule) => register(loadedModule, options.ctx),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (concern.run) {
|
|
117
|
+
try {
|
|
118
|
+
await concern.run(options.ctx);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
frameworkLog("app").error(
|
|
121
|
+
`Convention "${concern.name}" run hook failed`,
|
|
122
|
+
{ convention: concern.name },
|
|
123
|
+
error,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|