@znight/core 0.3.0 → 0.5.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/README.md +30 -0
- package/dist/index.d.mts +121 -2
- package/dist/index.mjs +45 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +10 -1
- package/src/hooks.ts +57 -0
- package/src/index.ts +15 -0
- package/src/params.ts +89 -0
package/package.json
CHANGED
package/src/errors.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* messaging, config, observability).
|
|
8
8
|
* - From `ZNIGHT030`, each first-party package reserves a block of ten: `ZNIGHT030`–`039`
|
|
9
9
|
* `@znight/openapi`, `ZNIGHT040`–`049` `@znight/schedule`, `ZNIGHT050`–`059` `@znight/websocket`
|
|
10
|
-
* (reserved), `ZNIGHT060`–`069` `@znight/graphql`.
|
|
10
|
+
* (reserved), `ZNIGHT060`–`069` `@znight/graphql`, `ZNIGHT070`–`079` `@znight/events`.
|
|
11
11
|
*
|
|
12
12
|
* Within a range, codes are allocated in order without gaps. A package uses its block for its own
|
|
13
13
|
* failure modes, with one catch-all code for invalid usage and options, and reuses a lower code
|
|
@@ -138,6 +138,14 @@ export const ErrorCode = {
|
|
|
138
138
|
* type or argument the SDL does not declare, a signature that does not conform to the SDL types.
|
|
139
139
|
*/
|
|
140
140
|
GraphQLSchemaMismatch: 'ZNIGHT068',
|
|
141
|
+
|
|
142
|
+
// ZNIGHT070–079: @znight/events (RFC 0011)
|
|
143
|
+
/**
|
|
144
|
+
* Invalid event listener: an `@OnEvent` method whose parameter is not a class (or a union of
|
|
145
|
+
* classes), a generic or optional event, an event class the generated module cannot import, invalid
|
|
146
|
+
* options, the NestJS string form, a detached listener depending on a request-scoped external.
|
|
147
|
+
*/
|
|
148
|
+
InvalidEventListener: 'ZNIGHT070',
|
|
141
149
|
} as const
|
|
142
150
|
|
|
143
151
|
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode]
|
|
@@ -189,6 +197,7 @@ export const ERROR_DESCRIPTIONS: Readonly<Record<ErrorCode, string>> = {
|
|
|
189
197
|
ZNIGHT066: 'Invalid GraphQL setup: metadata, schema, GraphQLPlugin options, driver or HttpPlugin missing',
|
|
190
198
|
ZNIGHT067: 'Invalid GraphQL SDL: the schema-first .graphql files cannot be read, parsed or validated',
|
|
191
199
|
ZNIGHT068: 'GraphQL schema mismatch: schema-first resolvers do not cover or conform to the SDL',
|
|
200
|
+
ZNIGHT070: 'Invalid event listener: an @OnEvent method does not listen to a class, or its options are invalid',
|
|
192
201
|
}
|
|
193
202
|
|
|
194
203
|
export interface ZnightErrorOptions extends ErrorOptions {
|
package/src/hooks.ts
CHANGED
|
@@ -33,6 +33,17 @@ export interface ZnightHooks {
|
|
|
33
33
|
* emitted when listeners exist.
|
|
34
34
|
*/
|
|
35
35
|
'graphql:operation': GraphQLOperationEvent
|
|
36
|
+
/**
|
|
37
|
+
* One publication of `@znight/events` (`EventPublisher.publish()` or `emit()`) that matched at least
|
|
38
|
+
* one listener (RFC 0011): wraps the inline phase with `hooks.run` when wrappers exist, then is
|
|
39
|
+
* emitted when listeners exist. A publication without listener produces nothing.
|
|
40
|
+
*/
|
|
41
|
+
'events:publish': EventPublishEvent
|
|
42
|
+
/**
|
|
43
|
+
* One listener invocation of `@znight/events` (RFC 0011), inline or detached: wrapped with
|
|
44
|
+
* `hooks.run` when wrappers exist, then emitted when listeners exist.
|
|
45
|
+
*/
|
|
46
|
+
'events:listener': EventListenerEvent
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
/**
|
|
@@ -227,6 +238,52 @@ export interface GraphQLOperationEvent {
|
|
|
227
238
|
errors?: readonly GraphQLOperationError[]
|
|
228
239
|
}
|
|
229
240
|
|
|
241
|
+
/** How an application event was published (`EventPublishEvent.mode`). */
|
|
242
|
+
export type EventPublishMode = 'publish' | 'emit'
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Instrumentation event of the `events:publish` hook, produced by `@znight/events` (RFC 0011) for a
|
|
246
|
+
* publication that matched at least one listener. The wrappers surround the inline phase (the
|
|
247
|
+
* detached listeners are scheduled after it succeeded); `error` and `failedListener` are set before
|
|
248
|
+
* `next()` rejects.
|
|
249
|
+
*/
|
|
250
|
+
export interface EventPublishEvent {
|
|
251
|
+
/** Class name of the event (`UserCreated`). */
|
|
252
|
+
readonly event: string
|
|
253
|
+
/** The published object. */
|
|
254
|
+
readonly payload: object
|
|
255
|
+
readonly mode: EventPublishMode
|
|
256
|
+
/** Number of inline listeners matched. */
|
|
257
|
+
readonly inline: number
|
|
258
|
+
/** Number of detached listeners matched. */
|
|
259
|
+
readonly detached: number
|
|
260
|
+
/** `performance.now()` when the publication started. */
|
|
261
|
+
readonly startTime: number
|
|
262
|
+
/** First inline error, set before `next()` rejects in the wrappers. */
|
|
263
|
+
error?: unknown
|
|
264
|
+
/** `Class.method` of the inline listener that failed. */
|
|
265
|
+
failedListener?: string
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Instrumentation event of the `events:listener` hook, produced by `@znight/events` (RFC 0011) for
|
|
270
|
+
* every listener invocation. Inline listeners run in the publisher's async context, detached ones in
|
|
271
|
+
* a fresh scope of their own; `error` is set before `next()` rejects.
|
|
272
|
+
*/
|
|
273
|
+
export interface EventListenerEvent {
|
|
274
|
+
/** Class name of the event. */
|
|
275
|
+
readonly event: string
|
|
276
|
+
/** The published object. */
|
|
277
|
+
readonly payload: object
|
|
278
|
+
/** `Class.method` of the listener (`WelcomeMail.send`). */
|
|
279
|
+
readonly listener: string
|
|
280
|
+
readonly detached: boolean
|
|
281
|
+
/** `performance.now()` when the invocation started. */
|
|
282
|
+
readonly startTime: number
|
|
283
|
+
/** What the listener threw or rejected with. */
|
|
284
|
+
error?: unknown
|
|
285
|
+
}
|
|
286
|
+
|
|
230
287
|
/** Name of a declared hook. */
|
|
231
288
|
export type HookName = keyof ZnightHooks
|
|
232
289
|
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,9 @@ export {
|
|
|
38
38
|
export { isProviderDefinition } from './definition.ts'
|
|
39
39
|
export * from './errors.ts'
|
|
40
40
|
export {
|
|
41
|
+
type EventListenerEvent,
|
|
42
|
+
type EventPublishEvent,
|
|
43
|
+
type EventPublishMode,
|
|
41
44
|
type GraphQLOperationError,
|
|
42
45
|
type GraphQLOperationEvent,
|
|
43
46
|
type GraphQLOperationOutcome,
|
|
@@ -78,6 +81,18 @@ export {
|
|
|
78
81
|
silentLogger,
|
|
79
82
|
} from './logger.ts'
|
|
80
83
|
export * from './metadata.ts'
|
|
84
|
+
export {
|
|
85
|
+
createParamDecorator,
|
|
86
|
+
type ParamContexts,
|
|
87
|
+
type ParamDataArgs,
|
|
88
|
+
type ParamDecorator,
|
|
89
|
+
type ParamFunctionOf,
|
|
90
|
+
type ParamResolver,
|
|
91
|
+
type ParamResolverClass,
|
|
92
|
+
type ParamResolverFunction,
|
|
93
|
+
type ParamResolverOf,
|
|
94
|
+
type ParamResolvers,
|
|
95
|
+
} from './params.ts'
|
|
81
96
|
export {
|
|
82
97
|
type Plugin,
|
|
83
98
|
type PluginContext,
|
package/src/params.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom parameter decorators (RFC 0010). `createParamDecorator` returns a marker decorator, read
|
|
3
|
+
* by `@znight/compiler`, and keeps the resolvers that the generated `invoke` of each transport calls.
|
|
4
|
+
*/
|
|
5
|
+
import type { ParameterMarker } from './decorators.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolver contexts by transport. Each transport package adds its own entry through declaration
|
|
9
|
+
* merging (`http: HttpContext` in `@znight/http`, `graphql: ResolverContext`, `messaging:
|
|
10
|
+
* InboundMessage`); a transport that is not installed has no entry.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* declare module '@znight/core' {
|
|
14
|
+
* interface ParamContexts {
|
|
15
|
+
* http: HttpContext
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
// biome-ignore lint/suspicious/noEmptyInterface: augmented by the transport packages (declaration merging)
|
|
21
|
+
export interface ParamContexts {}
|
|
22
|
+
|
|
23
|
+
/** A provider computing the value of a parameter, in the scope of the call (RFC 0010 §4.6). */
|
|
24
|
+
export interface ParamResolver<C, T, D = undefined> {
|
|
25
|
+
resolve(ctx: C, data: D): T | PromiseLike<T>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A resolver function: receives the context of its transport and the data argument of the use. */
|
|
29
|
+
export type ParamResolverFunction<C, T, D> = (ctx: C, data: D) => T | PromiseLike<T>
|
|
30
|
+
|
|
31
|
+
/** The class of a provider implementing {@link ParamResolver} (bound at build time, resolved in the scope of the call). */
|
|
32
|
+
export type ParamResolverClass<C, T, D> = abstract new (...args: never[]) => ParamResolver<C, T, D>
|
|
33
|
+
|
|
34
|
+
/** Computes the value of a parameter: a function, or the class of a provider implementing {@link ParamResolver}. */
|
|
35
|
+
export type ParamResolverOf<C, T, D> = ParamResolverFunction<C, T, D> | ParamResolverClass<C, T, D>
|
|
36
|
+
|
|
37
|
+
/** The resolvers of a definition, by transport (keys of {@link ParamContexts}). */
|
|
38
|
+
export type ParamResolvers<T, D> = {
|
|
39
|
+
readonly [K in keyof ParamContexts]?: ParamResolverOf<ParamContexts[K], T, D>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Arguments of a use: none when `D` is `undefined`, optional when `D` includes `undefined`, required otherwise. */
|
|
43
|
+
export type ParamDataArgs<D> = undefined extends D ? [data?: D] : [data: D]
|
|
44
|
+
|
|
45
|
+
/** What {@link createParamDecorator} returns: the decorator (a no-op marker) and its resolvers, for generated code. */
|
|
46
|
+
export interface ParamDecorator<T, D = undefined> {
|
|
47
|
+
(...data: ParamDataArgs<D>): ParameterMarker
|
|
48
|
+
/** The resolvers of the definition, frozen. */
|
|
49
|
+
readonly resolvers: ParamResolvers<T, D>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The resolver function of a definition for a transport, as generated code calls it:
|
|
54
|
+
* `(CurrentUser.resolvers.http as ParamFunctionOf<typeof CurrentUser, 'http'>)(ctx, undefined)`.
|
|
55
|
+
* The compiler emits it only for function resolvers.
|
|
56
|
+
*/
|
|
57
|
+
export type ParamFunctionOf<P extends { readonly resolvers: object }, K extends keyof P['resolvers']> = Extract<
|
|
58
|
+
NonNullable<P['resolvers'][K]>,
|
|
59
|
+
// biome-ignore lint/suspicious/noExplicitAny: matches every function type, whatever its parameters
|
|
60
|
+
(...args: any[]) => unknown
|
|
61
|
+
>
|
|
62
|
+
|
|
63
|
+
function marker(): void {
|
|
64
|
+
// intentionally empty: decorators are compile-time markers
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const parameterMarker: ParameterMarker = marker
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Defines a parameter decorator whose value is computed per call by a resolver of the handler's
|
|
71
|
+
* transport (RFC 0010). `T` (required) is the injected type, checked at build time against every
|
|
72
|
+
* decorated parameter; `D` is the type of the data argument (none by default). The definition must
|
|
73
|
+
* be an exported top-level `const`: the generated module imports it.
|
|
74
|
+
*
|
|
75
|
+
* ```ts
|
|
76
|
+
* export const CurrentUser = createParamDecorator<User>({
|
|
77
|
+
* http: ctx => userOf(ctx.state),
|
|
78
|
+
* graphql: ctx => userOf(ctx.operation.state),
|
|
79
|
+
* })
|
|
80
|
+
*
|
|
81
|
+
* @Get('/me')
|
|
82
|
+
* me(@CurrentUser() user: User) { ... }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function createParamDecorator<T, D = undefined>(resolvers: ParamResolvers<T, D>): ParamDecorator<T, D> {
|
|
86
|
+
const decorator = ((..._data: ParamDataArgs<D>): ParameterMarker => parameterMarker) as ParamDecorator<T, D>
|
|
87
|
+
Object.defineProperty(decorator, 'resolvers', { value: Object.freeze({ ...resolvers }), enumerable: true })
|
|
88
|
+
return decorator
|
|
89
|
+
}
|