@veloxts/router 0.8.0 → 0.8.2
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 +18 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/procedure/builder.d.ts +1 -1
- package/dist/procedure/builder.js +206 -17
- package/dist/procedure/pipeline-executor.d.ts +69 -0
- package/dist/procedure/pipeline-executor.js +134 -0
- package/dist/procedure/pipeline.d.ts +98 -0
- package/dist/procedure/pipeline.js +50 -0
- package/dist/procedure/types.d.ts +230 -19
- package/dist/rest/adapter.js +6 -11
- package/dist/types.d.ts +156 -10
- package/package.json +8 -8
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step and revert action factories
|
|
3
|
+
*
|
|
4
|
+
* Provides `defineStep` and `defineRevert` for building pipeline steps
|
|
5
|
+
* that execute in sequence via `.through()` on the procedure builder.
|
|
6
|
+
* Each step's output becomes the next step's input. External steps run
|
|
7
|
+
* outside DB transactions and can have revert actions for compensation.
|
|
8
|
+
*
|
|
9
|
+
* @module procedure/pipeline
|
|
10
|
+
*/
|
|
11
|
+
/** @internal */
|
|
12
|
+
export function defineStep(nameOrOptions, handler) {
|
|
13
|
+
const resolved = typeof nameOrOptions === 'string'
|
|
14
|
+
? { name: nameOrOptions, external: false }
|
|
15
|
+
: { name: nameOrOptions.name, external: nameOrOptions.external ?? false };
|
|
16
|
+
return createStep(resolved.name, resolved.external, handler, undefined);
|
|
17
|
+
}
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// defineRevert
|
|
20
|
+
// ============================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Define a revert action for compensating an external pipeline step
|
|
23
|
+
*
|
|
24
|
+
* Revert handlers receive the same `{ input, ctx }` shape but return void.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const refund = defineRevert('refundPayment', async ({ input, ctx }) => {
|
|
29
|
+
* await gateway.refund(input.chargeId);
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function defineRevert(name, handler) {
|
|
34
|
+
return { name, handler };
|
|
35
|
+
}
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Internal helpers
|
|
38
|
+
// ============================================================================
|
|
39
|
+
/** @internal Create a PipelineStep object with onRevert method */
|
|
40
|
+
function createStep(name, external, handler, revertAction) {
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
external,
|
|
44
|
+
handler,
|
|
45
|
+
revertAction,
|
|
46
|
+
onRevert(revert) {
|
|
47
|
+
return createStep(name, external, handler, revert);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module procedure/types
|
|
11
11
|
*/
|
|
12
|
-
import type { BaseContext } from '@veloxts/core';
|
|
12
|
+
import type { BaseContext, DomainError } from '@veloxts/core';
|
|
13
13
|
import type { ZodType } from 'zod';
|
|
14
14
|
import type { OutputForLevel, ResourceSchema, TaggedResourceSchema } from '../resource/index.js';
|
|
15
|
-
import type { CompiledProcedure, GuardLike, MiddlewareFunction, ParentResourceConfig, ProcedureHandler, RestRouteOverride } from '../types.js';
|
|
15
|
+
import type { AfterHandler, CompiledProcedure, GuardLike, MiddlewareFunction, ParentResourceConfig, PolicyActionLike, ProcedureHandler, RestRouteOverride, TransactionalOptions } from '../types.js';
|
|
16
|
+
import type { PipelineStep } from './pipeline.js';
|
|
16
17
|
/**
|
|
17
18
|
* Internal state type that accumulates type information through the builder chain
|
|
18
19
|
*
|
|
@@ -22,14 +23,16 @@ import type { CompiledProcedure, GuardLike, MiddlewareFunction, ParentResourceCo
|
|
|
22
23
|
* @template TInput - The validated input type (unknown if no input schema)
|
|
23
24
|
* @template TOutput - The validated output type (unknown if no output schema)
|
|
24
25
|
* @template TContext - The context type (starts as BaseContext, extended by middleware)
|
|
26
|
+
* @template TErrors - Union of domain error types (defaults to never)
|
|
25
27
|
*/
|
|
26
|
-
export interface ProcedureBuilderState<TInput = unknown, TOutput = unknown, TContext extends BaseContext = BaseContext> {
|
|
28
|
+
export interface ProcedureBuilderState<TInput = unknown, TOutput = unknown, TContext extends BaseContext = BaseContext, TErrors = never> {
|
|
27
29
|
/** Marker for state identification */
|
|
28
30
|
readonly _brand: 'ProcedureBuilderState';
|
|
29
31
|
/** Phantom type holders - not used at runtime */
|
|
30
32
|
readonly _input: TInput;
|
|
31
33
|
readonly _output: TOutput;
|
|
32
34
|
readonly _context: TContext;
|
|
35
|
+
readonly _errors: TErrors;
|
|
33
36
|
}
|
|
34
37
|
/**
|
|
35
38
|
* Constraint for valid input/output schemas
|
|
@@ -73,6 +76,7 @@ export type InferOutputSchema<T> = T extends TaggedResourceSchema ? OutputForLev
|
|
|
73
76
|
* @template TInput - Current input type
|
|
74
77
|
* @template TOutput - Current output type
|
|
75
78
|
* @template TContext - Current context type
|
|
79
|
+
* @template TErrors - Union of domain error types (defaults to never)
|
|
76
80
|
*
|
|
77
81
|
* @example
|
|
78
82
|
* ```typescript
|
|
@@ -84,7 +88,7 @@ export type InferOutputSchema<T> = T extends TaggedResourceSchema ? OutputForLev
|
|
|
84
88
|
* })
|
|
85
89
|
* ```
|
|
86
90
|
*/
|
|
87
|
-
export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext extends BaseContext = BaseContext> {
|
|
91
|
+
export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext extends BaseContext = BaseContext, TErrors = never> {
|
|
88
92
|
/**
|
|
89
93
|
* Defines the input validation schema for the procedure
|
|
90
94
|
*
|
|
@@ -105,7 +109,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
105
109
|
* // input is now typed as { id: string; name?: string }
|
|
106
110
|
* ```
|
|
107
111
|
*/
|
|
108
|
-
input<TSchema extends ValidSchema>(schema: TSchema): ProcedureBuilder<InferSchemaOutput<TSchema>, TOutput, TContext>;
|
|
112
|
+
input<TSchema extends ValidSchema>(schema: TSchema): ProcedureBuilder<InferSchemaOutput<TSchema>, TOutput, TContext, TErrors>;
|
|
109
113
|
/**
|
|
110
114
|
* Defines the output schema for the procedure
|
|
111
115
|
*
|
|
@@ -131,7 +135,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
131
135
|
* .query(handler)
|
|
132
136
|
* ```
|
|
133
137
|
*/
|
|
134
|
-
output<TSchema extends ValidOutputSchema>(schema: TSchema): ProcedureBuilder<TInput, InferOutputSchema<TSchema>, TContext>;
|
|
138
|
+
output<TSchema extends ValidOutputSchema>(schema: TSchema): ProcedureBuilder<TInput, InferOutputSchema<TSchema>, TContext, TErrors>;
|
|
135
139
|
/**
|
|
136
140
|
* Adds middleware to the procedure chain
|
|
137
141
|
*
|
|
@@ -161,7 +165,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
161
165
|
* })
|
|
162
166
|
* ```
|
|
163
167
|
*/
|
|
164
|
-
use<TNewContext extends BaseContext = TContext>(middleware: MiddlewareFunction<TInput, TContext, TNewContext, TOutput>): ProcedureBuilder<TInput, TOutput, TNewContext>;
|
|
168
|
+
use<TNewContext extends BaseContext = TContext>(middleware: MiddlewareFunction<TInput, TContext, TNewContext, TOutput>): ProcedureBuilder<TInput, TOutput, TNewContext, TErrors>;
|
|
165
169
|
/**
|
|
166
170
|
* Adds an authorization guard to the procedure
|
|
167
171
|
*
|
|
@@ -194,7 +198,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
194
198
|
* .mutation(async ({ input, ctx }) => { ... });
|
|
195
199
|
* ```
|
|
196
200
|
*/
|
|
197
|
-
guard<TGuardContext extends Partial<TContext>>(guard: GuardLike<TGuardContext>): ProcedureBuilder<TInput, TOutput, TContext>;
|
|
201
|
+
guard<TGuardContext extends Partial<TContext>>(guard: GuardLike<TGuardContext>): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
|
|
198
202
|
/**
|
|
199
203
|
* Adds multiple authorization guards at once
|
|
200
204
|
*
|
|
@@ -221,7 +225,62 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
221
225
|
* .mutation(async ({ input, ctx }) => { ... });
|
|
222
226
|
* ```
|
|
223
227
|
*/
|
|
224
|
-
guards<TGuards extends GuardLike<Partial<TContext>>[]>(...guards: TGuards): ProcedureBuilder<TInput, TOutput, TContext>;
|
|
228
|
+
guards<TGuards extends GuardLike<Partial<TContext>>[]>(...guards: TGuards): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
|
|
229
|
+
/**
|
|
230
|
+
* Adds a policy action check to the procedure
|
|
231
|
+
*
|
|
232
|
+
* Policy actions are checked during execution after guards but before
|
|
233
|
+
* the pipeline and handler. The policy looks up the resource on the
|
|
234
|
+
* context by lowercase resource name (e.g., `PostPolicy` → `ctx.post`).
|
|
235
|
+
*
|
|
236
|
+
* If the policy check fails, a ForbiddenError is thrown.
|
|
237
|
+
*
|
|
238
|
+
* **Compile-time safety:** When the resource name is a string literal
|
|
239
|
+
* (e.g., from `definePolicy('Post', ...)`), TypeScript enforces that the
|
|
240
|
+
* context contains the resource key. Forgetting `.use(loadPost)` before
|
|
241
|
+
* `.policy(PostPolicy.update)` produces a type error.
|
|
242
|
+
*
|
|
243
|
+
* @param action - Policy action reference (from definePolicy)
|
|
244
|
+
* @returns Same builder (no type changes)
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* import { PostPolicy } from './policies';
|
|
249
|
+
*
|
|
250
|
+
* procedure()
|
|
251
|
+
* .guard(authenticated)
|
|
252
|
+
* .use(loadPost) // adds { post: Post } to context
|
|
253
|
+
* .policy(PostPolicy.update) // ✓ context has 'post'
|
|
254
|
+
* .mutation(async ({ input, ctx }) => { ... });
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
policy<TResourceName extends string>(action: string extends TResourceName ? PolicyActionLike<unknown, unknown, TResourceName> : Uncapitalize<TResourceName> extends keyof TContext ? PolicyActionLike<unknown, unknown, TResourceName> : PolicyActionLike<unknown, unknown, TResourceName> & {
|
|
258
|
+
_contextMissing: `Add .use() middleware to provide '${Uncapitalize<TResourceName>}' in context before .policy()`;
|
|
259
|
+
}): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
|
|
260
|
+
/**
|
|
261
|
+
* Declares domain error classes that this procedure may throw
|
|
262
|
+
*
|
|
263
|
+
* Stores error class references on the compiled procedure for:
|
|
264
|
+
* - OpenAPI error response generation (per-endpoint error schemas)
|
|
265
|
+
* - Client-side error type narrowing (`InferProcedureErrors<T>`)
|
|
266
|
+
*
|
|
267
|
+
* Can be called multiple times — error classes accumulate across calls.
|
|
268
|
+
*
|
|
269
|
+
* @template TDomainErrors - Union of domain error types declared
|
|
270
|
+
* @param errorClasses - Domain error constructors this procedure may throw
|
|
271
|
+
* @returns New builder with updated TErrors union
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```typescript
|
|
275
|
+
* procedure()
|
|
276
|
+
* .input(z.object({ sku: z.string(), qty: z.number() }))
|
|
277
|
+
* .throws(InsufficientStock, PaymentFailed)
|
|
278
|
+
* .mutation(async ({ input }) => {
|
|
279
|
+
* // handler may throw InsufficientStock or PaymentFailed
|
|
280
|
+
* })
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
throws<TDomainErrors extends DomainError<Record<string, unknown>>>(...errorClasses: Array<new (data: Record<string, unknown>) => TDomainErrors>): ProcedureBuilder<TInput, TOutput, TContext, TErrors | TDomainErrors>;
|
|
225
284
|
/**
|
|
226
285
|
* Configures REST route override
|
|
227
286
|
*
|
|
@@ -237,7 +296,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
237
296
|
* .rest({ method: 'POST', path: '/users/:id/activate' })
|
|
238
297
|
* ```
|
|
239
298
|
*/
|
|
240
|
-
rest(config: RestRouteOverride): ProcedureBuilder<TInput, TOutput, TContext>;
|
|
299
|
+
rest(config: RestRouteOverride): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
|
|
241
300
|
/**
|
|
242
301
|
* Configures the procedure as a webhook endpoint
|
|
243
302
|
*
|
|
@@ -257,7 +316,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
257
316
|
* })
|
|
258
317
|
* ```
|
|
259
318
|
*/
|
|
260
|
-
webhook(path: string): ProcedureBuilder<TInput, TOutput, TContext>;
|
|
319
|
+
webhook(path: string): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
|
|
261
320
|
/**
|
|
262
321
|
* Marks the procedure as deprecated
|
|
263
322
|
*
|
|
@@ -280,7 +339,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
280
339
|
* .query(handler);
|
|
281
340
|
* ```
|
|
282
341
|
*/
|
|
283
|
-
deprecated(message?: string): ProcedureBuilder<TInput, TOutput, TContext>;
|
|
342
|
+
deprecated(message?: string): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
|
|
284
343
|
/**
|
|
285
344
|
* Declares a parent resource for nested routes (single level)
|
|
286
345
|
*
|
|
@@ -309,7 +368,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
309
368
|
* .query(async ({ input }) => { ... });
|
|
310
369
|
* ```
|
|
311
370
|
*/
|
|
312
|
-
parent(resource: string, param?: string): ProcedureBuilder<TInput, TOutput, TContext>;
|
|
371
|
+
parent(resource: string, param?: string): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
|
|
313
372
|
/**
|
|
314
373
|
* Declares multiple parent resources for deeply nested routes
|
|
315
374
|
*
|
|
@@ -340,7 +399,94 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
340
399
|
parents(config: Array<{
|
|
341
400
|
resource: string;
|
|
342
401
|
param?: string;
|
|
343
|
-
}>): ProcedureBuilder<TInput, TOutput, TContext>;
|
|
402
|
+
}>): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
|
|
403
|
+
/**
|
|
404
|
+
* Declares a domain event to emit after successful handler execution
|
|
405
|
+
*
|
|
406
|
+
* Events fire AFTER the handler returns its result (and AFTER transaction
|
|
407
|
+
* commit when `.transactional()` is used). Multiple `.emits()` calls
|
|
408
|
+
* accumulate — all declared events are emitted in order.
|
|
409
|
+
*
|
|
410
|
+
* Emission errors are caught and logged; they never fail the request.
|
|
411
|
+
*
|
|
412
|
+
* @template TEventData - The event's data payload type
|
|
413
|
+
* @param eventClass - Domain event class constructor
|
|
414
|
+
* @param mapper - Optional function to transform the handler result into event data.
|
|
415
|
+
* When omitted, the handler result is used directly as event data.
|
|
416
|
+
* @returns Same builder (no type changes)
|
|
417
|
+
*
|
|
418
|
+
* @example With mapper (recommended)
|
|
419
|
+
* ```typescript
|
|
420
|
+
* procedure()
|
|
421
|
+
* .emits(OrderCreated, (result) => ({ orderId: result.id, total: result.total }))
|
|
422
|
+
* .mutation(handler)
|
|
423
|
+
* ```
|
|
424
|
+
*
|
|
425
|
+
* @example Without mapper (result type must match event data type)
|
|
426
|
+
* ```typescript
|
|
427
|
+
* procedure()
|
|
428
|
+
* .emits(OrderCreated)
|
|
429
|
+
* .mutation(handler)
|
|
430
|
+
* ```
|
|
431
|
+
*/
|
|
432
|
+
emits<TEventData extends Record<string, unknown>>(eventClass: {
|
|
433
|
+
new (data: TEventData, options?: {
|
|
434
|
+
correlationId?: string;
|
|
435
|
+
}): unknown;
|
|
436
|
+
readonly name: string;
|
|
437
|
+
}, mapper?: (result: TOutput) => TEventData): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
|
|
438
|
+
/**
|
|
439
|
+
* Wraps the handler in a database transaction via `ctx.db.$transaction()`
|
|
440
|
+
*
|
|
441
|
+
* When set, `executeProcedure` replaces `ctx.db` with the transactional
|
|
442
|
+
* client inside the handler. On throw the transaction auto-rollbacks;
|
|
443
|
+
* on return it auto-commits.
|
|
444
|
+
*
|
|
445
|
+
* Gracefully degrades: if `ctx.db` or `ctx.db.$transaction` is missing,
|
|
446
|
+
* the handler runs without transaction wrapping.
|
|
447
|
+
*
|
|
448
|
+
* @param options - Optional isolation level and timeout
|
|
449
|
+
* @returns Same builder (no type changes)
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```typescript
|
|
453
|
+
* procedure()
|
|
454
|
+
* .input(CreateOrderSchema)
|
|
455
|
+
* .transactional({ isolationLevel: 'Serializable', timeout: 10000 })
|
|
456
|
+
* .mutation(async ({ input, ctx }) => {
|
|
457
|
+
* // ctx.db is the transactional client — all queries share the same tx
|
|
458
|
+
* const order = await ctx.db.order.create({ data: input });
|
|
459
|
+
* await ctx.db.inventory.update({ ... });
|
|
460
|
+
* return order; // auto-commit on success
|
|
461
|
+
* })
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
transactional(options?: TransactionalOptions): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
|
|
465
|
+
/**
|
|
466
|
+
* Adds pipeline steps that execute BEFORE the handler
|
|
467
|
+
*
|
|
468
|
+
* Steps run in declaration order. Each step's output becomes the next
|
|
469
|
+
* step's input, and the final step's output is passed to the handler
|
|
470
|
+
* as its `input`. If a step fails, revert actions for previously
|
|
471
|
+
* completed steps run in reverse order (compensation pattern).
|
|
472
|
+
*
|
|
473
|
+
* Can be called multiple times — steps accumulate across calls.
|
|
474
|
+
*
|
|
475
|
+
* @param steps - Pipeline steps to execute before the handler
|
|
476
|
+
* @returns Same builder (no type changes)
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```typescript
|
|
480
|
+
* procedure()
|
|
481
|
+
* .input(CreateOrderSchema)
|
|
482
|
+
* .through(validateInventory, chargePayment.onRevert(refund))
|
|
483
|
+
* .mutation(async ({ input, ctx }) => {
|
|
484
|
+
* // input has been transformed by pipeline steps
|
|
485
|
+
* return ctx.db.order.create({ data: input });
|
|
486
|
+
* })
|
|
487
|
+
* ```
|
|
488
|
+
*/
|
|
489
|
+
through(...steps: PipelineStep[]): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
|
|
344
490
|
/**
|
|
345
491
|
* Finalizes the procedure as a query (read-only operation)
|
|
346
492
|
*
|
|
@@ -348,7 +494,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
348
494
|
* The handler receives the validated input and context.
|
|
349
495
|
*
|
|
350
496
|
* @param handler - The query handler function
|
|
351
|
-
* @returns
|
|
497
|
+
* @returns PostHandlerBuilder with all CompiledProcedure fields plus .useAfter()
|
|
352
498
|
*
|
|
353
499
|
* @example
|
|
354
500
|
* ```typescript
|
|
@@ -359,7 +505,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
359
505
|
* })
|
|
360
506
|
* ```
|
|
361
507
|
*/
|
|
362
|
-
query(handler: ProcedureHandler<TInput, TOutput, TContext> | Record<string, ProcedureHandler<TInput, TOutput, TContext>>):
|
|
508
|
+
query(handler: ProcedureHandler<TInput, TOutput, TContext> | Record<string, ProcedureHandler<TInput, TOutput, TContext>>): PostHandlerBuilder<TInput, TOutput, TContext, 'query', TErrors>;
|
|
363
509
|
/**
|
|
364
510
|
* Finalizes the procedure as a mutation (write operation)
|
|
365
511
|
*
|
|
@@ -370,7 +516,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
370
516
|
* accepts a handler map keyed by `[Schema.level.key]` instead of a single handler.
|
|
371
517
|
*
|
|
372
518
|
* @param handler - The mutation handler function or handler map
|
|
373
|
-
* @returns
|
|
519
|
+
* @returns PostHandlerBuilder with all CompiledProcedure fields plus .useAfter()
|
|
374
520
|
*
|
|
375
521
|
* @example
|
|
376
522
|
* ```typescript
|
|
@@ -381,7 +527,52 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
381
527
|
* })
|
|
382
528
|
* ```
|
|
383
529
|
*/
|
|
384
|
-
mutation(handler: ProcedureHandler<TInput, TOutput, TContext> | Record<string, ProcedureHandler<TInput, TOutput, TContext>>):
|
|
530
|
+
mutation(handler: ProcedureHandler<TInput, TOutput, TContext> | Record<string, ProcedureHandler<TInput, TOutput, TContext>>): PostHandlerBuilder<TInput, TOutput, TContext, 'mutation', TErrors>;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Returned by `.query()` and `.mutation()` — extends CompiledProcedure with `.useAfter()`
|
|
534
|
+
*
|
|
535
|
+
* PostHandlerBuilder has all CompiledProcedure fields (so it's assignable to
|
|
536
|
+
* CompiledProcedure and passes `isCompiledProcedure` checks) plus a `.useAfter()`
|
|
537
|
+
* method for registering post-handler hooks.
|
|
538
|
+
*
|
|
539
|
+
* `.useAfter()` returns another PostHandlerBuilder so hooks can be chained.
|
|
540
|
+
*
|
|
541
|
+
* @template TInput - The validated input type
|
|
542
|
+
* @template TOutput - The handler output type
|
|
543
|
+
* @template TContext - The context type
|
|
544
|
+
* @template TType - The procedure type literal ('query' or 'mutation')
|
|
545
|
+
* @template TErrors - Union of domain error types (defaults to never)
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```typescript
|
|
549
|
+
* procedure()
|
|
550
|
+
* .input(z.object({ id: z.string() }))
|
|
551
|
+
* .mutation(async ({ input, ctx }) => {
|
|
552
|
+
* return ctx.db.user.delete({ where: { id: input.id } });
|
|
553
|
+
* })
|
|
554
|
+
* .useAfter(({ input, result, ctx }) => {
|
|
555
|
+
* console.log(`Deleted user ${input.id}`);
|
|
556
|
+
* })
|
|
557
|
+
* .useAfter(({ result }) => {
|
|
558
|
+
* invalidateCache(result.id);
|
|
559
|
+
* })
|
|
560
|
+
* ```
|
|
561
|
+
*/
|
|
562
|
+
export interface PostHandlerBuilder<TInput = unknown, TOutput = unknown, TContext extends BaseContext = BaseContext, TType extends 'query' | 'mutation' = 'query' | 'mutation', TErrors = never> extends CompiledProcedure<TInput, TOutput, TContext, TType, TErrors> {
|
|
563
|
+
/**
|
|
564
|
+
* Registers a post-handler hook that runs after successful handler execution
|
|
565
|
+
*
|
|
566
|
+
* Hooks run after events are emitted (if any) and auto-projection is applied.
|
|
567
|
+
* Errors in hooks are caught and logged — they never fail the request.
|
|
568
|
+
* The return value is ignored: hooks cannot modify the result.
|
|
569
|
+
*
|
|
570
|
+
* Multiple `.useAfter()` calls chain in registration order.
|
|
571
|
+
*
|
|
572
|
+
* @param handler - Post-handler hook function
|
|
573
|
+
* @returns New PostHandlerBuilder with the hook appended
|
|
574
|
+
*/
|
|
575
|
+
useAfter(handler: AfterHandler<TInput, TOutput, TContext>): PostHandlerBuilder<TInput, TOutput, TContext, TType, TErrors>;
|
|
385
576
|
}
|
|
386
577
|
/**
|
|
387
578
|
* Internal runtime state for the procedure builder
|
|
@@ -416,6 +607,26 @@ export interface BuilderRuntimeState {
|
|
|
416
607
|
isWebhook?: boolean;
|
|
417
608
|
/** Whether .output() received an untagged resource schema (Level 3 branching mode) */
|
|
418
609
|
branchingMode?: boolean;
|
|
610
|
+
/** Error classes declared via .throws() */
|
|
611
|
+
errorClasses?: Array<new (data: Record<string, unknown>) => unknown>;
|
|
612
|
+
/** Whether the handler should be wrapped in a database transaction */
|
|
613
|
+
transactional?: boolean;
|
|
614
|
+
/** Options for the database transaction (isolation level, timeout) */
|
|
615
|
+
transactionalOptions?: TransactionalOptions;
|
|
616
|
+
/** Domain events to emit after successful handler execution */
|
|
617
|
+
emittedEvents?: Array<{
|
|
618
|
+
eventClass: {
|
|
619
|
+
new (data: Record<string, unknown>, options?: {
|
|
620
|
+
correlationId?: string;
|
|
621
|
+
}): unknown;
|
|
622
|
+
readonly name: string;
|
|
623
|
+
};
|
|
624
|
+
mapper?: (result: unknown) => Record<string, unknown>;
|
|
625
|
+
}>;
|
|
626
|
+
/** Pipeline steps declared via .through() */
|
|
627
|
+
pipelineSteps?: PipelineStep[];
|
|
628
|
+
/** Policy action reference for declarative authorization */
|
|
629
|
+
policyAction?: PolicyActionLike;
|
|
419
630
|
}
|
|
420
631
|
/**
|
|
421
632
|
* Type for the procedures object passed to defineProcedures
|
|
@@ -430,7 +641,7 @@ export interface BuilderRuntimeState {
|
|
|
430
641
|
* The actual type safety is preserved through InferProcedures<T> which captures
|
|
431
642
|
* the concrete types at definition time. This `any` only allows the assignment.
|
|
432
643
|
*/
|
|
433
|
-
export type ProcedureDefinitions = Record<string, CompiledProcedure<any, any, any, any>>;
|
|
644
|
+
export type ProcedureDefinitions = Record<string, CompiledProcedure<any, any, any, any, any>>;
|
|
434
645
|
/**
|
|
435
646
|
* Type helper to preserve procedure types in a collection
|
|
436
647
|
*
|
package/dist/rest/adapter.js
CHANGED
|
@@ -218,25 +218,20 @@ function gatherInput(request, route) {
|
|
|
218
218
|
const params = isPlainObject(request.params) ? request.params : {};
|
|
219
219
|
const query = isPlainObject(request.query) ? request.query : {};
|
|
220
220
|
const body = isPlainObject(request.body) ? request.body : {};
|
|
221
|
-
// Check if this is a nested route (has single parent or multiple parents)
|
|
222
|
-
const hasParentResource = route.procedure.parentResource !== undefined ||
|
|
223
|
-
(route.procedure.parentResources !== undefined && route.procedure.parentResources.length > 0);
|
|
224
221
|
switch (route.method) {
|
|
225
222
|
case 'GET':
|
|
226
223
|
case 'DELETE':
|
|
227
224
|
// GET/DELETE: params (for :id and all parent params) + query (for filters/pagination/options)
|
|
228
225
|
return { ...params, ...query };
|
|
226
|
+
case 'POST':
|
|
229
227
|
case 'PUT':
|
|
230
228
|
case 'PATCH':
|
|
231
|
-
// PUT/PATCH: params (for :id and all parent params) + body (for data)
|
|
229
|
+
// POST/PUT/PATCH: params (for :id and all parent params) + body (for data).
|
|
230
|
+
// POST must merge params unconditionally — flat conventional creates have
|
|
231
|
+
// empty params (no-op spread), but RPC-style .rest() overrides such as
|
|
232
|
+
// POST /retro/phase/:sessionId/next rely on this merge to surface path
|
|
233
|
+
// params to the input schema.
|
|
232
234
|
return { ...params, ...body };
|
|
233
|
-
case 'POST':
|
|
234
|
-
// POST: For nested routes, merge params (for all parent IDs) with body
|
|
235
|
-
// For flat routes, use body only (no ID in params for creates)
|
|
236
|
-
if (hasParentResource) {
|
|
237
|
-
return { ...params, ...body };
|
|
238
|
-
}
|
|
239
|
-
return request.body;
|
|
240
235
|
default:
|
|
241
236
|
return request.body;
|
|
242
237
|
}
|