@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.
- package/CHANGELOG.md +18 -0
- package/GUIDE.md +51 -7
- package/dist/expose.d.ts +12 -9
- package/dist/expose.js +13 -9
- package/dist/index.d.ts +10 -6
- package/dist/index.js +6 -3
- package/dist/middleware/chain.d.ts +54 -0
- package/dist/middleware/chain.js +80 -0
- package/dist/middleware/index.d.ts +7 -0
- package/dist/middleware/index.js +6 -0
- package/dist/openapi/generator.js +10 -0
- package/dist/openapi/html-generator.d.ts +66 -0
- package/dist/openapi/html-generator.js +116 -0
- package/dist/openapi/index.d.ts +1 -0
- package/dist/openapi/index.js +4 -0
- package/dist/openapi/plugin.js +7 -90
- package/dist/procedure/builder.js +36 -59
- package/dist/procedure/types.d.ts +66 -6
- package/dist/rest/adapter.d.ts +38 -1
- package/dist/rest/adapter.js +94 -27
- package/dist/rest/index.d.ts +2 -2
- package/dist/rest/index.js +1 -1
- package/dist/rest/naming.d.ts +38 -2
- package/dist/rest/naming.js +65 -18
- package/dist/rpc.d.ts +144 -0
- package/dist/rpc.js +127 -0
- package/dist/trpc/adapter.d.ts +139 -10
- package/dist/trpc/adapter.js +33 -61
- package/dist/trpc/index.d.ts +3 -1
- package/dist/types.d.ts +51 -7
- package/package.json +3 -3
package/dist/openapi/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/openapi/index.js
CHANGED
|
@@ -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';
|
package/dist/openapi/plugin.js
CHANGED
|
@@ -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, '&')
|
|
88
|
-
.replace(/</g, '<')
|
|
89
|
-
.replace(/>/g, '>')
|
|
90
|
-
.replace(/"/g, '"')
|
|
91
|
-
.replace(/'/g, ''');
|
|
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(
|
|
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
|
-
*
|
|
216
|
+
* Marks the procedure as deprecated
|
|
213
217
|
*/
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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: `/${
|
|
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
|
|
261
|
-
* @param
|
|
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(
|
|
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
|
package/dist/rest/adapter.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/rest/adapter.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
41
|
-
* (e.g., /posts/:postId/comments/:id)
|
|
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
|
|
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
|
|
82
|
-
const path = procedure
|
|
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
|
|
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
|
|
151
|
-
*
|
|
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
|
|
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
|
|
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.
|
package/dist/rest/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/rest/index.js
CHANGED
|
@@ -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';
|