@veloxts/router 0.7.8 → 0.8.0

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # @veloxts/router
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - feat(router): new simplified procedure builder
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @veloxts/core@0.8.0
13
+ - @veloxts/validation@0.8.0
14
+
15
+ ## 0.7.9
16
+
17
+ ### Patch Changes
18
+
19
+ - feat(router): swagger auto-discovery of module collections
20
+ - Updated dependencies
21
+ - @veloxts/core@0.7.9
22
+ - @veloxts/validation@0.7.9
23
+
3
24
  ## 0.7.8
4
25
 
5
26
  ### Patch Changes
package/GUIDE.md CHANGED
@@ -28,11 +28,9 @@ export const userProcedures = procedures('users', {
28
28
  ## Procedure API
29
29
 
30
30
  - `.input(schema)` - Validate input with Zod
31
- - `.output(schema)` - Validate output with Zod
32
- - `.expose(schema)` - Context-dependent output with resource schema
31
+ - `.output(schema)` - Validate output with Zod schema or tagged resource view
33
32
  - `.use(middleware)` - Add middleware
34
33
  - `.guard(guard)` - Add authorization guard
35
- - `.guardNarrow(guard)` - Add guard with TypeScript type narrowing
36
34
  - `.rest({ method, path })` - Override REST path
37
35
  - `.query(handler)` - Finalize as read operation
38
36
  - `.mutation(handler)` - Finalize as write operation
@@ -405,7 +403,7 @@ console.table(routes);
405
403
 
406
404
  ## Resource API (Context-Dependent Outputs)
407
405
 
408
- The Resource API provides context-dependent output types using phantom types. Pass a resource schema to `.output()` instead of a Zod schema to define field visibility per access level.
406
+ The Resource API provides context-dependent output types using phantom types. Pass a tagged resource view to `.output()` instead of a plain Zod schema to define field visibility per access level.
409
407
 
410
408
  ### Defining a Resource Schema
411
409
 
@@ -423,28 +421,27 @@ const UserSchema = resourceSchema()
423
421
  .build();
424
422
  ```
425
423
 
426
- ### Automatic Projection (Simple Cases)
424
+ ### Automatic Projection with `.output()` and Guards
427
425
 
428
- The most elegant approach is to chain `.expose()` with a narrowing guard. The procedure executor automatically projects fields based on the guard's access level:
426
+ The most elegant approach is to chain `.guard()` with `.output()` using a tagged resource view. The procedure builder uses the guard's access level to determine which fields to include:
429
427
 
430
428
  ```typescript
431
- import { authenticatedNarrow, adminNarrow } from '@veloxts/auth';
429
+ import { authenticated, hasRole } from '@veloxts/auth';
432
430
 
433
431
  export const userProcedures = procedures('users', {
434
- // Authenticated endpoint - auto-projects { id, name, email, createdAt }
432
+ // Authenticated endpoint - projects { id, name, email, createdAt }
435
433
  getProfile: procedure()
436
- .guardNarrow(authenticatedNarrow)
437
- .expose(UserSchema)
434
+ .guard(authenticated)
435
+ .output(UserSchema.authenticated)
438
436
  .input(z.object({ id: z.string().uuid() }))
439
437
  .query(async ({ input, ctx }) => {
440
- // Just return the full data - projection is automatic!
441
438
  return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
442
439
  }),
443
440
 
444
- // Admin endpoint - auto-projects all fields
441
+ // Admin endpoint - projects all fields
445
442
  getFullProfile: procedure()
446
- .guardNarrow(adminNarrow)
447
- .expose(UserSchema)
443
+ .guard(hasRole('admin'))
444
+ .output(UserSchema.admin)
448
445
  .input(z.object({ id: z.string().uuid() }))
449
446
  .query(async ({ input, ctx }) => {
450
447
  return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
@@ -454,14 +451,13 @@ export const userProcedures = procedures('users', {
454
451
 
455
452
  **How it works:**
456
453
 
457
- 1. The `guardNarrow()` method accepts guards with an `accessLevel` property (`'public'`, `'authenticated'`, or `'admin'`)
458
- 2. After the guard passes, the procedure executor tracks the highest access level
459
- 3. When the handler returns, the executor automatically projects the result using the resource schema
460
- 4. No manual `.forX()` calls needed - the output type is inferred at compile time
454
+ 1. The `.output()` method accepts both plain Zod schemas and tagged resource views (e.g., `UserSchema.authenticated`)
455
+ 2. When a tagged view is passed, the procedure executor automatically projects the handler's return value
456
+ 3. The output type is inferred at compile time from the tagged view
461
457
 
462
458
  **Benefits:**
463
459
  - Clean, declarative code - no projection logic in handlers
464
- - Type-safe - return types are inferred from guard + schema
460
+ - Type-safe - return types are inferred from the tagged view
465
461
  - Less boilerplate - handlers just return data
466
462
  - Consistent - impossible to forget projection
467
463
 
@@ -516,10 +512,10 @@ export const userProcedures = procedures('users', {
516
512
  For arrays of items, use `resourceCollection()`:
517
513
 
518
514
  ```typescript
519
- // Automatic projection (simple cases)
515
+ // Automatic projection with .output()
520
516
  const listUsers = procedure()
521
- .guardNarrow(authenticatedNarrow)
522
- .expose(UserSchema)
517
+ .guard(authenticated)
518
+ .output(UserSchema.authenticated)
523
519
  .query(async ({ ctx }) => {
524
520
  return ctx.db.user.findMany({ take: 50 });
525
521
  });
@@ -539,7 +535,7 @@ For manual projection with dynamic access level, use `.for(ctx)`:
539
535
 
540
536
  ```typescript
541
537
  const getUser = procedure()
542
- .guardNarrow(authenticatedNarrow)
538
+ .guard(authenticated)
543
539
  .input(z.object({ id: z.string().uuid() }))
544
540
  .query(async ({ input, ctx }) => {
545
541
  const user = await ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
@@ -562,10 +558,10 @@ const getUser = procedure()
562
558
  The Resource API provides compile-time type safety:
563
559
 
564
560
  ```typescript
565
- // Automatic projection - type inferred from guard
561
+ // Automatic projection - type inferred from tagged view
566
562
  const getProfile = procedure()
567
- .guardNarrow(authenticatedNarrow)
568
- .expose(UserSchema)
563
+ .guard(authenticated)
564
+ .output(UserSchema.authenticated)
569
565
  .query(async ({ ctx }) => {
570
566
  return ctx.db.user.findFirst();
571
567
  });
@@ -586,7 +582,7 @@ const adminResult = resource(user, UserSchema.admin);
586
582
 
587
583
  | Scenario | Approach |
588
584
  |----------|----------|
589
- | Guard determines access level | **Automatic** (`.guardNarrow().output()`) |
585
+ | Guard determines access level | **Automatic** (`.guard().output(Schema.level)`) |
590
586
  | Public endpoints (no guard) | Tagged view (`UserSchema.public`) |
591
587
  | Conditional/dynamic projection | Tagged view or `.for(ctx)` in handler |
592
588
  | Simple, declarative code | **Automatic** |
@@ -603,43 +599,29 @@ const getUser = procedure()
603
599
  .query(handler);
604
600
  ```
605
601
 
606
- ## Guard Type Narrowing (Experimental)
602
+ ## Guard Type Narrowing
607
603
 
608
- When using guards like `authenticated`, TypeScript doesn't know that `ctx.user` is guaranteed non-null after the guard passes. Use `guardNarrow()` to narrow the context type:
604
+ Guards like `authenticated` and `hasRole()` both enforce authorization and narrow the context type. After a guard passes, TypeScript knows `ctx.user` is non-null:
609
605
 
610
606
  ```typescript
611
- import { authenticatedNarrow, hasRoleNarrow } from '@veloxts/auth';
607
+ import { authenticated, hasRole } from '@veloxts/auth';
612
608
 
613
609
  // ctx.user is guaranteed non-null after guard passes
614
610
  const getProfile = procedure()
615
- .guardNarrow(authenticatedNarrow)
611
+ .guard(authenticated)
616
612
  .query(({ ctx }) => {
617
613
  return { email: ctx.user.email }; // No null check needed!
618
614
  });
619
615
 
620
- // Chain multiple narrowing guards
616
+ // Chain multiple guards
621
617
  const adminAction = procedure()
622
- .guardNarrow(authenticatedNarrow)
623
- .guardNarrow(hasRoleNarrow('admin'))
618
+ .guard(authenticated)
619
+ .guard(hasRole('admin'))
624
620
  .mutation(({ ctx }) => {
625
621
  // ctx.user is non-null with roles
626
622
  });
627
623
  ```
628
624
 
629
- **Note**: This API is experimental. The current stable alternative is to use middleware for context extension:
630
-
631
- ```typescript
632
- const getProfile = procedure()
633
- .guard(authenticated)
634
- .use(async ({ ctx, next }) => {
635
- if (!ctx.user) throw new Error('Unreachable');
636
- return next({ ctx: { user: ctx.user } });
637
- })
638
- .query(({ ctx }) => {
639
- // ctx.user is non-null via middleware
640
- });
641
- ```
642
-
643
625
  ## Schema Browser-Safety
644
626
 
645
627
  When building full-stack apps, schemas may be imported on both server and client. Avoid importing server-only dependencies in schema files:
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @veloxts/router
2
2
 
3
- > **Early Access (v0.7.x)**
3
+ > **Early Access (v0.8.x)**
4
4
 
5
5
  Procedure-based routing for VeloxTS Framework - provides hybrid tRPC and REST adapters with naming convention-based HTTP method inference. Learn more at [@veloxts/velox](https://www.npmjs.com/package/@veloxts/velox).
6
6
 
package/dist/index.d.ts CHANGED
@@ -46,13 +46,13 @@ export { GuardError, isGuardError } from './errors.js';
46
46
  export type { DefineProceduresOptions, } from './procedure/builder.js';
47
47
  export { defineProcedures, executeProcedure, isCompiledProcedure, isProcedureCollection, procedure, procedures, } from './procedure/builder.js';
48
48
  export { createProcedure, typedProcedure } from './procedure/factory.js';
49
- export type { BuilderRuntimeState, InferProcedures, InferSchemaOutput, ProcedureBuilder, ProcedureBuilderState, ProcedureDefinitions, ValidSchema, } from './procedure/types.js';
49
+ export type { BuilderRuntimeState, InferOutputSchema, InferProcedures, InferSchemaOutput, ProcedureBuilder, ProcedureBuilderState, ProcedureDefinitions, ValidOutputSchema, ValidSchema, } from './procedure/types.js';
50
50
  export type { RouterResult } from './router-utils.js';
51
51
  export { createRouter, toRouter } from './router-utils.js';
52
52
  export type { NamingWarning, NamingWarningType, WarningConfig, WarningOption } from './warnings.js';
53
53
  export { analyzeNamingConvention, isDevelopment, normalizeWarningOption } from './warnings.js';
54
- export type { ExtractRoutesType, GenerateRestRoutesOptions, RestAdapterOptions, RestMapping, RestRoute, RouteMap, } from './rest/index.js';
55
- export { buildMultiLevelNestedPath, buildNestedRestPath, buildRestPath, calculateNestingDepth, extractRoutes, followsNamingConvention, generateRestRoutes, getRouteSummary, inferResourceName, parseNamingConvention, registerRestRoutes, rest, } from './rest/index.js';
54
+ export type { ExtractRoutesType, GenerateRestRoutesOptions, RegisteredCollection, RestAdapterOptions, RestMapping, RestRoute, RouteMap, } from './rest/index.js';
55
+ export { buildMultiLevelNestedPath, buildNestedRestPath, buildRestPath, calculateNestingDepth, clearCollectionRegistry, extractRoutes, followsNamingConvention, generateRestRoutes, getRegisteredCollections, getRouteSummary, inferResourceName, parseNamingConvention, registerRestRoutes, rest, } from './rest/index.js';
56
56
  export type { AnyRouter, CollectionsToRouterRecord, ExtractNamespace, ExtractProcedures, InferRouterFromCollections, MapProcedureRecordToTRPC, MapProcedureToTRPC, TRPCInstance, TRPCPluginOptions, TRPCRouter, } from './trpc/index.js';
57
57
  export { appRouter, buildTRPCRouter, createTRPCContextFactory, registerTRPCPlugin, trpc, veloxErrorToTRPCError, } from './trpc/index.js';
58
58
  export type { RpcOptions, RpcResult } from './rpc.js';
@@ -82,7 +82,7 @@ export { serve } from './expose.js';
82
82
  * ```
83
83
  */
84
84
  export type { BuildParametersOptions, BuildParametersResult, GuardMappingOptions, JSONSchema, OpenAPIComponents, OpenAPIContact, OpenAPIEncoding, OpenAPIExample, OpenAPIExternalDocs, OpenAPIGeneratorOptions, OpenAPIHeader, OpenAPIHttpMethod, OpenAPIInfo, OpenAPILicense, OpenAPILink, OpenAPIMediaType, OpenAPIOAuthFlow, OpenAPIOAuthFlows, OpenAPIOperation, OpenAPIParameter, OpenAPIPathItem, OpenAPIRequestBody, OpenAPIResponse, OpenAPISecurityRequirement, OpenAPISecurityScheme, OpenAPIServer, OpenAPISpec, OpenAPITag, ParameterIn, QueryParamExtractionOptions, RouteInfo, SchemaConversionOptions, SecuritySchemeType, SwaggerUIConfig, SwaggerUIHtmlOptions, SwaggerUIPluginOptions, } from './openapi/index.js';
85
- export { buildParameters, convertFromOpenAPIPath, convertToOpenAPIPath, createSecurityRequirement, createStringSchema, DEFAULT_GUARD_MAPPINGS, DEFAULT_SECURITY_SCHEMES, DEFAULT_UI_CONFIG, escapeHtml, extractGuardScopes, extractPathParamNames, extractQueryParameters, extractResourceFromPath, extractSchemaProperties, extractUsedSecuritySchemes, filterUsedSecuritySchemes, generateOpenApiSpec, generateSwaggerUIHtml, getOpenApiRouteSummary, getOpenApiSpec, guardsRequireAuth, guardsToSecurity, hasPathParameters, joinPaths, mapGuardToSecurity, mergeSchemas, mergeSecuritySchemes, normalizePath, parsePathParameters, removeSchemaProperties, SWAGGER_UI_CDN, schemaHasProperties, swaggerPlugin, validateOpenApiSpec, zodSchemaToJsonSchema, } from './openapi/index.js';
85
+ export { buildParameters, convertFromOpenAPIPath, convertToOpenAPIPath, createSecurityRequirement, createStringSchema, DEFAULT_GUARD_MAPPINGS, DEFAULT_SECURITY_SCHEMES, DEFAULT_UI_CONFIG, escapeHtml, extractGuardScopes, extractPathParamNames, extractQueryParameters, extractResourceFromPath, extractSchemaProperties, extractUsedSecuritySchemes, filterUsedSecuritySchemes, generateOpenApiSpec, generateOpenApiSpecFromRegistry, generateSwaggerUIHtml, getOpenApiRouteSummary, getOpenApiSpec, guardsRequireAuth, guardsToSecurity, hasPathParameters, joinPaths, mapGuardToSecurity, mergeSchemas, mergeSecuritySchemes, normalizePath, parsePathParameters, removeSchemaProperties, SWAGGER_UI_CDN, schemaHasProperties, swaggerPlugin, validateOpenApiSpec, zodSchemaToJsonSchema, } from './openapi/index.js';
86
86
  export type { AccessLevel, AccessLevelConfig, ADMIN, AdminOutput, AdminTaggedContext, ANONYMOUS, AnonymousOutput, AnonymousTaggedContext, AnyResourceOutput, AUTHENTICATED, AuthenticatedOutput, AuthenticatedTaggedContext, BuilderField, ContextTag, CustomResourceSchemaWithViews, CustomSchemaBuilder, ExtractTag, FilterFieldsByLevel, HasTag, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, IsVisibleToTag, LevelToTag, OutputForLevel, OutputForTag, PUBLIC, PublicOutput, PublicTaggedContext, RelationField, ResourceField, ResourceSchema, ResourceSchemaWithViews, RuntimeField, TaggedContext, TagToLevel, VisibilityLevel, WithTag, } from './resource/index.js';
87
87
  /**
88
88
  * Resource API for context-dependent output types using phantom types.
@@ -114,4 +114,4 @@ export type { AccessLevel, AccessLevelConfig, ADMIN, AdminOutput, AdminTaggedCon
114
114
  * });
115
115
  * ```
116
116
  */
117
- export { defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema, isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder, resource, resourceCollection, resourceSchema, } from './resource/index.js';
117
+ export { defaultAccess, defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema, isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder, resource, resourceCollection, resourceSchema, } from './resource/index.js';
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ defineProcedures, executeProcedure, isCompiledProcedure, isProcedureCollection,
58
58
  export { createProcedure, typedProcedure } from './procedure/factory.js';
59
59
  export { createRouter, toRouter } from './router-utils.js';
60
60
  export { analyzeNamingConvention, isDevelopment, normalizeWarningOption } from './warnings.js';
61
- export { buildMultiLevelNestedPath, buildNestedRestPath, buildRestPath, calculateNestingDepth, extractRoutes, followsNamingConvention, generateRestRoutes, getRouteSummary, inferResourceName, parseNamingConvention, registerRestRoutes, rest, } from './rest/index.js';
61
+ export { buildMultiLevelNestedPath, buildNestedRestPath, buildRestPath, calculateNestingDepth, clearCollectionRegistry, extractRoutes, followsNamingConvention, generateRestRoutes, getRegisteredCollections, getRouteSummary, inferResourceName, parseNamingConvention, registerRestRoutes, rest, } from './rest/index.js';
62
62
  export {
63
63
  // tRPC utilities
64
64
  appRouter, buildTRPCRouter, createTRPCContextFactory, registerTRPCPlugin, trpc, veloxErrorToTRPCError, } from './trpc/index.js';
@@ -73,7 +73,7 @@ createSecurityRequirement,
73
73
  // Schema converter
74
74
  createStringSchema, DEFAULT_GUARD_MAPPINGS, DEFAULT_SECURITY_SCHEMES, DEFAULT_UI_CONFIG, escapeHtml, extractGuardScopes, extractPathParamNames, extractQueryParameters, extractResourceFromPath, extractSchemaProperties, extractUsedSecuritySchemes, filterUsedSecuritySchemes,
75
75
  // Generator
76
- generateOpenApiSpec,
76
+ generateOpenApiSpec, generateOpenApiSpecFromRegistry,
77
77
  // HTML Generator
78
78
  generateSwaggerUIHtml, getOpenApiRouteSummary, getOpenApiSpec, guardsRequireAuth, guardsToSecurity, hasPathParameters, joinPaths, mapGuardToSecurity, mergeSchemas, mergeSecuritySchemes, normalizePath, parsePathParameters, removeSchemaProperties, SWAGGER_UI_CDN, schemaHasProperties,
79
79
  // Plugin
@@ -110,7 +110,7 @@ swaggerPlugin, validateOpenApiSpec, zodSchemaToJsonSchema, } from './openapi/ind
110
110
  */
111
111
  export {
112
112
  // Access level configuration
113
- defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema,
113
+ defaultAccess, defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema,
114
114
  // Visibility
115
115
  isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder,
116
116
  // Resource instances
@@ -5,11 +5,15 @@
5
5
  *
6
6
  * @module @veloxts/router/openapi/generator
7
7
  */
8
+ import type { RegisteredCollection } from '../rest/registry.js';
8
9
  import type { ProcedureCollection } from '../types.js';
9
10
  import type { OpenAPIGeneratorOptions, OpenAPISpec } from './types.js';
10
11
  /**
11
12
  * Generates an OpenAPI 3.0.3 specification from procedure collections
12
13
  *
14
+ * All collections share the same prefix from `options.prefix` (default '/api').
15
+ * For collections with different prefixes, use `generateOpenApiSpecFromRegistry()`.
16
+ *
13
17
  * @param collections - Array of procedure collections to document
14
18
  * @param options - Generator options
15
19
  * @returns Complete OpenAPI specification
@@ -30,6 +34,27 @@ import type { OpenAPIGeneratorOptions, OpenAPISpec } from './types.js';
30
34
  * ```
31
35
  */
32
36
  export declare function generateOpenApiSpec(collections: ProcedureCollection[], options: OpenAPIGeneratorOptions): OpenAPISpec;
37
+ /**
38
+ * Generates an OpenAPI 3.0.3 specification from registered collections
39
+ *
40
+ * Each entry carries its own prefix, allowing collections registered under
41
+ * different Fastify prefixes (e.g., `/api` and `/loterie`) to produce
42
+ * correct paths in a single spec.
43
+ *
44
+ * @param entries - Registered collections with per-entry prefixes
45
+ * @param options - Generator options (prefix field is ignored — per-entry prefix is used)
46
+ * @returns Complete OpenAPI specification
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * import { generateOpenApiSpecFromRegistry, getRegisteredCollections } from '@veloxts/router';
51
+ *
52
+ * const spec = generateOpenApiSpecFromRegistry(getRegisteredCollections(), {
53
+ * info: { title: 'My API', version: '1.0.0' },
54
+ * });
55
+ * ```
56
+ */
57
+ export declare function generateOpenApiSpecFromRegistry(entries: readonly RegisteredCollection[], options: OpenAPIGeneratorOptions): OpenAPISpec;
33
58
  /**
34
59
  * Gets route summary information for debugging/logging
35
60
  *
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { generateRestRoutes } from '../rest/adapter.js';
9
9
  import { buildParameters, convertToOpenAPIPath, joinPaths } from './path-extractor.js';
10
- import { removeSchemaProperties, resourceSchemaToJsonSchema, zodSchemaToJsonSchema, } from './schema-converter.js';
10
+ import { removeSchemaProperties, resourceSchemaToJsonSchema, resourceSchemaToJsonSchemaForBranching, zodSchemaToJsonSchema, } from './schema-converter.js';
11
11
  import { extractUsedSecuritySchemes, filterUsedSecuritySchemes, guardsToSecurity, mergeSecuritySchemes, } from './security-mapper.js';
12
12
  // ============================================================================
13
13
  // Main Generator
@@ -15,6 +15,9 @@ import { extractUsedSecuritySchemes, filterUsedSecuritySchemes, guardsToSecurity
15
15
  /**
16
16
  * Generates an OpenAPI 3.0.3 specification from procedure collections
17
17
  *
18
+ * All collections share the same prefix from `options.prefix` (default '/api').
19
+ * For collections with different prefixes, use `generateOpenApiSpecFromRegistry()`.
20
+ *
18
21
  * @param collections - Array of procedure collections to document
19
22
  * @param options - Generator options
20
23
  * @returns Complete OpenAPI specification
@@ -36,20 +39,53 @@ import { extractUsedSecuritySchemes, filterUsedSecuritySchemes, guardsToSecurity
36
39
  */
37
40
  export function generateOpenApiSpec(collections, options) {
38
41
  const prefix = options.prefix ?? '/api';
42
+ // Convert to RegisteredCollection entries with the shared prefix
43
+ const entries = collections.map((collection) => ({
44
+ collection,
45
+ prefix,
46
+ }));
47
+ return generateOpenApiSpecFromRegistry(entries, options);
48
+ }
49
+ /**
50
+ * Generates an OpenAPI 3.0.3 specification from registered collections
51
+ *
52
+ * Each entry carries its own prefix, allowing collections registered under
53
+ * different Fastify prefixes (e.g., `/api` and `/loterie`) to produce
54
+ * correct paths in a single spec.
55
+ *
56
+ * @param entries - Registered collections with per-entry prefixes
57
+ * @param options - Generator options (prefix field is ignored — per-entry prefix is used)
58
+ * @returns Complete OpenAPI specification
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * import { generateOpenApiSpecFromRegistry, getRegisteredCollections } from '@veloxts/router';
63
+ *
64
+ * const spec = generateOpenApiSpecFromRegistry(getRegisteredCollections(), {
65
+ * info: { title: 'My API', version: '1.0.0' },
66
+ * });
67
+ * ```
68
+ */
69
+ export function generateOpenApiSpecFromRegistry(entries, options) {
39
70
  const paths = {};
40
71
  const tags = [];
41
72
  const allGuards = [];
42
- // Process each collection
43
- for (const collection of collections) {
73
+ const seenTagNames = new Set();
74
+ // Process each registered entry
75
+ for (const entry of entries) {
76
+ const { collection, prefix } = entry;
44
77
  const routes = generateRestRoutes(collection);
45
- // Add tag for this namespace
46
- tags.push({
47
- name: collection.namespace,
48
- description: options.tagDescriptions?.[collection.namespace],
49
- });
78
+ // Add tag for this namespace (deduplicate across entries)
79
+ if (!seenTagNames.has(collection.namespace)) {
80
+ seenTagNames.add(collection.namespace);
81
+ tags.push({
82
+ name: collection.namespace,
83
+ description: options.tagDescriptions?.[collection.namespace],
84
+ });
85
+ }
50
86
  // Process each route
51
87
  for (const route of routes) {
52
- // Build full path with prefix
88
+ // Build full path with this entry's prefix
53
89
  const fullPath = joinPaths(prefix, route.path);
54
90
  const openApiPath = convertToOpenAPIPath(fullPath);
55
91
  // Initialize path item if not exists
@@ -114,6 +150,10 @@ function generateOperation(route, namespace, options) {
114
150
  if (procedure.outputSchema) {
115
151
  outputSchema = zodSchemaToJsonSchema(procedure.outputSchema);
116
152
  }
153
+ else if (procedure._handlerMap && procedure._resourceSchema) {
154
+ // Level 3: branched procedure — include all fields, non-public fields optional
155
+ outputSchema = resourceSchemaToJsonSchemaForBranching(procedure._resourceSchema);
156
+ }
117
157
  else if (procedure._resourceSchema) {
118
158
  const level = procedure._resourceLevel ?? 'public';
119
159
  outputSchema = resourceSchemaToJsonSchema(procedure._resourceSchema, level);
@@ -28,7 +28,7 @@
28
28
  * });
29
29
  * ```
30
30
  */
31
- export { generateOpenApiSpec, getOpenApiRouteSummary, validateOpenApiSpec, } from './generator.js';
31
+ export { generateOpenApiSpec, generateOpenApiSpecFromRegistry, getOpenApiRouteSummary, validateOpenApiSpec, } from './generator.js';
32
32
  export { getOpenApiSpec, swaggerPlugin, } from './plugin.js';
33
33
  export { DEFAULT_UI_CONFIG, escapeHtml, generateSwaggerUIHtml, SWAGGER_UI_CDN, type SwaggerUIHtmlOptions, } from './html-generator.js';
34
34
  export { createStringSchema, extractSchemaProperties, mergeSchemas, removeSchemaProperties, type SchemaConversionOptions, schemaHasProperties, zodSchemaToJsonSchema, } from './schema-converter.js';
@@ -31,7 +31,7 @@
31
31
  // ============================================================================
32
32
  // Generator
33
33
  // ============================================================================
34
- export { generateOpenApiSpec, getOpenApiRouteSummary, validateOpenApiSpec, } from './generator.js';
34
+ export { generateOpenApiSpec, generateOpenApiSpecFromRegistry, getOpenApiRouteSummary, validateOpenApiSpec, } from './generator.js';
35
35
  // ============================================================================
36
36
  // Plugin
37
37
  // ============================================================================
@@ -12,22 +12,27 @@ import type { OpenAPISpec, SwaggerUIPluginOptions } from './types.js';
12
12
  *
13
13
  * Registers routes for serving Swagger UI and the OpenAPI specification.
14
14
  *
15
+ * When `collections` is omitted, the plugin auto-discovers all collections
16
+ * previously registered via `rest()`, using each collection's effective prefix.
17
+ *
15
18
  * @example
16
19
  * ```typescript
17
20
  * import { swaggerPlugin } from '@veloxts/router';
18
21
  *
22
+ * // Explicit collections (backward compatible)
19
23
  * app.register(swaggerPlugin, {
20
24
  * routePrefix: '/docs',
21
25
  * collections: [userProcedures, postProcedures],
22
26
  * openapi: {
23
- * info: {
24
- * title: 'My API',
25
- * version: '1.0.0',
26
- * description: 'A VeloxTS-powered API',
27
- * },
27
+ * info: { title: 'My API', version: '1.0.0' },
28
28
  * servers: [{ url: 'http://localhost:3030' }],
29
29
  * },
30
30
  * });
31
+ *
32
+ * // Auto-discovery — no collections needed
33
+ * app.register(swaggerPlugin, {
34
+ * openapi: { info: { title: 'My API', version: '1.0.0' } },
35
+ * });
31
36
  * ```
32
37
  */
33
38
  export declare const swaggerPlugin: FastifyPluginAsync<SwaggerUIPluginOptions>;
@@ -35,6 +40,7 @@ export declare const swaggerPlugin: FastifyPluginAsync<SwaggerUIPluginOptions>;
35
40
  * Gets the generated OpenAPI specification without registering routes
36
41
  *
37
42
  * Useful for testing or exporting the spec programmatically.
43
+ * Supports auto-discovery when `collections` is omitted.
38
44
  *
39
45
  * @param options - Plugin options
40
46
  * @returns Generated OpenAPI specification
@@ -5,9 +5,27 @@
5
5
  *
6
6
  * @module @veloxts/router/openapi/plugin
7
7
  */
8
- import { generateOpenApiSpec } from './generator.js';
8
+ import { getRegisteredCollections } from '../rest/registry.js';
9
+ import { generateOpenApiSpec, generateOpenApiSpecFromRegistry } from './generator.js';
9
10
  import { generateSwaggerUIHtml } from './html-generator.js';
10
11
  // ============================================================================
12
+ // Internal Helpers
13
+ // ============================================================================
14
+ /**
15
+ * Build an OpenAPI spec from plugin options.
16
+ *
17
+ * - Explicit `collections` → use `generateOpenApiSpec` (single global prefix)
18
+ * - No collections → auto-discover from the registry (per-entry prefix)
19
+ */
20
+ function buildSpec(options) {
21
+ const { collections, openapi } = options;
22
+ if (collections && collections.length > 0) {
23
+ return generateOpenApiSpec(collections, openapi);
24
+ }
25
+ const registered = getRegisteredCollections();
26
+ return generateOpenApiSpecFromRegistry(registered, openapi);
27
+ }
28
+ // ============================================================================
11
29
  // Fastify Plugin
12
30
  // ============================================================================
13
31
  /**
@@ -15,30 +33,35 @@ import { generateSwaggerUIHtml } from './html-generator.js';
15
33
  *
16
34
  * Registers routes for serving Swagger UI and the OpenAPI specification.
17
35
  *
36
+ * When `collections` is omitted, the plugin auto-discovers all collections
37
+ * previously registered via `rest()`, using each collection's effective prefix.
38
+ *
18
39
  * @example
19
40
  * ```typescript
20
41
  * import { swaggerPlugin } from '@veloxts/router';
21
42
  *
43
+ * // Explicit collections (backward compatible)
22
44
  * app.register(swaggerPlugin, {
23
45
  * routePrefix: '/docs',
24
46
  * collections: [userProcedures, postProcedures],
25
47
  * openapi: {
26
- * info: {
27
- * title: 'My API',
28
- * version: '1.0.0',
29
- * description: 'A VeloxTS-powered API',
30
- * },
48
+ * info: { title: 'My API', version: '1.0.0' },
31
49
  * servers: [{ url: 'http://localhost:3030' }],
32
50
  * },
33
51
  * });
52
+ *
53
+ * // Auto-discovery — no collections needed
54
+ * app.register(swaggerPlugin, {
55
+ * openapi: { info: { title: 'My API', version: '1.0.0' } },
56
+ * });
34
57
  * ```
35
58
  */
36
59
  export const swaggerPlugin = async (fastify, options) => {
37
- const { routePrefix = '/docs', specRoute = `${routePrefix}/openapi.json`, uiConfig = {}, openapi, collections, title = 'API Documentation', favicon, } = options;
60
+ const { routePrefix = '/docs', specRoute = `${routePrefix}/openapi.json`, uiConfig = {}, title = 'API Documentation', favicon, } = options;
38
61
  // Generate the OpenAPI specification
39
62
  let spec;
40
63
  try {
41
- spec = generateOpenApiSpec(collections, openapi);
64
+ spec = buildSpec(options);
42
65
  }
43
66
  catch (error) {
44
67
  fastify.log.error(error, '[VeloxTS] Failed to generate OpenAPI specification');
@@ -73,6 +96,7 @@ export const swaggerPlugin = async (fastify, options) => {
73
96
  * Gets the generated OpenAPI specification without registering routes
74
97
  *
75
98
  * Useful for testing or exporting the spec programmatically.
99
+ * Supports auto-discovery when `collections` is omitted.
76
100
  *
77
101
  * @param options - Plugin options
78
102
  * @returns Generated OpenAPI specification
@@ -93,5 +117,5 @@ export const swaggerPlugin = async (fastify, options) => {
93
117
  * ```
94
118
  */
95
119
  export function getOpenApiSpec(options) {
96
- return generateOpenApiSpec(options.collections, options.openapi);
120
+ return buildSpec(options);
97
121
  }
@@ -113,3 +113,14 @@ export declare function schemaHasProperties(schema: JSONSchema | undefined): boo
113
113
  * @returns JSON Schema with only the fields visible at the given level
114
114
  */
115
115
  export declare function resourceSchemaToJsonSchema(schema: ResourceSchema, level?: string): JSONSchema;
116
+ /**
117
+ * Converts a resource schema to JSON Schema for Level 3 branched procedures
118
+ *
119
+ * Includes ALL fields from the resource schema. Fields visible at the
120
+ * lowest level (first in the levels array) are marked as required;
121
+ * higher-level-only fields are optional.
122
+ *
123
+ * @param schema - The resource schema (must have _levelConfig for custom levels)
124
+ * @returns JSON Schema with all fields, non-public fields optional
125
+ */
126
+ export declare function resourceSchemaToJsonSchemaForBranching(schema: ResourceSchema): JSONSchema;
@@ -308,3 +308,50 @@ export function resourceSchemaToJsonSchema(schema, level = 'public') {
308
308
  ...(required.length > 0 ? { required } : {}),
309
309
  };
310
310
  }
311
+ /**
312
+ * Converts a resource schema to JSON Schema for Level 3 branched procedures
313
+ *
314
+ * Includes ALL fields from the resource schema. Fields visible at the
315
+ * lowest level (first in the levels array) are marked as required;
316
+ * higher-level-only fields are optional.
317
+ *
318
+ * @param schema - The resource schema (must have _levelConfig for custom levels)
319
+ * @returns JSON Schema with all fields, non-public fields optional
320
+ */
321
+ export function resourceSchemaToJsonSchemaForBranching(schema) {
322
+ const properties = {};
323
+ const required = [];
324
+ // Determine the lowest (most accessible) level
325
+ const schemaWithConfig = schema;
326
+ const lowestLevel = schemaWithConfig._levelConfig?.levels[0] ?? 'public';
327
+ for (const field of schema.fields) {
328
+ const runtimeField = field;
329
+ if (field.nestedSchema) {
330
+ const nestedJsonSchema = resourceSchemaToJsonSchemaForBranching(field.nestedSchema);
331
+ if (field.cardinality === 'many') {
332
+ properties[field.name] = { type: 'array', items: nestedJsonSchema };
333
+ }
334
+ else {
335
+ properties[field.name] = { ...nestedJsonSchema, nullable: true };
336
+ }
337
+ }
338
+ else if (field.schema) {
339
+ const fieldJsonSchema = zodSchemaToJsonSchema(field.schema);
340
+ if (fieldJsonSchema) {
341
+ properties[field.name] = fieldJsonSchema;
342
+ }
343
+ }
344
+ else {
345
+ properties[field.name] = {};
346
+ }
347
+ // Only public/lowest-level fields are required
348
+ if (isFieldVisibleToLevel(runtimeField, lowestLevel)) {
349
+ required.push(field.name);
350
+ }
351
+ }
352
+ return {
353
+ type: 'object',
354
+ properties,
355
+ ...(required.length > 0 ? { required } : {}),
356
+ };
357
+ }
@@ -404,9 +404,12 @@ export interface SwaggerUIPluginOptions {
404
404
  */
405
405
  openapi: OpenAPIGeneratorOptions;
406
406
  /**
407
- * Procedure collections to document
407
+ * Procedure collections to document.
408
+ *
409
+ * When omitted, the plugin auto-discovers collections registered via `rest()`
410
+ * along with their effective prefixes.
408
411
  */
409
- collections: ProcedureCollection[];
412
+ collections?: ProcedureCollection[];
410
413
  /**
411
414
  * Custom page title
412
415
  * @default 'API Documentation'