@veloxts/router 0.7.9 → 0.8.1

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.
@@ -9,11 +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
- import type { FilterFieldsByLevel, OutputForTag, ResourceSchema, TaggedResourceSchema } from '../resource/index.js';
15
- import type { ContextTag, ExtractTag, LevelToTag, TaggedContext } from '../resource/tags.js';
16
- import type { CompiledProcedure, GuardLike, MiddlewareFunction, ParentResourceConfig, ProcedureHandler, RestRouteOverride } from '../types.js';
14
+ import type { OutputForLevel, ResourceSchema, TaggedResourceSchema } from '../resource/index.js';
15
+ import type { AfterHandler, CompiledProcedure, GuardLike, MiddlewareFunction, ParentResourceConfig, PolicyActionLike, ProcedureHandler, RestRouteOverride, TransactionalOptions } from '../types.js';
16
+ import type { PipelineStep } from './pipeline.js';
17
17
  /**
18
18
  * Internal state type that accumulates type information through the builder chain
19
19
  *
@@ -23,14 +23,16 @@ import type { CompiledProcedure, GuardLike, MiddlewareFunction, ParentResourceCo
23
23
  * @template TInput - The validated input type (unknown if no input schema)
24
24
  * @template TOutput - The validated output type (unknown if no output schema)
25
25
  * @template TContext - The context type (starts as BaseContext, extended by middleware)
26
+ * @template TErrors - Union of domain error types (defaults to never)
26
27
  */
27
- 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> {
28
29
  /** Marker for state identification */
29
30
  readonly _brand: 'ProcedureBuilderState';
30
31
  /** Phantom type holders - not used at runtime */
31
32
  readonly _input: TInput;
32
33
  readonly _output: TOutput;
33
34
  readonly _context: TContext;
35
+ readonly _errors: TErrors;
34
36
  }
35
37
  /**
36
38
  * Constraint for valid input/output schemas
@@ -50,6 +52,20 @@ export type ValidSchema = ZodType;
50
52
  export type InferSchemaOutput<T> = T extends {
51
53
  parse: (data: unknown) => infer O;
52
54
  } ? O : never;
55
+ /**
56
+ * Valid types for `.output()` — Zod schemas, tagged resource views, or untagged resource schemas
57
+ */
58
+ export type ValidOutputSchema = ValidSchema | TaggedResourceSchema | ResourceSchema;
59
+ /**
60
+ * Infers the output type from a schema passed to `.output()`
61
+ *
62
+ * - Zod schema → uses structural `parse()` matching
63
+ * - Tagged resource view → uses `OutputForLevel` to compute projected fields
64
+ * - Untagged resource schema → falls back to `unknown` (Level 3 branching resolves at runtime)
65
+ */
66
+ export type InferOutputSchema<T> = T extends TaggedResourceSchema ? OutputForLevel<T> : T extends {
67
+ parse: (data: unknown) => infer O;
68
+ } ? O : unknown;
53
69
  /**
54
70
  * Fluent procedure builder interface
55
71
  *
@@ -60,6 +76,7 @@ export type InferSchemaOutput<T> = T extends {
60
76
  * @template TInput - Current input type
61
77
  * @template TOutput - Current output type
62
78
  * @template TContext - Current context type
79
+ * @template TErrors - Union of domain error types (defaults to never)
63
80
  *
64
81
  * @example
65
82
  * ```typescript
@@ -71,7 +88,7 @@ export type InferSchemaOutput<T> = T extends {
71
88
  * })
72
89
  * ```
73
90
  */
74
- 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> {
75
92
  /**
76
93
  * Defines the input validation schema for the procedure
77
94
  *
@@ -92,56 +109,33 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
92
109
  * // input is now typed as { id: string; name?: string }
93
110
  * ```
94
111
  */
95
- input<TSchema extends ValidSchema>(schema: TSchema): ProcedureBuilder<InferSchemaOutput<TSchema>, TOutput, TContext>;
112
+ input<TSchema extends ValidSchema>(schema: TSchema): ProcedureBuilder<InferSchemaOutput<TSchema>, TOutput, TContext, TErrors>;
96
113
  /**
97
- * Defines the output validation schema (Zod)
114
+ * Defines the output schema for the procedure
98
115
  *
99
- * Sets a Zod schema that validates the handler's return value.
100
- * All callers receive the same fields.
116
+ * Accepts two schema variants:
117
+ * 1. **Zod schema** — validates handler return value, all callers see same fields
118
+ * 2. **Tagged resource view** (e.g., `UserSchema.authenticated`) — auto-projects fields by access level
101
119
  *
102
- * For field-level visibility (different fields per access level),
103
- * use `.expose()` with a resource schema instead.
104
- *
105
- * @template TSchema - The Zod schema type
106
- * @param schema - Zod schema for output validation
120
+ * @template TSchema - The Zod or tagged resource schema type
121
+ * @param schema - Zod schema or tagged resource view
107
122
  * @returns New builder with updated output type
108
123
  *
109
- * @example
124
+ * @example Zod schema
110
125
  * ```typescript
111
126
  * procedure()
112
127
  * .output(z.object({ id: z.string(), name: z.string() }))
113
128
  * .query(handler) // handler must return { id: string; name: string }
114
129
  * ```
115
- */
116
- output<TSchema extends ValidSchema>(schema: TSchema): ProcedureBuilder<TInput, InferSchemaOutput<TSchema>, TContext>;
117
- /**
118
- * Sets field-level visibility via a resource schema
119
- *
120
- * Accepts two resource schema variants:
121
- * 1. **Tagged resource schema** (e.g., `UserSchema.authenticated`) — explicit field projection by access level
122
- * 2. **Plain resource schema** (e.g., `UserSchema`) — context-derived field projection from `guardNarrow`
123
130
  *
124
- * @template TSchema - The resource schema type
125
- * @param schema - Resource schema for field projection
126
- * @returns New builder with updated output type
127
- *
128
- * @example Tagged resource schema — explicit projection level
131
+ * @example Tagged resource view
129
132
  * ```typescript
130
133
  * procedure()
131
- * .guard(authenticated)
132
- * .expose(UserSchema.authenticated) // returns { id, name, email }
133
- * .query(handler)
134
- * ```
135
- *
136
- * @example Plain resource schema — derives level from guardNarrow
137
- * ```typescript
138
- * procedure()
139
- * .guardNarrow(authenticatedNarrow)
140
- * .expose(UserSchema) // auto-projects based on guard's accessLevel
134
+ * .output(UserSchema.authenticated) // auto-projects to { id, name, email }
141
135
  * .query(handler)
142
136
  * ```
143
137
  */
144
- expose<TSchema extends ResourceSchema>(schema: TSchema): ProcedureBuilder<TInput, TSchema extends TaggedResourceSchema<infer TFields, infer TLevel> ? TLevel extends 'admin' | 'authenticated' | 'public' ? OutputForTag<ResourceSchema<TFields>, LevelToTag<TLevel>> : FilterFieldsByLevel<TFields, TLevel> : TContext extends TaggedContext<infer TTag> ? TTag extends ContextTag ? OutputForTag<TSchema, TTag> : OutputForTag<TSchema, ExtractTag<TContext>> : OutputForTag<TSchema, ExtractTag<TContext>>, TContext>;
138
+ output<TSchema extends ValidOutputSchema>(schema: TSchema): ProcedureBuilder<TInput, InferOutputSchema<TSchema>, TContext, TErrors>;
145
139
  /**
146
140
  * Adds middleware to the procedure chain
147
141
  *
@@ -171,7 +165,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
171
165
  * })
172
166
  * ```
173
167
  */
174
- 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>;
175
169
  /**
176
170
  * Adds an authorization guard to the procedure
177
171
  *
@@ -204,42 +198,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
204
198
  * .mutation(async ({ input, ctx }) => { ... });
205
199
  * ```
206
200
  */
207
- guard<TGuardContext extends Partial<TContext>>(guard: GuardLike<TGuardContext>): ProcedureBuilder<TInput, TOutput, TContext>;
208
- /**
209
- * Adds an authorization guard with type narrowing (EXPERIMENTAL)
210
- *
211
- * Unlike `.guard()`, this method narrows the context type based on
212
- * what the guard guarantees. For example, `authenticatedNarrow` narrows
213
- * `ctx.user` from `User | undefined` to `User`.
214
- *
215
- * **EXPERIMENTAL**: This API may change. Consider using middleware
216
- * for context type extension as the current stable alternative.
217
- *
218
- * @template TNarrowedContext - The context type guaranteed by the guard
219
- * @param guard - Narrowing guard definition with `_narrows` type
220
- * @returns New builder with narrowed context type
221
- *
222
- * @example
223
- * ```typescript
224
- * import { authenticatedNarrow, hasRoleNarrow } from '@veloxts/auth';
225
- *
226
- * // ctx.user is guaranteed non-null after guard passes
227
- * procedure()
228
- * .guardNarrow(authenticatedNarrow)
229
- * .query(({ ctx }) => {
230
- * return { email: ctx.user.email }; // No null check needed!
231
- * });
232
- *
233
- * // Chain multiple narrowing guards
234
- * procedure()
235
- * .guardNarrow(authenticatedNarrow)
236
- * .guardNarrow(hasRoleNarrow('admin'))
237
- * .mutation(({ ctx }) => { ... });
238
- * ```
239
- */
240
- guardNarrow<TNarrowedContext>(guard: GuardLike<Partial<TContext>> & {
241
- readonly _narrows: TNarrowedContext;
242
- }): ProcedureBuilder<TInput, TOutput, TContext & TNarrowedContext>;
201
+ guard<TGuardContext extends Partial<TContext>>(guard: GuardLike<TGuardContext>): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
243
202
  /**
244
203
  * Adds multiple authorization guards at once
245
204
  *
@@ -266,7 +225,62 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
266
225
  * .mutation(async ({ input, ctx }) => { ... });
267
226
  * ```
268
227
  */
269
- 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>;
270
284
  /**
271
285
  * Configures REST route override
272
286
  *
@@ -282,7 +296,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
282
296
  * .rest({ method: 'POST', path: '/users/:id/activate' })
283
297
  * ```
284
298
  */
285
- rest(config: RestRouteOverride): ProcedureBuilder<TInput, TOutput, TContext>;
299
+ rest(config: RestRouteOverride): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
286
300
  /**
287
301
  * Configures the procedure as a webhook endpoint
288
302
  *
@@ -302,7 +316,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
302
316
  * })
303
317
  * ```
304
318
  */
305
- webhook(path: string): ProcedureBuilder<TInput, TOutput, TContext>;
319
+ webhook(path: string): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
306
320
  /**
307
321
  * Marks the procedure as deprecated
308
322
  *
@@ -325,7 +339,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
325
339
  * .query(handler);
326
340
  * ```
327
341
  */
328
- deprecated(message?: string): ProcedureBuilder<TInput, TOutput, TContext>;
342
+ deprecated(message?: string): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
329
343
  /**
330
344
  * Declares a parent resource for nested routes (single level)
331
345
  *
@@ -354,7 +368,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
354
368
  * .query(async ({ input }) => { ... });
355
369
  * ```
356
370
  */
357
- parent(resource: string, param?: string): ProcedureBuilder<TInput, TOutput, TContext>;
371
+ parent(resource: string, param?: string): ProcedureBuilder<TInput, TOutput, TContext, TErrors>;
358
372
  /**
359
373
  * Declares multiple parent resources for deeply nested routes
360
374
  *
@@ -385,7 +399,94 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
385
399
  parents(config: Array<{
386
400
  resource: string;
387
401
  param?: string;
388
- }>): 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>;
389
490
  /**
390
491
  * Finalizes the procedure as a query (read-only operation)
391
492
  *
@@ -393,7 +494,7 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
393
494
  * The handler receives the validated input and context.
394
495
  *
395
496
  * @param handler - The query handler function
396
- * @returns Compiled procedure ready for registration
497
+ * @returns PostHandlerBuilder with all CompiledProcedure fields plus .useAfter()
397
498
  *
398
499
  * @example
399
500
  * ```typescript
@@ -404,15 +505,18 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
404
505
  * })
405
506
  * ```
406
507
  */
407
- query(handler: 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>;
408
509
  /**
409
510
  * Finalizes the procedure as a mutation (write operation)
410
511
  *
411
512
  * Mutations map to POST/PUT/DELETE in REST and can modify data.
412
513
  * The handler receives the validated input and context.
413
514
  *
414
- * @param handler - The mutation handler function
415
- * @returns Compiled procedure ready for registration
515
+ * In Level 3 branching mode (when `.output()` receives an untagged resource schema),
516
+ * accepts a handler map keyed by `[Schema.level.key]` instead of a single handler.
517
+ *
518
+ * @param handler - The mutation handler function or handler map
519
+ * @returns PostHandlerBuilder with all CompiledProcedure fields plus .useAfter()
416
520
  *
417
521
  * @example
418
522
  * ```typescript
@@ -423,22 +527,52 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
423
527
  * })
424
528
  * ```
425
529
  */
426
- mutation(handler: 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> {
427
563
  /**
428
- * @deprecated Use `.expose()` instead. `.resource()` will be removed in v1.0.
564
+ * Registers a post-handler hook that runs after successful handler execution
429
565
  *
430
- * Sets field-level visibility via a resource schema.
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.
431
569
  *
432
- * @example Migration
433
- * ```typescript
434
- * // Before
435
- * procedure().resource(UserSchema.authenticated).query(handler)
570
+ * Multiple `.useAfter()` calls chain in registration order.
436
571
  *
437
- * // After
438
- * procedure().expose(UserSchema.authenticated).query(handler)
439
- * ```
572
+ * @param handler - Post-handler hook function
573
+ * @returns New PostHandlerBuilder with the hook appended
440
574
  */
441
- resource<TSchema extends ResourceSchema>(schema: TSchema): ProcedureBuilder<TInput, TSchema extends TaggedResourceSchema<infer TFields, infer TLevel> ? OutputForTag<ResourceSchema<TFields>, LevelToTag<TLevel>> : TContext extends TaggedContext<infer TTag> ? TTag extends ContextTag ? OutputForTag<TSchema, TTag> : OutputForTag<TSchema, ExtractTag<TContext>> : OutputForTag<TSchema, ExtractTag<TContext>>, TContext>;
575
+ useAfter(handler: AfterHandler<TInput, TOutput, TContext>): PostHandlerBuilder<TInput, TOutput, TContext, TType, TErrors>;
442
576
  }
443
577
  /**
444
578
  * Internal runtime state for the procedure builder
@@ -471,6 +605,28 @@ export interface BuilderRuntimeState {
471
605
  deprecationMessage?: string;
472
606
  /** Whether this procedure is a webhook endpoint (metadata marker) */
473
607
  isWebhook?: boolean;
608
+ /** Whether .output() received an untagged resource schema (Level 3 branching mode) */
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;
474
630
  }
475
631
  /**
476
632
  * Type for the procedures object passed to defineProcedures
@@ -485,7 +641,7 @@ export interface BuilderRuntimeState {
485
641
  * The actual type safety is preserved through InferProcedures<T> which captures
486
642
  * the concrete types at definition time. This `any` only allows the assignment.
487
643
  */
488
- export type ProcedureDefinitions = Record<string, CompiledProcedure<any, any, any, any>>;
644
+ export type ProcedureDefinitions = Record<string, CompiledProcedure<any, any, any, any, any>>;
489
645
  /**
490
646
  * Type helper to preserve procedure types in a collection
491
647
  *
@@ -39,7 +39,7 @@
39
39
  *
40
40
  * // Authenticated endpoint → returns { id, name, email }
41
41
  * getProfile: procedure()
42
- * .guardNarrow(authenticatedNarrow)
42
+ * .guard(authenticated)
43
43
  * .input(z.object({ id: z.string() }))
44
44
  * .query(async ({ input, ctx }) => {
45
45
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
@@ -48,7 +48,7 @@
48
48
  *
49
49
  * // Admin endpoint → returns { id, name, email, internalNotes }
50
50
  * getFullProfile: procedure()
51
- * .guardNarrow(adminNarrow)
51
+ * .guard(hasRole('admin'))
52
52
  * .input(z.object({ id: z.string() }))
53
53
  * .query(async ({ input, ctx }) => {
54
54
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
@@ -60,8 +60,8 @@
60
60
  * @module resource
61
61
  */
62
62
  export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
63
- export type { AccessLevelConfig } from './levels.js';
64
- export { defineAccessLevels } from './levels.js';
63
+ export type { AccessLevelConfig, AccessLevelGuard, AccessLevelGuards } from './levels.js';
64
+ export { defaultAccess, defineAccessLevels } from './levels.js';
65
65
  export type { AdminOutput, AnonymousOutput, AuthenticatedOutput, BuilderField, CustomResourceSchemaWithViews, CustomSchemaBuilder, FilterFieldsByLevel, OutputForLevel, OutputForTag, PublicOutput, RelationField, ResourceField, ResourceSchema, ResourceSchemaWithViews, RuntimeField, TaggedResourceSchema, } from './schema.js';
66
66
  export { isResourceSchema, isTaggedResourceSchema, ResourceSchemaBuilder, resourceSchema, } from './schema.js';
67
67
  export type { AccessLevel, ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, HasTag, LevelToTag, PUBLIC, TaggedContext, TagToLevel, WithTag, } from './tags.js';
@@ -39,7 +39,7 @@
39
39
  *
40
40
  * // Authenticated endpoint → returns { id, name, email }
41
41
  * getProfile: procedure()
42
- * .guardNarrow(authenticatedNarrow)
42
+ * .guard(authenticated)
43
43
  * .input(z.object({ id: z.string() }))
44
44
  * .query(async ({ input, ctx }) => {
45
45
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
@@ -48,7 +48,7 @@
48
48
  *
49
49
  * // Admin endpoint → returns { id, name, email, internalNotes }
50
50
  * getFullProfile: procedure()
51
- * .guardNarrow(adminNarrow)
51
+ * .guard(hasRole('admin'))
52
52
  * .input(z.object({ id: z.string() }))
53
53
  * .query(async ({ input, ctx }) => {
54
54
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
@@ -64,7 +64,7 @@
64
64
  // ============================================================================
65
65
  // Resource instances
66
66
  export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
67
- export { defineAccessLevels } from './levels.js';
67
+ export { defaultAccess, defineAccessLevels } from './levels.js';
68
68
  // Schema builder
69
69
  export { isResourceSchema, isTaggedResourceSchema, ResourceSchemaBuilder, resourceSchema, } from './schema.js';
70
70
  // Visibility
@@ -82,7 +82,7 @@ export declare class Resource<TSchema extends ResourceSchema> {
82
82
  *
83
83
  * @example
84
84
  * ```typescript
85
- * // In a procedure with guardNarrow(authenticatedNarrow)
85
+ * // In a procedure with guard(authenticated)
86
86
  * const profile = resource(user, UserSchema).for(ctx);
87
87
  * // Type is automatically inferred based on ctx's tag
88
88
  * ```
@@ -179,7 +179,7 @@ export class Resource {
179
179
  *
180
180
  * @example
181
181
  * ```typescript
182
- * // In a procedure with guardNarrow(authenticatedNarrow)
182
+ * // In a procedure with guard(authenticated)
183
183
  * const profile = resource(user, UserSchema).for(ctx);
184
184
  * // Type is automatically inferred based on ctx's tag
185
185
  * ```