@veloxts/router 0.6.84 → 0.6.86

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.
@@ -31,6 +31,7 @@
31
31
  */
32
32
  export { generateOpenApiSpec, getOpenApiRouteSummary, validateOpenApiSpec, } from './generator.js';
33
33
  export { createSwaggerUI, getOpenApiSpec, registerDocs, swaggerUIPlugin, } from './plugin.js';
34
+ export { DEFAULT_UI_CONFIG, escapeHtml, generateSwaggerUIHtml, SWAGGER_UI_CDN, type SwaggerUIHtmlOptions, } from './html-generator.js';
34
35
  export { createStringSchema, extractSchemaProperties, mergeSchemas, removeSchemaProperties, type SchemaConversionOptions, schemaHasProperties, zodSchemaToJsonSchema, } from './schema-converter.js';
35
36
  export { type BuildParametersOptions, type BuildParametersResult, buildParameters, convertFromOpenAPIPath, convertToOpenAPIPath, extractPathParamNames, extractQueryParameters, extractResourceFromPath, hasPathParameters, joinPaths, normalizePath, parsePathParameters, type QueryParamExtractionOptions, } from './path-extractor.js';
36
37
  export { createSecurityRequirement, DEFAULT_GUARD_MAPPINGS, DEFAULT_SECURITY_SCHEMES, extractGuardScopes, extractUsedSecuritySchemes, filterUsedSecuritySchemes, type GuardMappingOptions, guardsRequireAuth, guardsToSecurity, mapGuardToSecurity, mergeSecuritySchemes, } from './security-mapper.js';
@@ -38,6 +38,10 @@ export { generateOpenApiSpec, getOpenApiRouteSummary, validateOpenApiSpec, } fro
38
38
  // ============================================================================
39
39
  export { createSwaggerUI, getOpenApiSpec, registerDocs, swaggerUIPlugin, } from './plugin.js';
40
40
  // ============================================================================
41
+ // HTML Generator
42
+ // ============================================================================
43
+ export { DEFAULT_UI_CONFIG, escapeHtml, generateSwaggerUIHtml, SWAGGER_UI_CDN, } from './html-generator.js';
44
+ // ============================================================================
41
45
  // Schema Converter
42
46
  // ============================================================================
43
47
  export { createStringSchema, extractSchemaProperties, mergeSchemas, removeSchemaProperties, schemaHasProperties, zodSchemaToJsonSchema, } from './schema-converter.js';
@@ -6,90 +6,7 @@
6
6
  * @module @veloxts/router/openapi/plugin
7
7
  */
8
8
  import { generateOpenApiSpec } from './generator.js';
9
- // ============================================================================
10
- // Swagger UI HTML Generation
11
- // ============================================================================
12
- /**
13
- * Default Swagger UI configuration
14
- */
15
- const DEFAULT_UI_CONFIG = {
16
- deepLinking: true,
17
- displayOperationId: false,
18
- defaultModelsExpandDepth: 1,
19
- defaultModelExpandDepth: 1,
20
- docExpansion: 'list',
21
- filter: false,
22
- showExtensions: false,
23
- tryItOutEnabled: true,
24
- persistAuthorization: false,
25
- };
26
- /**
27
- * Swagger UI CDN URLs
28
- */
29
- const SWAGGER_UI_CDN = {
30
- css: 'https://unpkg.com/swagger-ui-dist@5/swagger-ui.css',
31
- bundle: 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js',
32
- standalonePreset: 'https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js',
33
- };
34
- /**
35
- * Generates the Swagger UI HTML page
36
- */
37
- function generateSwaggerUIHtml(specUrl, config, title, favicon) {
38
- const configJson = JSON.stringify({
39
- url: specUrl,
40
- dom_id: '#swagger-ui',
41
- deepLinking: config.deepLinking,
42
- displayOperationId: config.displayOperationId,
43
- defaultModelsExpandDepth: config.defaultModelsExpandDepth,
44
- defaultModelExpandDepth: config.defaultModelExpandDepth,
45
- docExpansion: config.docExpansion,
46
- filter: config.filter,
47
- showExtensions: config.showExtensions,
48
- tryItOutEnabled: config.tryItOutEnabled,
49
- persistAuthorization: config.persistAuthorization,
50
- presets: ['SwaggerUIBundle.presets.apis', 'SwaggerUIStandalonePreset'],
51
- plugins: ['SwaggerUIBundle.plugins.DownloadUrl'],
52
- layout: 'StandaloneLayout',
53
- });
54
- const faviconTag = favicon ? `<link rel="icon" type="image/x-icon" href="${favicon}">` : '';
55
- return `<!DOCTYPE html>
56
- <html lang="en">
57
- <head>
58
- <meta charset="UTF-8">
59
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
60
- <title>${escapeHtml(title)}</title>
61
- ${faviconTag}
62
- <link rel="stylesheet" href="${SWAGGER_UI_CDN.css}">
63
- <style>
64
- html { box-sizing: border-box; overflow-y: scroll; }
65
- *, *:before, *:after { box-sizing: inherit; }
66
- body { margin: 0; background: #fafafa; }
67
- .swagger-ui .topbar { display: none; }
68
- </style>
69
- </head>
70
- <body>
71
- <div id="swagger-ui"></div>
72
- <script src="${SWAGGER_UI_CDN.bundle}"></script>
73
- <script src="${SWAGGER_UI_CDN.standalonePreset}"></script>
74
- <script>
75
- window.onload = function() {
76
- window.ui = SwaggerUIBundle(${configJson.replace(/"(SwaggerUIBundle\.presets\.apis|SwaggerUIStandalonePreset|SwaggerUIBundle\.plugins\.DownloadUrl)"/g, '$1')});
77
- };
78
- </script>
79
- </body>
80
- </html>`;
81
- }
82
- /**
83
- * Escapes HTML special characters
84
- */
85
- function escapeHtml(text) {
86
- return text
87
- .replace(/&/g, '&amp;')
88
- .replace(/</g, '&lt;')
89
- .replace(/>/g, '&gt;')
90
- .replace(/"/g, '&quot;')
91
- .replace(/'/g, '&#039;');
92
- }
9
+ import { generateSwaggerUIHtml } from './html-generator.js';
93
10
  // ============================================================================
94
11
  // Fastify Plugin
95
12
  // ============================================================================
@@ -118,11 +35,6 @@ function escapeHtml(text) {
118
35
  */
119
36
  export const swaggerUIPlugin = async (fastify, options) => {
120
37
  const { routePrefix = '/docs', specRoute = `${routePrefix}/openapi.json`, uiConfig = {}, openapi, collections, title = 'API Documentation', favicon, } = options;
121
- // Merge UI config with defaults
122
- const mergedConfig = {
123
- ...DEFAULT_UI_CONFIG,
124
- ...uiConfig,
125
- };
126
38
  // Generate the OpenAPI specification
127
39
  let spec;
128
40
  try {
@@ -137,7 +49,12 @@ export const swaggerUIPlugin = async (fastify, options) => {
137
49
  return reply.header('Content-Type', 'application/json').send(spec);
138
50
  });
139
51
  // Register Swagger UI HTML route
140
- const htmlContent = generateSwaggerUIHtml(specRoute, mergedConfig, title, favicon);
52
+ const htmlContent = generateSwaggerUIHtml({
53
+ specUrl: specRoute,
54
+ title,
55
+ favicon,
56
+ config: uiConfig,
57
+ });
141
58
  fastify.get(routePrefix, async (_request, reply) => {
142
59
  return reply.header('Content-Type', 'text/html; charset=utf-8').send(htmlContent);
143
60
  });
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { ConfigurationError, logWarning } from '@veloxts/core';
11
11
  import { GuardError } from '../errors.js';
12
+ import { createMiddlewareExecutor, executeMiddlewareChain } from '../middleware/chain.js';
12
13
  import { analyzeNamingConvention, isDevelopment, normalizeWarningOption, } from '../warnings.js';
13
14
  // ============================================================================
14
15
  // Utility Functions
@@ -115,6 +116,9 @@ export function procedure() {
115
116
  guards: [],
116
117
  restOverride: undefined,
117
118
  parentResource: undefined,
119
+ parentResources: undefined,
120
+ deprecated: undefined,
121
+ deprecationMessage: undefined,
118
122
  });
119
123
  }
120
124
  // ============================================================================
@@ -209,18 +213,41 @@ function createBuilder(state) {
209
213
  });
210
214
  },
211
215
  /**
212
- * Declares a parent resource for nested routes
216
+ * Marks the procedure as deprecated
213
217
  */
214
- parent(namespace, paramName) {
218
+ deprecated(message) {
219
+ return createBuilder({
220
+ ...state,
221
+ deprecated: true,
222
+ deprecationMessage: message,
223
+ });
224
+ },
225
+ /**
226
+ * Declares a parent resource for nested routes (single level)
227
+ */
228
+ parent(resource, param) {
215
229
  const parentConfig = {
216
- namespace,
217
- paramName: paramName ?? deriveParentParamName(namespace),
230
+ resource,
231
+ param: param ?? deriveParentParamName(resource),
218
232
  };
219
233
  return createBuilder({
220
234
  ...state,
221
235
  parentResource: parentConfig,
222
236
  });
223
237
  },
238
+ /**
239
+ * Declares multiple parent resources for deeply nested routes
240
+ */
241
+ parents(config) {
242
+ const parentConfigs = config.map((item) => ({
243
+ resource: item.resource,
244
+ param: item.param ?? deriveParentParamName(item.resource),
245
+ }));
246
+ return createBuilder({
247
+ ...state,
248
+ parentResources: parentConfigs,
249
+ });
250
+ },
224
251
  /**
225
252
  * Finalizes as a query procedure
226
253
  */
@@ -260,7 +287,10 @@ function compileProcedure(type, handler, state) {
260
287
  middlewares: typedMiddlewares,
261
288
  guards: state.guards,
262
289
  restOverride: state.restOverride,
290
+ deprecated: state.deprecated,
291
+ deprecationMessage: state.deprecationMessage,
263
292
  parentResource: state.parentResource,
293
+ parentResources: state.parentResources,
264
294
  // Store pre-compiled executor for performance
265
295
  _precompiledExecutor: precompiledExecutor,
266
296
  };
@@ -275,36 +305,7 @@ function compileProcedure(type, handler, state) {
275
305
  * @internal
276
306
  */
277
307
  function createPrecompiledMiddlewareExecutor(middlewares, handler) {
278
- // Pre-build the chain executor once
279
- return async (input, ctx) => {
280
- // Create mutable context copy for middleware extensions
281
- const mutableCtx = ctx;
282
- // Build the handler wrapper
283
- const executeHandler = async () => {
284
- const output = await handler({ input, ctx: mutableCtx });
285
- return { output };
286
- };
287
- // Build chain from end to start (only done once per request, not per middleware)
288
- let next = executeHandler;
289
- for (let i = middlewares.length - 1; i >= 0; i--) {
290
- const middleware = middlewares[i];
291
- const currentNext = next;
292
- next = async () => {
293
- return middleware({
294
- input,
295
- ctx: mutableCtx,
296
- next: async (opts) => {
297
- if (opts?.ctx) {
298
- Object.assign(mutableCtx, opts.ctx);
299
- }
300
- return currentNext();
301
- },
302
- });
303
- };
304
- }
305
- const result = await next();
306
- return result.output;
307
- };
308
+ return createMiddlewareExecutor(middlewares, handler);
308
309
  }
309
310
  /**
310
311
  * Defines a collection of procedures under a namespace
@@ -502,31 +503,7 @@ export async function executeProcedure(procedure, rawInput, ctx) {
502
503
  * @internal
503
504
  */
504
505
  async function executeMiddlewareChainFallback(middlewares, input, ctx, handler) {
505
- // Build the chain from the end (handler) back to the start
506
- let next = async () => {
507
- const output = await handler();
508
- return { output };
509
- };
510
- // Wrap each middleware from last to first
511
- for (let i = middlewares.length - 1; i >= 0; i--) {
512
- const middleware = middlewares[i];
513
- const currentNext = next;
514
- next = async () => {
515
- return middleware({
516
- input,
517
- ctx,
518
- next: async (opts) => {
519
- // Allow middleware to extend context
520
- if (opts?.ctx) {
521
- Object.assign(ctx, opts.ctx);
522
- }
523
- return currentNext();
524
- },
525
- });
526
- };
527
- }
528
- const result = await next();
529
- return result.output;
506
+ return executeMiddlewareChain(middlewares, input, ctx, handler);
530
507
  }
531
508
  // ============================================================================
532
509
  // Type Utilities
@@ -249,16 +249,39 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
249
249
  */
250
250
  rest(config: RestRouteOverride): ProcedureBuilder<TInput, TOutput, TContext>;
251
251
  /**
252
- * Declares a parent resource for nested routes
252
+ * Marks the procedure as deprecated
253
+ *
254
+ * Deprecated procedures will be marked in OpenAPI documentation with the
255
+ * deprecated flag, helping API consumers know they should migrate to alternatives.
256
+ *
257
+ * @param message - Optional message explaining the deprecation and suggesting alternatives
258
+ * @returns Same builder (no type changes)
259
+ *
260
+ * @example
261
+ * ```typescript
262
+ * // Simple deprecation
263
+ * procedure()
264
+ * .deprecated()
265
+ * .query(handler);
266
+ *
267
+ * // With migration message
268
+ * procedure()
269
+ * .deprecated('Use getUserById instead. This endpoint will be removed in v2.0.')
270
+ * .query(handler);
271
+ * ```
272
+ */
273
+ deprecated(message?: string): ProcedureBuilder<TInput, TOutput, TContext>;
274
+ /**
275
+ * Declares a parent resource for nested routes (single level)
253
276
  *
254
277
  * When a procedure has a parent resource, the REST path will be nested
255
- * under the parent: `/${parentNamespace}/:${parentParam}/${childNamespace}/:id`
278
+ * under the parent: `/${parentResource}/:${parentParam}/${childResource}/:id`
256
279
  *
257
280
  * The input schema should include the parent parameter (e.g., `postId`) for
258
281
  * proper type safety and runtime validation.
259
282
  *
260
- * @param namespace - Parent resource namespace (e.g., 'posts', 'users')
261
- * @param paramName - Optional custom parameter name (default: `${singularNamespace}Id`)
283
+ * @param resource - Parent resource name (e.g., 'posts', 'users')
284
+ * @param param - Optional custom parameter name (default: `${singularResource}Id`)
262
285
  * @returns Same builder (no type changes)
263
286
  *
264
287
  * @example
@@ -276,7 +299,38 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
276
299
  * .query(async ({ input }) => { ... });
277
300
  * ```
278
301
  */
279
- parent(namespace: string, paramName?: string): ProcedureBuilder<TInput, TOutput, TContext>;
302
+ parent(resource: string, param?: string): ProcedureBuilder<TInput, TOutput, TContext>;
303
+ /**
304
+ * Declares multiple parent resources for deeply nested routes
305
+ *
306
+ * When a procedure has multiple parent resources, the REST path will be
307
+ * deeply nested: `/${parent1}/:${param1}/${parent2}/:${param2}/.../${child}/:id`
308
+ *
309
+ * The input schema should include ALL parent parameters for proper type safety.
310
+ *
311
+ * @param config - Array of parent resource configurations from outermost to innermost
312
+ * @returns Same builder (no type changes)
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * // Generates: GET /organizations/:orgId/projects/:projectId/tasks/:id
317
+ * const getTask = procedure()
318
+ * .parents([
319
+ * { resource: 'organizations', param: 'orgId' },
320
+ * { resource: 'projects', param: 'projectId' },
321
+ * ])
322
+ * .input(z.object({
323
+ * orgId: z.string(),
324
+ * projectId: z.string(),
325
+ * id: z.string()
326
+ * }))
327
+ * .query(async ({ input }) => { ... });
328
+ * ```
329
+ */
330
+ parents(config: Array<{
331
+ resource: string;
332
+ param?: string;
333
+ }>): ProcedureBuilder<TInput, TOutput, TContext>;
280
334
  /**
281
335
  * Finalizes the procedure as a query (read-only operation)
282
336
  *
@@ -333,8 +387,14 @@ export interface BuilderRuntimeState {
333
387
  guards: GuardLike<unknown>[];
334
388
  /** REST route override */
335
389
  restOverride?: RestRouteOverride;
336
- /** Parent resource configuration for nested routes */
390
+ /** Parent resource configuration for nested routes (single level) */
337
391
  parentResource?: ParentResourceConfig;
392
+ /** Multi-level parent resource configuration for deeply nested routes */
393
+ parentResources?: ParentResourceConfig[];
394
+ /** Whether this procedure is deprecated */
395
+ deprecated?: boolean;
396
+ /** Deprecation message */
397
+ deprecationMessage?: string;
338
398
  }
339
399
  /**
340
400
  * Type for the procedures object passed to defineProcedures
@@ -41,6 +41,35 @@ export interface RestAdapterOptions {
41
41
  prefix?: string;
42
42
  /** Custom error handler */
43
43
  onError?: (error: unknown, request: FastifyRequest, reply: FastifyReply) => void;
44
+ /**
45
+ * Generate flat shortcut routes alongside nested routes.
46
+ *
47
+ * When enabled, nested routes like `/organizations/:orgId/projects/:projectId/tasks/:id`
48
+ * will also generate a flat shortcut route like `/tasks/:id`.
49
+ *
50
+ * Note: Shortcuts only work for single-resource operations (GET, PUT, PATCH, DELETE with :id).
51
+ * Collection operations (list, create) require parent context and are NOT generated as shortcuts.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * rest([tasks], { shortcuts: true })
56
+ * // Generates:
57
+ * // GET /organizations/:orgId/projects/:projectId/tasks/:id (nested)
58
+ * // GET /tasks/:id (shortcut)
59
+ * ```
60
+ *
61
+ * @default false
62
+ */
63
+ shortcuts?: boolean;
64
+ /**
65
+ * Enable warnings about deep nesting (3+ levels).
66
+ *
67
+ * When true (default), the router warns when nesting exceeds 3 levels.
68
+ * Set to false to silence these warnings.
69
+ *
70
+ * @default true
71
+ */
72
+ nestingWarnings?: boolean;
44
73
  }
45
74
  /**
46
75
  * Internal options used during route registration
@@ -53,6 +82,13 @@ interface InternalRegistrationOptions extends RestAdapterOptions {
53
82
  */
54
83
  _prefixHandledByFastify?: boolean;
55
84
  }
85
+ /** Options for generating REST routes */
86
+ export interface GenerateRestRoutesOptions {
87
+ /** Generate flat shortcut routes alongside nested routes */
88
+ shortcuts?: boolean;
89
+ /** Enable nesting depth warnings (default: true) */
90
+ nestingWarnings?: boolean;
91
+ }
56
92
  /**
57
93
  * Generate REST routes from a procedure collection
58
94
  *
@@ -62,9 +98,10 @@ interface InternalRegistrationOptions extends RestAdapterOptions {
62
98
  * 3. Skipping if neither applies (tRPC-only procedure)
63
99
  *
64
100
  * @param collection - Procedure collection to generate routes from
101
+ * @param options - Optional route generation options
65
102
  * @returns Array of REST route definitions
66
103
  */
67
- export declare function generateRestRoutes(collection: ProcedureCollection): RestRoute[];
104
+ export declare function generateRestRoutes(collection: ProcedureCollection, options?: GenerateRestRoutesOptions): RestRoute[];
68
105
  /**
69
106
  * Register REST routes from procedure collections onto a Fastify instance
70
107
  *
@@ -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';