@veloxts/router 0.6.84 → 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.
@@ -9,10 +9,9 @@
9
9
  */
10
10
  import { ConfigurationError } from '@veloxts/core';
11
11
  import { executeProcedure } from '../procedure/builder.js';
12
- import { buildNestedRestPath, buildRestPath, parseNamingConvention } from './naming.js';
13
- // ============================================================================
14
- // Route Generation
15
- // ============================================================================
12
+ import { buildMultiLevelNestedPath, buildNestedRestPath, buildRestPath, calculateNestingDepth, parseNamingConvention, } from './naming.js';
13
+ /** Default nesting depth threshold for warnings */
14
+ const NESTING_DEPTH_WARNING_THRESHOLD = 3;
16
15
  /**
17
16
  * Generate REST routes from a procedure collection
18
17
  *
@@ -22,23 +21,90 @@ import { buildNestedRestPath, buildRestPath, parseNamingConvention } from './nam
22
21
  * 3. Skipping if neither applies (tRPC-only procedure)
23
22
  *
24
23
  * @param collection - Procedure collection to generate routes from
24
+ * @param options - Optional route generation options
25
25
  * @returns Array of REST route definitions
26
26
  */
27
- export function generateRestRoutes(collection) {
27
+ export function generateRestRoutes(collection, options = {}) {
28
28
  const routes = [];
29
+ const { shortcuts = false, nestingWarnings = true } = options;
29
30
  for (const [name, procedure] of Object.entries(collection.procedures)) {
30
31
  const route = generateRouteForProcedure(name, procedure, collection.namespace);
31
32
  if (route) {
32
33
  routes.push(route);
34
+ // Check nesting depth and warn if too deep
35
+ if (nestingWarnings) {
36
+ const depth = calculateNestingDepth(procedure.parentResource, procedure.parentResources);
37
+ if (depth >= NESTING_DEPTH_WARNING_THRESHOLD) {
38
+ console.warn(`⚠️ Resource '${collection.namespace}/${name}' has ${depth} levels of nesting. ` +
39
+ `Consider using shortcuts: true or restructuring your API.`);
40
+ }
41
+ }
42
+ // Generate shortcut route if enabled and route is nested with ID parameter
43
+ if (shortcuts && isNestedRoute(procedure) && route.path.endsWith('/:id')) {
44
+ const shortcutRoute = generateFlatRoute(route, collection.namespace);
45
+ if (shortcutRoute) {
46
+ routes.push(shortcutRoute);
47
+ }
48
+ }
33
49
  }
34
50
  }
35
51
  return routes;
36
52
  }
53
+ /**
54
+ * Check if a procedure has parent resources (is nested)
55
+ */
56
+ function isNestedRoute(procedure) {
57
+ return (procedure.parentResource !== undefined ||
58
+ (procedure.parentResources !== undefined && procedure.parentResources.length > 0));
59
+ }
60
+ /**
61
+ * Generate a shortcut route for a nested route
62
+ *
63
+ * Only generates shortcuts for single-resource operations (with :id).
64
+ * Collection operations require parent context and are not suitable for shortcuts.
65
+ */
66
+ function generateFlatRoute(nestedRoute, namespace) {
67
+ // Only generate shortcuts for operations with :id (single resource)
68
+ if (!nestedRoute.path.endsWith('/:id')) {
69
+ return undefined;
70
+ }
71
+ // Build shortcut path: /{namespace}/:id
72
+ const shortcutPath = `/${namespace}/:id`;
73
+ return {
74
+ method: nestedRoute.method,
75
+ path: shortcutPath,
76
+ procedureName: `${nestedRoute.procedureName}Shortcut`,
77
+ procedure: nestedRoute.procedure,
78
+ };
79
+ }
80
+ /**
81
+ * Build the REST path for a procedure based on its nesting configuration
82
+ *
83
+ * Handles:
84
+ * - Flat routes: /users/:id
85
+ * - Single-level nested: /posts/:postId/comments/:id
86
+ * - Multi-level nested: /organizations/:orgId/projects/:projectId/tasks/:id
87
+ *
88
+ * @internal
89
+ */
90
+ function buildProcedurePath(procedure, namespace, mapping) {
91
+ // Multi-level nesting takes precedence
92
+ if (procedure.parentResources && procedure.parentResources.length > 0) {
93
+ return buildMultiLevelNestedPath(procedure.parentResources, namespace, mapping);
94
+ }
95
+ // Single-level nesting
96
+ if (procedure.parentResource) {
97
+ return buildNestedRestPath(procedure.parentResource, namespace, mapping);
98
+ }
99
+ // Flat route
100
+ return buildRestPath(namespace, mapping);
101
+ }
37
102
  /**
38
103
  * Generate a REST route for a single procedure
39
104
  *
40
- * Handles both flat routes (e.g., /users/:id) and nested routes
41
- * (e.g., /posts/:postId/comments/:id) when a parent resource is configured.
105
+ * Handles flat routes (e.g., /users/:id), single-level nested routes
106
+ * (e.g., /posts/:postId/comments/:id), and multi-level nested routes
107
+ * (e.g., /organizations/:orgId/projects/:projectId/tasks/:id).
42
108
  *
43
109
  * @internal
44
110
  */
@@ -60,11 +126,8 @@ function generateRouteForProcedure(name, procedure, namespace) {
60
126
  // Partial override - try to fill in missing parts from convention
61
127
  const convention = parseNamingConvention(name, procedure.type);
62
128
  if (convention) {
63
- // Build path based on whether there's a parent resource
64
- const path = override.path ??
65
- (procedure.parentResource
66
- ? buildNestedRestPath(procedure.parentResource, namespace, convention)
67
- : buildRestPath(namespace, convention));
129
+ // Build path based on nesting configuration
130
+ const path = override.path ?? buildProcedurePath(procedure, namespace, convention);
68
131
  return {
69
132
  method: override.method ?? convention.method,
70
133
  path,
@@ -78,10 +141,8 @@ function generateRouteForProcedure(name, procedure, namespace) {
78
141
  // Try to infer from naming convention
79
142
  const mapping = parseNamingConvention(name, procedure.type);
80
143
  if (mapping) {
81
- // Build path based on whether there's a parent resource
82
- const path = procedure.parentResource
83
- ? buildNestedRestPath(procedure.parentResource, namespace, mapping)
84
- : buildRestPath(namespace, mapping);
144
+ // Build path based on nesting configuration
145
+ const path = buildProcedurePath(procedure, namespace, mapping);
85
146
  return {
86
147
  method: mapping.method,
87
148
  path,
@@ -143,32 +204,34 @@ function isPlainObject(value) {
143
204
  * Gather input data from the request based on HTTP method
144
205
  *
145
206
  * - GET: Merge params and query
146
- * - POST: Use body, but merge params for nested routes (parent ID in URL)
207
+ * - POST: Use body, but merge params for nested routes (parent IDs in URL)
147
208
  * - PUT/PATCH: Merge params (for ID) and body (for data)
148
209
  * - DELETE: Merge params and query (no body per REST conventions)
149
210
  *
150
- * For nested routes (e.g., /posts/:postId/comments), the parent param
151
- * is extracted from the URL and merged with the body/query as appropriate.
211
+ * For nested routes (e.g., /posts/:postId/comments or
212
+ * /organizations/:orgId/projects/:projectId/tasks), all parent params
213
+ * are extracted from the URL and merged with the body/query as appropriate.
152
214
  */
153
215
  function gatherInput(request, route) {
154
216
  const params = isPlainObject(request.params) ? request.params : {};
155
217
  const query = isPlainObject(request.query) ? request.query : {};
156
218
  const body = isPlainObject(request.body) ? request.body : {};
157
- // Check if this is a nested route (has parent resource)
158
- const hasParentResource = route.procedure.parentResource !== undefined;
219
+ // Check if this is a nested route (has single parent or multiple parents)
220
+ const hasParentResource = route.procedure.parentResource !== undefined ||
221
+ (route.procedure.parentResources !== undefined && route.procedure.parentResources.length > 0);
159
222
  switch (route.method) {
160
223
  case 'GET':
161
- // GET: params (for :id and parent params) + query (for filters/pagination)
224
+ // GET: params (for :id and all parent params) + query (for filters/pagination)
162
225
  return { ...params, ...query };
163
226
  case 'DELETE':
164
- // DELETE: params (for :id and parent params) + query (for options), no body per REST conventions
227
+ // DELETE: params (for :id and all parent params) + query (for options), no body per REST conventions
165
228
  return { ...params, ...query };
166
229
  case 'PUT':
167
230
  case 'PATCH':
168
- // PUT/PATCH: params (for :id and parent params) + body (for data)
231
+ // PUT/PATCH: params (for :id and all parent params) + body (for data)
169
232
  return { ...params, ...body };
170
233
  case 'POST':
171
- // POST: For nested routes, merge params (for parent ID) with body
234
+ // POST: For nested routes, merge params (for all parent IDs) with body
172
235
  // For flat routes, use body only (no ID in params for creates)
173
236
  if (hasParentResource) {
174
237
  return { ...params, ...body };
@@ -244,9 +307,13 @@ function getContextFromRequest(request) {
244
307
  * ```
245
308
  */
246
309
  export function registerRestRoutes(server, collections, options = {}) {
247
- const { prefix = '/api', _prefixHandledByFastify = false } = options;
310
+ const { prefix = '/api', _prefixHandledByFastify = false, shortcuts = false, nestingWarnings = true, } = options;
311
+ const routeGenOptions = {
312
+ shortcuts,
313
+ nestingWarnings,
314
+ };
248
315
  for (const collection of collections) {
249
- const routes = generateRestRoutes(collection);
316
+ const routes = generateRestRoutes(collection, routeGenOptions);
250
317
  for (const route of routes) {
251
318
  // When used with server.register(), Fastify handles the prefix automatically.
252
319
  // When used in legacy mode, we prepend the prefix manually.
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * @module rest
5
5
  */
6
- export type { RestAdapterOptions, RestPlugin, RestRoute } from './adapter.js';
6
+ export type { GenerateRestRoutesOptions, RestAdapterOptions, RestPlugin, RestRoute, } from './adapter.js';
7
7
  export { generateRestRoutes, getRouteSummary, registerRestRoutes, rest, } from './adapter.js';
8
8
  export type { RestMapping } from './naming.js';
9
- export { buildNestedRestPath, buildRestPath, followsNamingConvention, inferResourceName, parseNamingConvention, } from './naming.js';
9
+ export { buildMultiLevelNestedPath, buildNestedRestPath, buildRestPath, calculateNestingDepth, followsNamingConvention, inferResourceName, parseNamingConvention, } from './naming.js';
10
10
  export type { ExtractRoutesType, RouteEntry, RouteMap } from './routes.js';
11
11
  export { extractRoutes } from './routes.js';
@@ -4,5 +4,5 @@
4
4
  * @module rest
5
5
  */
6
6
  export { generateRestRoutes, getRouteSummary, registerRestRoutes, rest, } from './adapter.js';
7
- export { buildNestedRestPath, buildRestPath, followsNamingConvention, inferResourceName, parseNamingConvention, } from './naming.js';
7
+ export { buildMultiLevelNestedPath, buildNestedRestPath, buildRestPath, calculateNestingDepth, followsNamingConvention, inferResourceName, parseNamingConvention, } from './naming.js';
8
8
  export { extractRoutes } from './routes.js';
@@ -59,7 +59,7 @@ export declare function parseNamingConvention(name: string, type: ProcedureType)
59
59
  */
60
60
  export declare function buildRestPath(namespace: string, mapping: RestMapping): string;
61
61
  /**
62
- * Build a nested REST path with parent resource prefix
62
+ * Build a nested REST path with parent resource prefix (single level)
63
63
  *
64
64
  * Creates paths like `/posts/:postId/comments/:id` for nested resources.
65
65
  *
@@ -70,7 +70,7 @@ export declare function buildRestPath(namespace: string, mapping: RestMapping):
70
70
  *
71
71
  * @example
72
72
  * ```typescript
73
- * const parent = { namespace: 'posts', paramName: 'postId' };
73
+ * const parent = { resource: 'posts', param: 'postId' };
74
74
  *
75
75
  * buildNestedRestPath(parent, 'comments', { method: 'GET', path: '/:id', hasIdParam: true })
76
76
  * // Returns: '/posts/:postId/comments/:id'
@@ -83,6 +83,42 @@ export declare function buildRestPath(namespace: string, mapping: RestMapping):
83
83
  * ```
84
84
  */
85
85
  export declare function buildNestedRestPath(parentResource: ParentResourceConfig, childNamespace: string, mapping: RestMapping): string;
86
+ /**
87
+ * Build a deeply nested REST path with multiple parent resources
88
+ *
89
+ * Creates paths like `/organizations/:orgId/projects/:projectId/tasks/:id`
90
+ * for deeply nested resources.
91
+ *
92
+ * @param parentResources - Array of parent resource configurations (outermost to innermost)
93
+ * @param childNamespace - Child resource namespace (e.g., 'tasks')
94
+ * @param mapping - REST mapping from parseNamingConvention
95
+ * @returns Full deeply nested path
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const parents = [
100
+ * { resource: 'organizations', param: 'orgId' },
101
+ * { resource: 'projects', param: 'projectId' },
102
+ * ];
103
+ *
104
+ * buildMultiLevelNestedPath(parents, 'tasks', { method: 'GET', path: '/:id', hasIdParam: true })
105
+ * // Returns: '/organizations/:orgId/projects/:projectId/tasks/:id'
106
+ *
107
+ * buildMultiLevelNestedPath(parents, 'tasks', { method: 'GET', path: '/', hasIdParam: false })
108
+ * // Returns: '/organizations/:orgId/projects/:projectId/tasks'
109
+ * ```
110
+ */
111
+ export declare function buildMultiLevelNestedPath(parentResources: readonly ParentResourceConfig[], childNamespace: string, mapping: RestMapping): string;
112
+ /**
113
+ * Calculate the nesting depth of a route
114
+ *
115
+ * Returns the number of parent levels plus 1 for the resource itself.
116
+ *
117
+ * @param parentResource - Single parent resource (optional)
118
+ * @param parentResources - Multiple parent resources (optional)
119
+ * @returns The total nesting depth (1 = flat, 2 = one parent, 3+ = deeply nested)
120
+ */
121
+ export declare function calculateNestingDepth(parentResource?: ParentResourceConfig, parentResources?: readonly ParentResourceConfig[]): number;
86
122
  /**
87
123
  * Infer the resource name from a procedure name
88
124
  *
@@ -140,6 +140,16 @@ export function parseNamingConvention(name, type) {
140
140
  // No convention matched
141
141
  return undefined;
142
142
  }
143
+ /**
144
+ * Append the mapping suffix to a base path
145
+ *
146
+ * @param basePath - The base path (e.g., '/users', '/posts/:postId/comments')
147
+ * @param mapping - REST mapping containing the path suffix
148
+ * @returns The complete path with suffix appended (if not just '/')
149
+ */
150
+ function appendMappingSuffix(basePath, mapping) {
151
+ return mapping.path === '/' ? basePath : `${basePath}${mapping.path}`;
152
+ }
143
153
  /**
144
154
  * Build the full REST path from namespace and mapping
145
155
  *
@@ -157,16 +167,10 @@ export function parseNamingConvention(name, type) {
157
167
  * ```
158
168
  */
159
169
  export function buildRestPath(namespace, mapping) {
160
- const basePath = `/${namespace}`;
161
- // If path is just '/', return the base path without trailing slash
162
- if (mapping.path === '/') {
163
- return basePath;
164
- }
165
- // Otherwise append the path (e.g., '/:id')
166
- return `${basePath}${mapping.path}`;
170
+ return appendMappingSuffix(`/${namespace}`, mapping);
167
171
  }
168
172
  /**
169
- * Build a nested REST path with parent resource prefix
173
+ * Build a nested REST path with parent resource prefix (single level)
170
174
  *
171
175
  * Creates paths like `/posts/:postId/comments/:id` for nested resources.
172
176
  *
@@ -177,7 +181,7 @@ export function buildRestPath(namespace, mapping) {
177
181
  *
178
182
  * @example
179
183
  * ```typescript
180
- * const parent = { namespace: 'posts', paramName: 'postId' };
184
+ * const parent = { resource: 'posts', param: 'postId' };
181
185
  *
182
186
  * buildNestedRestPath(parent, 'comments', { method: 'GET', path: '/:id', hasIdParam: true })
183
187
  * // Returns: '/posts/:postId/comments/:id'
@@ -190,16 +194,59 @@ export function buildRestPath(namespace, mapping) {
190
194
  * ```
191
195
  */
192
196
  export function buildNestedRestPath(parentResource, childNamespace, mapping) {
193
- // Build parent path segment: /{parentNamespace}/:{parentParamName}
194
- const parentPath = `/${parentResource.namespace}/:${parentResource.paramName}`;
195
- // Build child path segment
196
- const childBasePath = `/${childNamespace}`;
197
- // If mapping path is just '/', don't add extra suffix
198
- if (mapping.path === '/') {
199
- return `${parentPath}${childBasePath}`;
197
+ const parentPath = `/${parentResource.resource}/:${parentResource.param}`;
198
+ const basePath = `${parentPath}/${childNamespace}`;
199
+ return appendMappingSuffix(basePath, mapping);
200
+ }
201
+ /**
202
+ * Build a deeply nested REST path with multiple parent resources
203
+ *
204
+ * Creates paths like `/organizations/:orgId/projects/:projectId/tasks/:id`
205
+ * for deeply nested resources.
206
+ *
207
+ * @param parentResources - Array of parent resource configurations (outermost to innermost)
208
+ * @param childNamespace - Child resource namespace (e.g., 'tasks')
209
+ * @param mapping - REST mapping from parseNamingConvention
210
+ * @returns Full deeply nested path
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * const parents = [
215
+ * { resource: 'organizations', param: 'orgId' },
216
+ * { resource: 'projects', param: 'projectId' },
217
+ * ];
218
+ *
219
+ * buildMultiLevelNestedPath(parents, 'tasks', { method: 'GET', path: '/:id', hasIdParam: true })
220
+ * // Returns: '/organizations/:orgId/projects/:projectId/tasks/:id'
221
+ *
222
+ * buildMultiLevelNestedPath(parents, 'tasks', { method: 'GET', path: '/', hasIdParam: false })
223
+ * // Returns: '/organizations/:orgId/projects/:projectId/tasks'
224
+ * ```
225
+ */
226
+ export function buildMultiLevelNestedPath(parentResources, childNamespace, mapping) {
227
+ const parentSegments = parentResources
228
+ .map((parent) => `/${parent.resource}/:${parent.param}`)
229
+ .join('');
230
+ const basePath = `${parentSegments}/${childNamespace}`;
231
+ return appendMappingSuffix(basePath, mapping);
232
+ }
233
+ /**
234
+ * Calculate the nesting depth of a route
235
+ *
236
+ * Returns the number of parent levels plus 1 for the resource itself.
237
+ *
238
+ * @param parentResource - Single parent resource (optional)
239
+ * @param parentResources - Multiple parent resources (optional)
240
+ * @returns The total nesting depth (1 = flat, 2 = one parent, 3+ = deeply nested)
241
+ */
242
+ export function calculateNestingDepth(parentResource, parentResources) {
243
+ if (parentResources && parentResources.length > 0) {
244
+ return parentResources.length + 1;
245
+ }
246
+ if (parentResource) {
247
+ return 2;
200
248
  }
201
- // Otherwise append the path (e.g., '/:id')
202
- return `${parentPath}${childBasePath}${mapping.path}`;
249
+ return 1;
203
250
  }
204
251
  /**
205
252
  * Infer the resource name from a procedure name
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * RPC helper for type-safe tRPC registration
3
+ *
4
+ * Provides a symmetric API to `rest()` for registering tRPC endpoints
5
+ * with full type preservation for client inference.
6
+ *
7
+ * @module rpc
8
+ */
9
+ import { type VeloxApp } from '@veloxts/core';
10
+ import type { FastifyInstance } from 'fastify';
11
+ import { type AnyRouter, type InferRouterFromCollections } from './trpc/index.js';
12
+ import type { ProcedureCollection } from './types.js';
13
+ /**
14
+ * Options for RPC registration
15
+ */
16
+ export interface RpcOptions {
17
+ /**
18
+ * tRPC endpoint prefix
19
+ *
20
+ * @default '/trpc'
21
+ */
22
+ prefix?: string;
23
+ }
24
+ /**
25
+ * Result of rpc() for type-safe router access
26
+ *
27
+ * Contains both the typed router (for type export) and an async
28
+ * registration function (for registering with Fastify).
29
+ */
30
+ export interface RpcResult<T extends readonly ProcedureCollection[]> {
31
+ /**
32
+ * The typed tRPC router
33
+ *
34
+ * Use `typeof router` to export the AppRouter type for clients.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const { router } = rpc([userProcedures, postProcedures] as const);
39
+ * export type AppRouter = typeof router;
40
+ * ```
41
+ */
42
+ readonly router: AnyRouter & InferRouterFromCollections<T>;
43
+ /**
44
+ * Register the tRPC routes with a Fastify instance
45
+ *
46
+ * This is an async function that registers the tRPC plugin.
47
+ *
48
+ * @param server - Fastify instance or VeloxApp
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const { register } = rpc([userProcedures] as const);
53
+ * await register(app.server);
54
+ * ```
55
+ */
56
+ readonly register: (server: FastifyInstance) => Promise<void>;
57
+ }
58
+ /**
59
+ * Create a type-safe tRPC router from procedure collections
60
+ *
61
+ * This is the RPC equivalent of `rest()`. It returns a typed router
62
+ * and a registration function, enabling proper type inference for
63
+ * client-side usage.
64
+ *
65
+ * **IMPORTANT**: Use `as const` on the collections array to preserve
66
+ * literal types for full type inference.
67
+ *
68
+ * @param collections - Array of procedure collections (use `as const`)
69
+ * @param options - Optional RPC configuration
70
+ * @returns RpcResult with typed router and register function
71
+ *
72
+ * @example Basic usage with VeloxApp
73
+ * ```typescript
74
+ * import { veloxApp } from '@veloxts/core';
75
+ * import { rpc } from '@veloxts/router';
76
+ *
77
+ * const app = await veloxApp({ port: 3030 });
78
+ *
79
+ * const { router, register } = rpc([
80
+ * userProcedures,
81
+ * postProcedures,
82
+ * ] as const);
83
+ *
84
+ * // Register tRPC routes
85
+ * await register(app.server);
86
+ *
87
+ * // Export type for clients - fully typed!
88
+ * export type AppRouter = typeof router;
89
+ *
90
+ * await app.start();
91
+ * ```
92
+ *
93
+ * @example With custom prefix
94
+ * ```typescript
95
+ * const { router, register } = rpc([userProcedures] as const, {
96
+ * prefix: '/api/trpc',
97
+ * });
98
+ *
99
+ * await register(app.server);
100
+ * export type AppRouter = typeof router;
101
+ * ```
102
+ *
103
+ * @example Combined with REST
104
+ * ```typescript
105
+ * // Serve same procedures via both REST and tRPC
106
+ * const collections = [userProcedures, postProcedures] as const;
107
+ *
108
+ * // REST endpoints at /api/*
109
+ * app.routes(rest([...collections], { prefix: '/api' }));
110
+ *
111
+ * // tRPC endpoints at /trpc/*
112
+ * const { router, register } = rpc(collections);
113
+ * await register(app.server);
114
+ *
115
+ * export type AppRouter = typeof router;
116
+ * ```
117
+ */
118
+ export declare function rpc<const T extends readonly ProcedureCollection[]>(collections: T, options?: RpcOptions): RpcResult<T>;
119
+ /**
120
+ * Register tRPC routes and return the typed router
121
+ *
122
+ * This is a convenience function that combines router creation and
123
+ * registration in a single async call. Use this when you don't need
124
+ * to separate router creation from registration.
125
+ *
126
+ * **IMPORTANT**: Use `as const` on the collections array to preserve
127
+ * literal types for full type inference.
128
+ *
129
+ * @param app - VeloxApp instance
130
+ * @param collections - Array of procedure collections (use `as const`)
131
+ * @param options - Optional RPC configuration
132
+ * @returns The typed tRPC router
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * const router = await registerRpc(app, [
137
+ * userProcedures,
138
+ * postProcedures,
139
+ * ] as const);
140
+ *
141
+ * export type AppRouter = typeof router;
142
+ * ```
143
+ */
144
+ export declare function registerRpc<const T extends readonly ProcedureCollection[]>(app: VeloxApp, collections: T, options?: RpcOptions): Promise<AnyRouter & InferRouterFromCollections<T>>;
package/dist/rpc.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * RPC helper for type-safe tRPC registration
3
+ *
4
+ * Provides a symmetric API to `rest()` for registering tRPC endpoints
5
+ * with full type preservation for client inference.
6
+ *
7
+ * @module rpc
8
+ */
9
+ import { fail } from '@veloxts/core';
10
+ import { appRouter, registerTRPCPlugin, trpc, } from './trpc/index.js';
11
+ // ============================================================================
12
+ // Main Function
13
+ // ============================================================================
14
+ /**
15
+ * Create a type-safe tRPC router from procedure collections
16
+ *
17
+ * This is the RPC equivalent of `rest()`. It returns a typed router
18
+ * and a registration function, enabling proper type inference for
19
+ * client-side usage.
20
+ *
21
+ * **IMPORTANT**: Use `as const` on the collections array to preserve
22
+ * literal types for full type inference.
23
+ *
24
+ * @param collections - Array of procedure collections (use `as const`)
25
+ * @param options - Optional RPC configuration
26
+ * @returns RpcResult with typed router and register function
27
+ *
28
+ * @example Basic usage with VeloxApp
29
+ * ```typescript
30
+ * import { veloxApp } from '@veloxts/core';
31
+ * import { rpc } from '@veloxts/router';
32
+ *
33
+ * const app = await veloxApp({ port: 3030 });
34
+ *
35
+ * const { router, register } = rpc([
36
+ * userProcedures,
37
+ * postProcedures,
38
+ * ] as const);
39
+ *
40
+ * // Register tRPC routes
41
+ * await register(app.server);
42
+ *
43
+ * // Export type for clients - fully typed!
44
+ * export type AppRouter = typeof router;
45
+ *
46
+ * await app.start();
47
+ * ```
48
+ *
49
+ * @example With custom prefix
50
+ * ```typescript
51
+ * const { router, register } = rpc([userProcedures] as const, {
52
+ * prefix: '/api/trpc',
53
+ * });
54
+ *
55
+ * await register(app.server);
56
+ * export type AppRouter = typeof router;
57
+ * ```
58
+ *
59
+ * @example Combined with REST
60
+ * ```typescript
61
+ * // Serve same procedures via both REST and tRPC
62
+ * const collections = [userProcedures, postProcedures] as const;
63
+ *
64
+ * // REST endpoints at /api/*
65
+ * app.routes(rest([...collections], { prefix: '/api' }));
66
+ *
67
+ * // tRPC endpoints at /trpc/*
68
+ * const { router, register } = rpc(collections);
69
+ * await register(app.server);
70
+ *
71
+ * export type AppRouter = typeof router;
72
+ * ```
73
+ */
74
+ export function rpc(collections, options = {}) {
75
+ const { prefix = '/trpc' } = options;
76
+ // Validate inputs
77
+ if (collections.length === 0) {
78
+ throw fail('VELOX-2006');
79
+ }
80
+ // Create the typed router immediately
81
+ const t = trpc();
82
+ const router = appRouter(t, collections);
83
+ // Create the registration function
84
+ const register = async (server) => {
85
+ await registerTRPCPlugin(server, {
86
+ prefix,
87
+ router,
88
+ });
89
+ };
90
+ return {
91
+ router,
92
+ register,
93
+ };
94
+ }
95
+ // ============================================================================
96
+ // Async Helper (Alternative API)
97
+ // ============================================================================
98
+ /**
99
+ * Register tRPC routes and return the typed router
100
+ *
101
+ * This is a convenience function that combines router creation and
102
+ * registration in a single async call. Use this when you don't need
103
+ * to separate router creation from registration.
104
+ *
105
+ * **IMPORTANT**: Use `as const` on the collections array to preserve
106
+ * literal types for full type inference.
107
+ *
108
+ * @param app - VeloxApp instance
109
+ * @param collections - Array of procedure collections (use `as const`)
110
+ * @param options - Optional RPC configuration
111
+ * @returns The typed tRPC router
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const router = await registerRpc(app, [
116
+ * userProcedures,
117
+ * postProcedures,
118
+ * ] as const);
119
+ *
120
+ * export type AppRouter = typeof router;
121
+ * ```
122
+ */
123
+ export async function registerRpc(app, collections, options = {}) {
124
+ const { router, register } = rpc(collections, options);
125
+ await register(app.server);
126
+ return router;
127
+ }