@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.
@@ -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 Compiled procedure ready for registration
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>>): CompiledProcedure<TInput, TOutput, TContext, 'query'>;
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 Compiled procedure ready for registration
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>>): CompiledProcedure<TInput, TOutput, TContext, 'mutation'>;
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
  *
@@ -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
  }