@veloxts/router 0.6.83 → 0.6.85

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.
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module trpc/adapter
8
8
  */
9
- import type { AnyRouter as TRPCAnyRouter } from '@trpc/server';
9
+ import type { AnyRouter as TRPCAnyRouter, TRPCMutationProcedure, TRPCQueryProcedure } from '@trpc/server';
10
10
  import { TRPCError } from '@trpc/server';
11
11
  import type { BaseContext } from '@veloxts/core';
12
12
  import type { FastifyInstance } from 'fastify';
@@ -17,7 +17,83 @@ import type { FastifyInstance } from 'fastify';
17
17
  * which helps avoid TypeScript compilation memory issues with tRPC v11.7+.
18
18
  */
19
19
  export type AnyRouter = TRPCAnyRouter;
20
- import type { ProcedureCollection } from '../types.js';
20
+ import type { CompiledProcedure, ProcedureCollection, ProcedureRecord } from '../types.js';
21
+ /**
22
+ * Maps a VeloxTS CompiledProcedure to the corresponding tRPC procedure type
23
+ *
24
+ * This preserves the input/output types through the type mapping, enabling
25
+ * proper type inference when using the router on the client.
26
+ *
27
+ * Note: The `meta` field is required by tRPC's BuiltProcedureDef interface.
28
+ * We use `unknown` since VeloxTS doesn't use procedure-level metadata.
29
+ */
30
+ export type MapProcedureToTRPC<T extends CompiledProcedure> = T extends CompiledProcedure<infer TInput, infer TOutput, BaseContext, 'query'> ? TRPCQueryProcedure<{
31
+ input: TInput;
32
+ output: TOutput;
33
+ meta: unknown;
34
+ }> : T extends CompiledProcedure<infer TInput, infer TOutput, BaseContext, 'mutation'> ? TRPCMutationProcedure<{
35
+ input: TInput;
36
+ output: TOutput;
37
+ meta: unknown;
38
+ }> : never;
39
+ /**
40
+ * Maps a ProcedureRecord to a tRPC router record with proper types
41
+ *
42
+ * This preserves each procedure's input/output types through the mapping.
43
+ */
44
+ export type MapProcedureRecordToTRPC<T extends ProcedureRecord> = {
45
+ [K in keyof T]: MapProcedureToTRPC<T[K]>;
46
+ };
47
+ /**
48
+ * Extracts the namespace from a ProcedureCollection
49
+ */
50
+ export type ExtractNamespace<T> = T extends ProcedureCollection<infer N, ProcedureRecord> ? N : never;
51
+ /**
52
+ * Extracts the procedures record from a ProcedureCollection
53
+ */
54
+ export type ExtractProcedures<T> = T extends ProcedureCollection<string, infer P> ? P : never;
55
+ /**
56
+ * Maps a tuple of ProcedureCollections to a tRPC router record
57
+ *
58
+ * This creates a properly typed object where each key is the namespace
59
+ * and each value is the mapped procedure record.
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * type Collections = [
64
+ * ProcedureCollection<'users', { getUser: CompiledProcedure<...> }>,
65
+ * ProcedureCollection<'posts', { listPosts: CompiledProcedure<...> }>
66
+ * ];
67
+ *
68
+ * type Result = CollectionsToRouterRecord<Collections>;
69
+ * // Result = {
70
+ * // users: { getUser: TRPCQueryProcedure<...> },
71
+ * // posts: { listPosts: TRPCQueryProcedure<...> }
72
+ * // }
73
+ * ```
74
+ */
75
+ export type CollectionsToRouterRecord<T extends readonly ProcedureCollection[]> = T extends readonly [] ? object : T extends readonly [infer First extends ProcedureCollection, ...infer Rest] ? Rest extends readonly ProcedureCollection[] ? {
76
+ [K in ExtractNamespace<First>]: MapProcedureRecordToTRPC<ExtractProcedures<First>>;
77
+ } & CollectionsToRouterRecord<Rest> : {
78
+ [K in ExtractNamespace<First>]: MapProcedureRecordToTRPC<ExtractProcedures<First>>;
79
+ } : object;
80
+ /**
81
+ * Infers the complete router type from procedure collections
82
+ *
83
+ * This is the main type that should be used for `export type AppRouter = ...`
84
+ * to ensure full type preservation for the tRPC client.
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const collections = [userProcedures, postProcedures] as const;
89
+ * const router = await rpc(app, collections);
90
+ * export type AppRouter = InferRouterFromCollections<typeof collections>;
91
+ *
92
+ * // Client usage:
93
+ * // client.users.getUser({ id: '123' }) // Fully typed!
94
+ * ```
95
+ */
96
+ export type InferRouterFromCollections<T extends readonly ProcedureCollection[]> = CollectionsToRouterRecord<T>;
21
97
  declare const baseTRPC: import("@trpc/server").TRPCRootObject<BaseContext, object, import("@trpc/server").TRPCRuntimeConfigOptions<BaseContext, object>, {
22
98
  ctx: BaseContext;
23
99
  meta: object;
@@ -73,10 +149,11 @@ export declare function buildTRPCRouter(t: TRPCInstance<BaseContext>, collection
73
149
  * Create a namespaced app router from multiple procedure collections
74
150
  *
75
151
  * Each collection becomes a nested router under its namespace.
152
+ * Use `as const` on the collections array to preserve literal types.
76
153
  *
77
154
  * @param t - tRPC instance
78
- * @param collections - Array of procedure collections
79
- * @returns Merged app router
155
+ * @param collections - Array of procedure collections (use `as const` for best type inference)
156
+ * @returns Merged app router with preserved types
80
157
  *
81
158
  * @example
82
159
  * ```typescript
@@ -84,21 +161,62 @@ export declare function buildTRPCRouter(t: TRPCInstance<BaseContext>, collection
84
161
  * const router = appRouter(t, [
85
162
  * userProcedures, // namespace: 'users'
86
163
  * postProcedures, // namespace: 'posts'
87
- * ]);
164
+ * ] as const);
88
165
  *
89
166
  * // Usage:
90
167
  * // router.users.getUser({ id: '123' })
91
168
  * // router.posts.listPosts({ page: 1 })
92
169
  *
93
- * // Export type for client
170
+ * // Export type for client - fully typed!
94
171
  * export type AppRouter = typeof router;
95
172
  * ```
96
173
  */
97
- export declare function appRouter(t: TRPCInstance<BaseContext>, collections: ProcedureCollection[]): AnyRouter;
174
+ export declare function appRouter<const T extends readonly ProcedureCollection[]>(t: TRPCInstance<BaseContext>, collections: T): AnyRouter & InferRouterFromCollections<T>;
98
175
  /**
99
- * Helper type to infer the AppRouter type
176
+ * Helper type to infer the AppRouter type from procedure collections
177
+ *
178
+ * @deprecated Use `InferRouterFromCollections<T>` instead for better type inference.
179
+ * This type alias is kept for backward compatibility.
100
180
  */
101
- export type InferAppRouter = AnyRouter;
181
+ export type InferAppRouter<T extends readonly ProcedureCollection[] = readonly ProcedureCollection[]> = InferRouterFromCollections<T>;
182
+ /**
183
+ * Converts a VeloxTS router to a tRPC-compatible type for `@trpc/react-query`.
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * import { createTRPCReact } from '@trpc/react-query';
188
+ * import type { TRPCRouter } from '@veloxts/router';
189
+ * import type { AppRouter } from '../api';
190
+ *
191
+ * export const trpc = createTRPCReact<TRPCRouter<AppRouter>>();
192
+ * ```
193
+ *
194
+ * @see {@link InferRouterFromCollections} for transforming collections array types
195
+ */
196
+ export type TRPCRouter<TRouter> = TRouter extends Record<string, ProcedureCollection<string, ProcedureRecord>> ? TRPCRouterBase & {
197
+ [K in keyof TRouter]: TRouter[K] extends ProcedureCollection<string, infer P extends ProcedureRecord> ? MapProcedureRecordToTRPC<P> : never;
198
+ } : never;
199
+ /**
200
+ * @deprecated Use `TRPCRouter` instead. Will be removed in v1.0.
201
+ */
202
+ export type AsTRPCRouter<TRouter> = TRPCRouter<TRouter>;
203
+ /**
204
+ * tRPC router structural requirements for type compatibility.
205
+ * @internal
206
+ */
207
+ type TRPCRouterBase = {
208
+ _def: {
209
+ _config: {
210
+ $types: {
211
+ ctx: BaseContext;
212
+ meta: unknown;
213
+ };
214
+ };
215
+ record: Record<string, unknown>;
216
+ procedures: Record<string, unknown>;
217
+ };
218
+ createCaller: (ctx: unknown) => unknown;
219
+ };
102
220
  /**
103
221
  * Create a tRPC context factory for Fastify
104
222
  *
@@ -126,6 +244,17 @@ export declare function createTRPCContextFactory(): ({ req }: {
126
244
  context?: BaseContext;
127
245
  };
128
246
  }) => BaseContext;
247
+ /**
248
+ * Cause structure for VeloxTS errors converted to tRPC errors
249
+ */
250
+ export interface VeloxTRPCCause {
251
+ /** Discriminator for VeloxTS errors */
252
+ readonly source: 'velox';
253
+ /** VeloxTS error code */
254
+ readonly code?: string;
255
+ /** Guard name (only present for GuardError) */
256
+ readonly guardName?: string;
257
+ }
129
258
  /**
130
259
  * Convert a VeloxTS error to a tRPC error
131
260
  *
@@ -147,7 +276,7 @@ export declare function veloxErrorToTRPCError(error: Error & {
147
276
  * using veloxErrorToTRPCError().
148
277
  */
149
278
  export declare function isVeloxTRPCError(error: TRPCError): error is TRPCError & {
150
- cause: string;
279
+ cause: VeloxTRPCCause;
151
280
  };
152
281
  /**
153
282
  * Options for tRPC plugin registration
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { initTRPC, TRPCError } from '@trpc/server';
10
10
  import { isGuardError } from '../errors.js';
11
+ import { executeMiddlewareChain } from '../middleware/chain.js';
11
12
  // ============================================================================
12
13
  // tRPC Initialization
13
14
  // ============================================================================
@@ -74,33 +75,23 @@ export function buildTRPCRouter(t, collection) {
74
75
  * @internal
75
76
  */
76
77
  function buildTRPCProcedure(t, procedure) {
77
- // Start with base procedure builder
78
- const baseProcedure = t.procedure;
79
- // Build the procedure chain based on configuration
80
- if (procedure.inputSchema && procedure.outputSchema) {
81
- // Both input and output schemas
82
- const withInput = baseProcedure.input(procedure.inputSchema);
83
- const withOutput = withInput.output(procedure.outputSchema);
84
- const handler = createHandler(procedure);
85
- return procedure.type === 'query' ? withOutput.query(handler) : withOutput.mutation(handler);
86
- }
78
+ // Build the procedure chain incrementally
79
+ // biome-ignore lint/suspicious/noExplicitAny: tRPC procedure builder has complex types that vary by chain state
80
+ let builder = t.procedure;
81
+ // Add input schema if present
87
82
  if (procedure.inputSchema) {
88
- // Only input schema
89
- const withInput = baseProcedure.input(procedure.inputSchema);
90
- const handler = createHandler(procedure);
91
- return procedure.type === 'query' ? withInput.query(handler) : withInput.mutation(handler);
83
+ builder = builder.input(procedure.inputSchema);
92
84
  }
85
+ // Add output schema if present
93
86
  if (procedure.outputSchema) {
94
- // Only output schema
95
- const withOutput = baseProcedure.output(procedure.outputSchema);
96
- const handler = createNoInputHandler(procedure);
97
- return procedure.type === 'query' ? withOutput.query(handler) : withOutput.mutation(handler);
87
+ builder = builder.output(procedure.outputSchema);
98
88
  }
99
- // No schemas - use base procedure
100
- const handler = createNoInputHandler(procedure);
101
- return procedure.type === 'query'
102
- ? baseProcedure.query(handler)
103
- : baseProcedure.mutation(handler);
89
+ // Select handler based on whether input is expected
90
+ const handler = procedure.inputSchema
91
+ ? createHandler(procedure)
92
+ : createNoInputHandler(procedure);
93
+ // Finalize as query or mutation
94
+ return procedure.type === 'query' ? builder.query(handler) : builder.mutation(handler);
104
95
  }
105
96
  /**
106
97
  * Create a handler function for a procedure with input
@@ -141,31 +132,7 @@ function createNoInputHandler(procedure) {
141
132
  * @internal
142
133
  */
143
134
  async function executeWithMiddleware(procedure, input, ctx) {
144
- // Build middleware chain from end to start
145
- let next = async () => {
146
- const output = await procedure.handler({ input, ctx });
147
- return { output };
148
- };
149
- // Wrap each middleware from last to first
150
- for (let i = procedure.middlewares.length - 1; i >= 0; i--) {
151
- const middleware = procedure.middlewares[i];
152
- const currentNext = next;
153
- next = async () => {
154
- return middleware({
155
- input,
156
- ctx,
157
- next: async (opts) => {
158
- // Allow middleware to extend context
159
- if (opts?.ctx) {
160
- Object.assign(ctx, opts.ctx);
161
- }
162
- return currentNext();
163
- },
164
- });
165
- };
166
- }
167
- const result = await next();
168
- return result.output;
135
+ return executeMiddlewareChain(procedure.middlewares, input, ctx, async () => procedure.handler({ input, ctx }));
169
136
  }
170
137
  // ============================================================================
171
138
  // App Router Creation
@@ -174,10 +141,11 @@ async function executeWithMiddleware(procedure, input, ctx) {
174
141
  * Create a namespaced app router from multiple procedure collections
175
142
  *
176
143
  * Each collection becomes a nested router under its namespace.
144
+ * Use `as const` on the collections array to preserve literal types.
177
145
  *
178
146
  * @param t - tRPC instance
179
- * @param collections - Array of procedure collections
180
- * @returns Merged app router
147
+ * @param collections - Array of procedure collections (use `as const` for best type inference)
148
+ * @returns Merged app router with preserved types
181
149
  *
182
150
  * @example
183
151
  * ```typescript
@@ -185,13 +153,13 @@ async function executeWithMiddleware(procedure, input, ctx) {
185
153
  * const router = appRouter(t, [
186
154
  * userProcedures, // namespace: 'users'
187
155
  * postProcedures, // namespace: 'posts'
188
- * ]);
156
+ * ] as const);
189
157
  *
190
158
  * // Usage:
191
159
  * // router.users.getUser({ id: '123' })
192
160
  * // router.posts.listPosts({ page: 1 })
193
161
  *
194
- * // Export type for client
162
+ * // Export type for client - fully typed!
195
163
  * export type AppRouter = typeof router;
196
164
  * ```
197
165
  */
@@ -239,9 +207,6 @@ export function createTRPCContextFactory() {
239
207
  return req.context;
240
208
  };
241
209
  }
242
- // ============================================================================
243
- // Error Utilities
244
- // ============================================================================
245
210
  /**
246
211
  * Convert a VeloxTS error to a tRPC error
247
212
  *
@@ -268,19 +233,25 @@ export function veloxErrorToTRPCError(error, defaultCode = 'INTERNAL_SERVER_ERRO
268
233
  const trpcCode = error.statusCode ? (statusToTRPC[error.statusCode] ?? defaultCode) : defaultCode;
269
234
  // Handle GuardError specifically to preserve guard metadata
270
235
  if (isGuardError(error)) {
236
+ const cause = {
237
+ source: 'velox',
238
+ code: error.code,
239
+ guardName: error.guardName,
240
+ };
271
241
  return new TRPCError({
272
242
  code: trpcCode,
273
243
  message: error.message,
274
- cause: {
275
- code: error.code,
276
- guardName: error.guardName,
277
- },
244
+ cause,
278
245
  });
279
246
  }
247
+ const cause = {
248
+ source: 'velox',
249
+ code: error.code,
250
+ };
280
251
  return new TRPCError({
281
252
  code: trpcCode,
282
253
  message: error.message,
283
- cause: error.code,
254
+ cause,
284
255
  });
285
256
  }
286
257
  /**
@@ -290,7 +261,8 @@ export function veloxErrorToTRPCError(error, defaultCode = 'INTERNAL_SERVER_ERRO
290
261
  * using veloxErrorToTRPCError().
291
262
  */
292
263
  export function isVeloxTRPCError(error) {
293
- return typeof error.cause === 'string';
264
+ const cause = error.cause;
265
+ return (cause != null && typeof cause === 'object' && 'source' in cause && cause.source === 'velox');
294
266
  }
295
267
  /**
296
268
  * Register tRPC plugin with Fastify server
@@ -3,5 +3,7 @@
3
3
  *
4
4
  * @module trpc
5
5
  */
6
- export type { AnyRouter, InferAppRouter, TRPCInstance, TRPCPluginOptions } from './adapter.js';
6
+ export type { AnyRouter,
7
+ /** @deprecated Use `TRPCRouter` instead */
8
+ AsTRPCRouter, CollectionsToRouterRecord, ExtractNamespace, ExtractProcedures, InferAppRouter, InferRouterFromCollections, MapProcedureRecordToTRPC, MapProcedureToTRPC, TRPCInstance, TRPCPluginOptions, TRPCRouter, } from './adapter.js';
7
9
  export { appRouter, buildTRPCRouter, createTRPCContextFactory, isVeloxTRPCError, registerTRPCPlugin, trpc, veloxErrorToTRPCError, } from './adapter.js';
package/dist/types.d.ts CHANGED
@@ -193,7 +193,7 @@ export interface RestRouteOverride {
193
193
  /**
194
194
  * Parent resource configuration for nested routes
195
195
  *
196
- * Defines the parent resource relationship for generating nested REST routes
196
+ * Defines a single parent resource relationship for generating nested REST routes
197
197
  * like `/posts/:postId/comments/:id`.
198
198
  *
199
199
  * @example
@@ -207,16 +207,39 @@ export interface RestRouteOverride {
207
207
  */
208
208
  export interface ParentResourceConfig {
209
209
  /**
210
- * Parent resource namespace (e.g., 'posts', 'users')
211
- * Used to build the parent path segment: `/${namespace}/:${paramName}`
210
+ * Parent resource name (e.g., 'posts', 'users')
211
+ * Used to build the parent path segment: `/${resource}/:${param}`
212
212
  */
213
- readonly namespace: string;
213
+ readonly resource: string;
214
214
  /**
215
215
  * Parent resource parameter name in the path
216
- * Defaults to `${singularNamespace}Id` if not specified
216
+ * Defaults to `${singularResource}Id` if not specified
217
217
  * (e.g., 'posts' -> 'postId', 'users' -> 'userId')
218
218
  */
219
- readonly paramName: string;
219
+ readonly param: string;
220
+ }
221
+ /**
222
+ * Multi-level parent resource configuration for deeply nested routes
223
+ *
224
+ * Defines multiple parent resources for generating deeply nested REST routes
225
+ * like `/organizations/:orgId/projects/:projectId/tasks/:id`.
226
+ *
227
+ * @example
228
+ * ```typescript
229
+ * // Multi-level nesting
230
+ * procedure().parents([
231
+ * { resource: 'organizations', param: 'orgId' },
232
+ * { resource: 'projects', param: 'projectId' },
233
+ * ])
234
+ * // Generates: /organizations/:orgId/projects/:projectId/tasks/:id
235
+ * ```
236
+ */
237
+ export interface ParentResourceChain {
238
+ /**
239
+ * Array of parent resources from outermost to innermost
240
+ * E.g., [organizations, projects] for /organizations/:orgId/projects/:projectId/tasks
241
+ */
242
+ readonly parents: readonly ParentResourceConfig[];
220
243
  }
221
244
  /**
222
245
  * Compiled procedure with all metadata and handlers
@@ -249,7 +272,7 @@ export interface CompiledProcedure<TInput = unknown, TOutput = unknown, TContext
249
272
  /** REST route override (if specified) */
250
273
  readonly restOverride?: RestRouteOverride;
251
274
  /**
252
- * Parent resource configuration for nested routes
275
+ * Parent resource configuration for nested routes (single level)
253
276
  *
254
277
  * When specified, the REST path will be prefixed with the parent resource:
255
278
  * `/${parent.namespace}/:${parent.paramName}/${childNamespace}/:id`
@@ -262,6 +285,23 @@ export interface CompiledProcedure<TInput = unknown, TOutput = unknown, TContext
262
285
  * ```
263
286
  */
264
287
  readonly parentResource?: ParentResourceConfig;
288
+ /**
289
+ * Multi-level parent resource configuration for deeply nested routes
290
+ *
291
+ * When specified, the REST path will be prefixed with all parent resources:
292
+ * `/${parent1.namespace}/:${parent1.paramName}/${parent2.namespace}/:${parent2.paramName}/...`
293
+ *
294
+ * @example
295
+ * ```typescript
296
+ * // With parentResources: [
297
+ * // { namespace: 'organizations', paramName: 'orgId' },
298
+ * // { namespace: 'projects', paramName: 'projectId' }
299
+ * // ]
300
+ * // and namespace: 'tasks'
301
+ * // Generates: /organizations/:orgId/projects/:projectId/tasks/:id
302
+ * ```
303
+ */
304
+ readonly parentResources?: readonly ParentResourceConfig[];
265
305
  /**
266
306
  * Pre-compiled middleware chain executor
267
307
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veloxts/router",
3
- "version": "0.6.83",
3
+ "version": "0.6.85",
4
4
  "description": "Procedure definitions with tRPC and REST routing for VeloxTS framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,8 +40,8 @@
40
40
  "@trpc/server": "11.8.0",
41
41
  "fastify": "5.6.2",
42
42
  "zod-to-json-schema": "3.24.5",
43
- "@veloxts/core": "0.6.83",
44
- "@veloxts/validation": "0.6.83"
43
+ "@veloxts/core": "0.6.85",
44
+ "@veloxts/validation": "0.6.85"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@vitest/coverage-v8": "4.0.16",