moost 0.5.32 → 0.6.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.
@@ -0,0 +1,185 @@
1
+ # Decorators & Metadata — moost
2
+
3
+ > The decorator system, metadata storage via `@prostojs/mate`, handler registration, and how to create custom decorators.
4
+
5
+ ## Concepts
6
+
7
+ Moost's decorator system is built on `@prostojs/mate`, a metadata management library. All decorators write to a shared metadata workspace (`'moost'`) via `getMoostMate()`. This metadata is then read during `app.init()` to bind handlers, resolve dependencies, and configure interceptors.
8
+
9
+ **Key types of decorators:**
10
+ - **Class decorators** — `@Controller()`, `@Injectable()`, `@ImportController()`
11
+ - **Method decorators** — `@Get()`, `@Cli()`, `@Intercept()`, handler registrations
12
+ - **Parameter decorators** — `@Param()`, `@Resolve()`, `@Inject()`, `@Body()`
13
+ - **Property decorators** — `@Inject()`, `@Provide()`, hooks
14
+
15
+ Metadata is stored per-class and per-method using TypeScript's legacy experimental decorators (`experimentalDecorators: true`, `emitDecoratorMetadata: true`).
16
+
17
+ ## API Reference
18
+
19
+ ### `getMoostMate()`
20
+
21
+ Returns the singleton `Mate` instance for the `'moost'` metadata workspace.
22
+
23
+ ```ts
24
+ import { getMoostMate } from 'moost'
25
+
26
+ const mate = getMoostMate()
27
+
28
+ // Read class metadata
29
+ const classMeta = mate.read(MyController)
30
+
31
+ // Read method metadata
32
+ const methodMeta = mate.read(MyController.prototype, 'myMethod')
33
+
34
+ // Write custom metadata via decorator
35
+ mate.decorate('myKey', myValue) // Overwrites
36
+ mate.decorate('myArray', item, true) // Appends to array
37
+ ```
38
+
39
+ ### `TMoostMetadata` (interface)
40
+
41
+ The shape of metadata stored per class/method:
42
+
43
+ ```ts
44
+ interface TMoostMetadata<H = {}> {
45
+ controller?: { prefix?: string }
46
+ importController?: Array<{ prefix?; typeResolver?; provide? }>
47
+ injectable?: true | 'FOR_EVENT' | 'SINGLETON'
48
+ interceptor?: { priority: TInterceptorPriority }
49
+ interceptors?: TInterceptorData[]
50
+ handlers?: TMoostHandler<H>[] // Set by adapter decorators
51
+ pipes?: TPipeData[]
52
+ provide?: TProvideRegistry
53
+ replace?: TReplaceRegistry
54
+ params: Array<TMateParamMeta & TMoostParamsMetadata>
55
+ returnType?: Function
56
+ loggerTopic?: string
57
+ id?: string // Handler ID for path builders
58
+ // ... and more
59
+ }
60
+ ```
61
+
62
+ ### `TMoostHandler<H>` (type)
63
+
64
+ ```ts
65
+ type TMoostHandler<H> = {
66
+ type: string // Adapter type identifier ('HTTP', 'WS_MESSAGE', 'CRON', etc.)
67
+ path?: string // Route/command path
68
+ } & H // Custom handler metadata fields
69
+ ```
70
+
71
+ ### `@Controller(prefix?: string)`
72
+
73
+ Marks a class as a controller. The prefix is prepended to all handler paths.
74
+
75
+ ```ts
76
+ @Controller('api/v1')
77
+ class ApiController { }
78
+ ```
79
+
80
+ ### `@Injectable(scope?: 'SINGLETON' | 'FOR_EVENT')`
81
+
82
+ Marks a class for DI registration with a lifecycle scope.
83
+
84
+ ```ts
85
+ @Injectable('SINGLETON') // One instance for the app lifetime
86
+ class DbService { }
87
+
88
+ @Injectable('FOR_EVENT') // New instance per event (request, command, etc.)
89
+ class RequestScoped { }
90
+ ```
91
+
92
+ ### `@Description(text: string)`
93
+
94
+ Adds a description to a class or method (used by swagger, CLI help).
95
+
96
+ ```ts
97
+ @Description('User management endpoints')
98
+ @Controller('users')
99
+ class UserController {
100
+ @Description('Get user by ID')
101
+ @Get(':id')
102
+ getUser() { }
103
+ }
104
+ ```
105
+
106
+ ## Common Patterns
107
+
108
+ ### Pattern: Creating a Handler Decorator
109
+
110
+ All adapter-specific decorators follow the same pattern:
111
+
112
+ ```ts
113
+ import type { TEmpty, TMoostMetadata } from 'moost'
114
+ import { getMoostMate } from 'moost'
115
+
116
+ function MyDecorator(path?: string): MethodDecorator {
117
+ return getMoostMate<TEmpty, TMoostMetadata<{ /* extra fields */ }>>().decorate(
118
+ 'handlers', // Always 'handlers' for handler registration
119
+ {
120
+ path,
121
+ type: 'MY_TYPE', // Unique string your adapter filters by
122
+ // ...extra fields
123
+ },
124
+ true, // Array mode — accumulates multiple decorators
125
+ )
126
+ }
127
+ ```
128
+
129
+ ### Pattern: Reading Metadata in an Adapter
130
+
131
+ ```ts
132
+ bindHandler<T extends object>(opts: TMoostAdapterOptions<TMyMeta, T>) {
133
+ const mate = getMoostMate()
134
+
135
+ for (const handler of opts.handlers) {
136
+ if (handler.type !== 'MY_TYPE') continue
137
+
138
+ // Read additional method-level metadata
139
+ const methodMeta = mate.read(opts.fakeInstance, opts.method as string)
140
+
141
+ // Read class-level metadata
142
+ const classMeta = mate.read(opts.fakeInstance)
143
+
144
+ // Access custom metadata set by your decorators
145
+ const myCustomData = methodMeta?.myCustomField
146
+ }
147
+ }
148
+ ```
149
+
150
+ ### Pattern: Dual Parameter/Property Decorator
151
+
152
+ Some decorators work on both parameters and class properties:
153
+
154
+ ```ts
155
+ function MyValue(): ParameterDecorator & PropertyDecorator {
156
+ return Resolve(() => computeValue(), 'my-value')
157
+ }
158
+
159
+ // As parameter:
160
+ @Get('test')
161
+ handler(@MyValue() val: string) { }
162
+
163
+ // As property:
164
+ @Controller()
165
+ class MyCtrl {
166
+ @MyValue()
167
+ val!: string
168
+
169
+ @Get('test')
170
+ handler() { return this.val }
171
+ }
172
+ ```
173
+
174
+ ## Best Practices
175
+
176
+ - Use `getMoostMate()` (not `new Mate()`) — all Moost metadata must share the same workspace
177
+ - Handler decorators must use array mode (`true` as third arg to `decorate`) so multiple decorators can coexist
178
+ - Use distinctive `type` strings to avoid collisions between adapters
179
+ - Keep metadata types lean — store only what `bindHandler` needs
180
+
181
+ ## Gotchas
182
+
183
+ - `getMoostMate().read(instance, method)` requires the **prototype** instance, not a constructed one — use `opts.fakeInstance` in adapters
184
+ - Metadata is inherited from parent classes by default for class-level metadata (when `inherit: true` is set)
185
+ - TypeScript's `emitDecoratorMetadata` must be enabled for type reflection to work (`design:type`, `design:paramtypes`, `design:returntype`)
@@ -0,0 +1,161 @@
1
+ # Dependency Injection — moost
2
+
3
+ > DI container powered by `@prostojs/infact`, scoping strategies, provider registration, and injection patterns.
4
+
5
+ ## Concepts
6
+
7
+ Moost's DI is built on `@prostojs/infact`. It supports two scopes:
8
+
9
+ - **SINGLETON** — One instance per application. Created during `init()` or on first access.
10
+ - **FOR_EVENT** — One instance per event (HTTP request, CLI command, etc.). Cleaned up when the event scope is unregistered.
11
+
12
+ DI resolution happens automatically during the handler lifecycle — controller instances, interceptors, and injected dependencies are all resolved through the same container.
13
+
14
+ ### Scope Lifecycle
15
+
16
+ ```
17
+ app.init()
18
+ → SINGLETON instances created (once)
19
+
20
+ Event arrives:
21
+ → DI scope registered (useScopeId + registerEventScope)
22
+ → FOR_EVENT instances created on demand
23
+ → Handler executes
24
+ → DI scope unregistered → FOR_EVENT instances released
25
+ ```
26
+
27
+ ## API Reference
28
+
29
+ ### `@Injectable(scope?: 'SINGLETON' | 'FOR_EVENT' | true)`
30
+
31
+ Marks a class as DI-managed. `true` is an alias for `'SINGLETON'`.
32
+
33
+ ```ts
34
+ @Injectable('SINGLETON')
35
+ class DatabaseService {
36
+ constructor() { /* connects once */ }
37
+ }
38
+
39
+ @Injectable('FOR_EVENT')
40
+ class RequestContext {
41
+ // Fresh instance per request/event
42
+ }
43
+ ```
44
+
45
+ ### `@Inject(token: string | symbol | Class)`
46
+
47
+ Explicit injection by token. Use when automatic type-based resolution isn't sufficient.
48
+
49
+ ```ts
50
+ @Injectable()
51
+ class MyService {
52
+ constructor(@Inject('CONFIG') private config: AppConfig) {}
53
+ }
54
+ ```
55
+
56
+ ### `@Provide(token?, value?)`
57
+
58
+ Registers a value or factory in the DI container from a controller/class.
59
+
60
+ ```ts
61
+ @Controller()
62
+ class AppController {
63
+ @Provide('APP_VERSION')
64
+ version = '1.0.0'
65
+ }
66
+ ```
67
+
68
+ ### `@Circular(() => Type)`
69
+
70
+ Handles circular dependency references by deferring resolution.
71
+
72
+ ```ts
73
+ @Injectable()
74
+ class ServiceA {
75
+ constructor(@Circular(() => ServiceB) private b: ServiceB) {}
76
+ }
77
+ ```
78
+
79
+ ### `createProvideRegistry(...entries)`
80
+
81
+ Creates a provider registry for adapter `getProvideRegistry()`.
82
+
83
+ ```ts
84
+ import { createProvideRegistry } from 'moost'
85
+
86
+ const registry = createProvideRegistry(
87
+ [MyClass, () => myInstance],
88
+ ['string-token', () => someValue],
89
+ )
90
+ ```
91
+
92
+ ### `createReplaceRegistry(...entries)`
93
+
94
+ Creates a replacement registry for overriding existing providers (useful for testing).
95
+
96
+ ```ts
97
+ import { createReplaceRegistry } from 'moost'
98
+
99
+ const replacements = createReplaceRegistry(
100
+ [RealService, () => new MockService()],
101
+ )
102
+ ```
103
+
104
+ ### `getMoostInfact()`
105
+
106
+ Returns the singleton `Infact` DI container instance. Mainly used internally by adapters.
107
+
108
+ ```ts
109
+ import { getMoostInfact } from 'moost'
110
+
111
+ const infact = getMoostInfact()
112
+ infact.registerScope(scopeId)
113
+ infact.unregisterScope(scopeId)
114
+ ```
115
+
116
+ ## Common Patterns
117
+
118
+ ### Pattern: Adapter DI Providers
119
+
120
+ Adapters expose their services to controllers via `getProvideRegistry()`:
121
+
122
+ ```ts
123
+ class MoostHttp implements TMoostAdapter<THttpHandlerMeta> {
124
+ getProvideRegistry() {
125
+ return createProvideRegistry(
126
+ [WooksHttp, () => this.getHttpApp()],
127
+ [HttpServer, () => this.getHttpApp().getServer()],
128
+ )
129
+ }
130
+ }
131
+ ```
132
+
133
+ ### Pattern: Scope Cleanup for Long-lived Events
134
+
135
+ For events that outlive a handler call, adapters manually manage scope lifecycle:
136
+
137
+ ```ts
138
+ const fn = defineMoostEventHandler({
139
+ manualUnscope: true,
140
+ hooks: {
141
+ init: ({ unscope }) => {
142
+ // Defer cleanup to when the event truly ends
143
+ onEventEnd(() => unscope())
144
+ },
145
+ },
146
+ })
147
+ ```
148
+
149
+ ## Best Practices
150
+
151
+ - Default to `SINGLETON` scope unless instances need per-event state
152
+ - Use `FOR_EVENT` for anything that holds request-specific data (user context, auth state)
153
+ - Adapters should provide both class tokens and string tokens in `getProvideRegistry()`
154
+ - Avoid manual `getMoostInfact()` calls in application code — use decorators instead
155
+
156
+ ## Gotchas
157
+
158
+ - `SINGLETON` instances are created during `app.init()` inside a synthetic event context — composables may not behave as expected in constructors
159
+ - `FOR_EVENT` instances are only available during event processing — injecting them outside an event context will fail
160
+ - Circular dependencies require `@Circular(() => Type)` — without it, the container throws at resolution time
161
+ - Scope IDs are monotonic integers (not UUIDs) for performance — don't rely on their format
@@ -0,0 +1,199 @@
1
+ # Interceptors — moost
2
+
3
+ > Interceptor lifecycle, priority levels, `@Intercept` decorator, `InterceptorHandler` internals, and creating custom interceptors.
4
+
5
+ ## Concepts
6
+
7
+ Interceptors wrap handler execution in an onion-like pattern. They can:
8
+ - **Short-circuit** — Return a response before the handler runs
9
+ - **Modify responses** — Transform the handler's return value
10
+ - **Handle errors** — Catch and replace errors
11
+ - **Add side effects** — Logging, tracing, auth checks
12
+
13
+ ### Priority Levels
14
+
15
+ Interceptors run in priority order (lowest first):
16
+
17
+ | Priority | Constant | Typical Use |
18
+ |----------|----------|-------------|
19
+ | 0 | `BEFORE_ALL` | Logging, tracing setup |
20
+ | 10 | `BEFORE_GUARD` | Pre-auth setup |
21
+ | 20 | `GUARD` | Authentication/authorization checks |
22
+ | 30 | `AFTER_GUARD` | Post-auth enrichment |
23
+ | 40 | `INTERCEPTOR` | General-purpose (default) |
24
+ | 50 | `CATCH_ERROR` | Error handling |
25
+ | 60 | `AFTER_ALL` | Response transformation, cleanup |
26
+
27
+ ### Lifecycle Phases
28
+
29
+ ```
30
+ before phase (all interceptors, in priority order)
31
+ └─ Each can return a value to short-circuit
32
+ handler execution
33
+ after phase (in LIFO/reverse order)
34
+ └─ Each receives the response, can modify it
35
+ onError phase (if handler threw)
36
+ └─ Each receives the error, can replace it
37
+ ```
38
+
39
+ The after/error phases use **LIFO order** (last registered runs first) — creating a wrap-around pattern where the outermost interceptor has first and last access.
40
+
41
+ ## API Reference
42
+
43
+ ### `@Intercept(handler, priority?)`
44
+
45
+ Registers an interceptor on a class or method. The handler can be a class or a functional interceptor definition.
46
+
47
+ ```ts
48
+ import { Intercept, TInterceptorPriority } from 'moost'
49
+
50
+ @Intercept(MyGuard, TInterceptorPriority.GUARD)
51
+ @Controller()
52
+ class ProtectedController { }
53
+
54
+ @Intercept(loggingInterceptor)
55
+ @Get('data')
56
+ getData() { }
57
+ ```
58
+
59
+ ### Functional Interceptor (TInterceptorDef)
60
+
61
+ ```ts
62
+ import type { TInterceptorDef } from 'moost'
63
+
64
+ const myInterceptor: TInterceptorDef = {
65
+ // Called before handler — return value to short-circuit
66
+ before(reply: (response: unknown) => void) {
67
+ // Optional: reply(value) to skip handler
68
+ },
69
+
70
+ // Called after handler (or after before's reply)
71
+ after(response: unknown, reply: (response: unknown) => void) {
72
+ // Optional: modify response via reply(newValue)
73
+ },
74
+
75
+ // Called if handler threw
76
+ onError(error: unknown, reply: (response: unknown) => void) {
77
+ // Optional: recover from error via reply(fallbackValue)
78
+ },
79
+ }
80
+ ```
81
+
82
+ ### Class-Based Interceptor
83
+
84
+ ```ts
85
+ import { Injectable, Before, After, OnError } from 'moost'
86
+
87
+ @Injectable()
88
+ class LoggingInterceptor {
89
+ @Before()
90
+ before() {
91
+ console.log('Before handler')
92
+ // Return undefined to continue, or return a value to short-circuit
93
+ }
94
+
95
+ @After()
96
+ after(response: unknown) {
97
+ console.log('After handler:', response)
98
+ // Return undefined to keep original, or return new value
99
+ }
100
+
101
+ @OnError()
102
+ onError(error: unknown) {
103
+ console.error('Handler error:', error)
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### `InterceptorHandler` (class)
109
+
110
+ Used internally by `defineMoostEventHandler`. You don't create these directly — they're provided via `opts.getIterceptorHandler()`.
111
+
112
+ Key properties:
113
+ - `count` — Total number of interceptors (0 means skip interceptor phase)
114
+ - `countAfter` — Number of after-phase handlers
115
+ - `countOnError` — Number of error-phase handlers
116
+
117
+ Key methods:
118
+ - `before()` — Runs all before interceptors. Returns early response or `undefined`
119
+ - `fireAfter(response)` — Runs after/error handlers in LIFO order. Returns final response
120
+
121
+ ## Common Patterns
122
+
123
+ ### Pattern: Auth Guard
124
+
125
+ ```ts
126
+ const authGuard: TInterceptorDef = {
127
+ before(reply) {
128
+ const token = useHeaders().get('authorization')
129
+ if (!token) {
130
+ reply(new HttpError(401, 'Unauthorized'))
131
+ }
132
+ },
133
+ }
134
+
135
+ @Intercept(authGuard, TInterceptorPriority.GUARD)
136
+ @Controller('protected')
137
+ class ProtectedController { }
138
+ ```
139
+
140
+ ### Pattern: Response Wrapper
141
+
142
+ ```ts
143
+ const wrapResponse: TInterceptorDef = {
144
+ after(response, reply) {
145
+ reply({ data: response, timestamp: Date.now() })
146
+ },
147
+ }
148
+ ```
149
+
150
+ ### Pattern: Global Error Handler
151
+
152
+ ```ts
153
+ app.interceptor({
154
+ handler: {
155
+ onError(error, reply) {
156
+ if (error instanceof HttpError) {
157
+ reply({ error: error.message, status: error.statusCode })
158
+ }
159
+ },
160
+ },
161
+ priority: TInterceptorPriority.CATCH_ERROR,
162
+ })
163
+ ```
164
+
165
+ ## Integration
166
+
167
+ ### With Adapters
168
+
169
+ Adapters receive `getIterceptorHandler` in `bindHandler()` options. Pass it through to `defineMoostEventHandler()` — interceptors are handled automatically.
170
+
171
+ For not-found handlers, use `moost.getGlobalInterceptorHandler()` to run global interceptors (CORS, logging) even on unmatched routes.
172
+
173
+ ### With Context Types
174
+
175
+ Interceptors can check `contextType` to apply only to specific event types:
176
+
177
+ ```ts
178
+ const httpOnly: TInterceptorDef = {
179
+ before() {
180
+ // This interceptor is registered globally but only meaningful for HTTP
181
+ // Check event type if needed
182
+ },
183
+ }
184
+ ```
185
+
186
+ ## Best Practices
187
+
188
+ - Use `GUARD` priority for auth checks — they run before general interceptors
189
+ - Use `AFTER_ALL` for response transformations — they run after everything else
190
+ - Use `CATCH_ERROR` for error recovery — keeps error handling separate from business logic
191
+ - Prefer functional interceptors for simple logic, class-based for complex logic requiring DI
192
+ - Return `undefined` from `before()` to continue to the handler; return a value to short-circuit
193
+
194
+ ## Gotchas
195
+
196
+ - After-phase interceptors run in **LIFO** order (reverse of registration) — the last registered interceptor's `after()` runs first
197
+ - If `before()` returns a value, the handler is **skipped** but after-phase interceptors still run
198
+ - Error interceptors receive the error as the `response` parameter, not as a thrown exception
199
+ - `InterceptorHandler.count === 0` means no interceptors — the entire interceptor phase is skipped for performance