@veloxts/router 0.7.5 → 0.7.7

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,23 @@
1
1
  # @veloxts/router
2
2
 
3
+ ## 0.7.7
4
+
5
+ ### Patch Changes
6
+
7
+ - refactor(router): rename swaggerUIPlugin → swaggerPlugin, remove redundant exports
8
+ - Updated dependencies
9
+ - @veloxts/core@0.7.7
10
+ - @veloxts/validation@0.7.7
11
+
12
+ ## 0.7.6
13
+
14
+ ### Patch Changes
15
+
16
+ - feat(router): custom access levels for the Resource API + advanced Architectural Patterns
17
+ - Updated dependencies
18
+ - @veloxts/core@0.7.6
19
+ - @veloxts/validation@0.7.6
20
+
3
21
  ## 0.7.5
4
22
 
5
23
  ### Patch Changes
package/GUIDE.md CHANGED
@@ -28,8 +28,8 @@ 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 (same fields for all users)
32
- - `.resource(schema)` - Context-dependent output with field visibility
31
+ - `.output(schema)` - Validate output with Zod
32
+ - `.expose(schema)` - Context-dependent output with resource schema
33
33
  - `.use(middleware)` - Add middleware
34
34
  - `.guard(guard)` - Add authorization guard
35
35
  - `.guardNarrow(guard)` - Add guard with TypeScript type narrowing
@@ -198,9 +198,9 @@ fs.writeFileSync('openapi.json', JSON.stringify(spec, null, 2));
198
198
  Serve interactive API documentation with Swagger UI:
199
199
 
200
200
  ```typescript
201
- import { swaggerUIPlugin } from '@veloxts/router';
201
+ import { swaggerPlugin } from '@veloxts/router';
202
202
 
203
- app.server.register(swaggerUIPlugin, {
203
+ app.server.register(swaggerPlugin, {
204
204
  routePrefix: '/docs',
205
205
  collections: [userProcedures, postProcedures],
206
206
  openapi: {
@@ -215,31 +215,6 @@ app.server.register(swaggerUIPlugin, {
215
215
  // - /docs/openapi.json - Raw OpenAPI spec
216
216
  ```
217
217
 
218
- ### Factory Functions
219
-
220
- ```typescript
221
- import { createSwaggerUI, getOpenApiSpec, registerDocs } from '@veloxts/router';
222
-
223
- // Create pre-configured plugin
224
- const docs = createSwaggerUI({
225
- collections: [userProcedures],
226
- openapi: { info: { title: 'My API', version: '1.0.0' } },
227
- });
228
- app.server.register(docs);
229
-
230
- // Get spec without registering routes
231
- const spec = getOpenApiSpec({
232
- collections: [userProcedures],
233
- openapi: { info: { title: 'My API', version: '1.0.0' } },
234
- });
235
-
236
- // Register docs with one call
237
- await registerDocs(app.server, {
238
- collections: [userProcedures],
239
- openapi: { info: { title: 'My API', version: '1.0.0' } },
240
- });
241
- ```
242
-
243
218
  ### CLI Commands
244
219
 
245
220
  Generate and serve OpenAPI specs from the command line:
@@ -430,7 +405,7 @@ console.table(routes);
430
405
 
431
406
  ## Resource API (Context-Dependent Outputs)
432
407
 
433
- The Resource API provides context-dependent output types using phantom types. Unlike `.output()` which returns the same fields to all users, `.resource()` lets you define field visibility per access level.
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.
434
409
 
435
410
  ### Defining a Resource Schema
436
411
 
@@ -450,7 +425,7 @@ const UserSchema = resourceSchema()
450
425
 
451
426
  ### Automatic Projection (Simple Cases)
452
427
 
453
- The most elegant approach is to chain `.resource()` with a narrowing guard. The procedure executor automatically projects fields based on the guard's access level:
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:
454
429
 
455
430
  ```typescript
456
431
  import { authenticatedNarrow, adminNarrow } from '@veloxts/auth';
@@ -459,7 +434,7 @@ export const userProcedures = procedures('users', {
459
434
  // Authenticated endpoint - auto-projects { id, name, email, createdAt }
460
435
  getProfile: procedure()
461
436
  .guardNarrow(authenticatedNarrow)
462
- .resource(UserSchema)
437
+ .expose(UserSchema)
463
438
  .input(z.object({ id: z.string().uuid() }))
464
439
  .query(async ({ input, ctx }) => {
465
440
  // Just return the full data - projection is automatic!
@@ -469,7 +444,7 @@ export const userProcedures = procedures('users', {
469
444
  // Admin endpoint - auto-projects all fields
470
445
  getFullProfile: procedure()
471
446
  .guardNarrow(adminNarrow)
472
- .resource(UserSchema)
447
+ .expose(UserSchema)
473
448
  .input(z.object({ id: z.string().uuid() }))
474
449
  .query(async ({ input, ctx }) => {
475
450
  return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
@@ -544,7 +519,7 @@ For arrays of items, use `resourceCollection()`:
544
519
  // Automatic projection (simple cases)
545
520
  const listUsers = procedure()
546
521
  .guardNarrow(authenticatedNarrow)
547
- .resource(UserSchema)
522
+ .expose(UserSchema)
548
523
  .query(async ({ ctx }) => {
549
524
  return ctx.db.user.findMany({ take: 50 });
550
525
  });
@@ -590,7 +565,7 @@ The Resource API provides compile-time type safety:
590
565
  // Automatic projection - type inferred from guard
591
566
  const getProfile = procedure()
592
567
  .guardNarrow(authenticatedNarrow)
593
- .resource(UserSchema)
568
+ .expose(UserSchema)
594
569
  .query(async ({ ctx }) => {
595
570
  return ctx.db.user.findFirst();
596
571
  });
@@ -611,7 +586,7 @@ const adminResult = resource(user, UserSchema.admin);
611
586
 
612
587
  | Scenario | Approach |
613
588
  |----------|----------|
614
- | Guard determines access level | **Automatic** (`.guardNarrow().resource()`) |
589
+ | Guard determines access level | **Automatic** (`.guardNarrow().output()`) |
615
590
  | Public endpoints (no guard) | Tagged view (`UserSchema.public`) |
616
591
  | Conditional/dynamic projection | Tagged view or `.for(ctx)` in handler |
617
592
  | Simple, declarative code | **Automatic** |
package/dist/index.d.ts CHANGED
@@ -39,7 +39,7 @@
39
39
  */
40
40
  /** Router package version */
41
41
  export declare const ROUTER_VERSION: string;
42
- export type { CompiledProcedure, ContextExtensions, ContextFactory, ExtendedContext, GuardLike, HttpMethod, InferProcedureContext, InferProcedureInput, InferProcedureOutput, MiddlewareArgs, MiddlewareFunction, MiddlewareNext, MiddlewareResult, ParentResourceChain, ParentResourceConfig, ProcedureCollection, ProcedureHandler, ProcedureHandlerArgs, ProcedureRecord, ProcedureType, RestRouteOverride, } from './types.js';
42
+ export type { CompiledProcedure, ContextExtensions, ContextFactory, ExtendedContext, GuardLike, HttpMethod, InferProcedureContext, InferProcedureInput, InferProcedureOutput, Middleware, MiddlewareArgs, MiddlewareFunction, MiddlewareNext, MiddlewareResult, ParentResourceChain, ParentResourceConfig, ProcedureCollection, ProcedureHandler, ProcedureHandlerArgs, ProcedureRecord, ProcedureType, RestRouteOverride, } from './types.js';
43
43
  export { PROCEDURE_METHOD_MAP, } from './types.js';
44
44
  export type { GuardErrorResponse, RouterErrorCode } from './errors.js';
45
45
  export { GuardError, isGuardError } from './errors.js';
@@ -66,7 +66,7 @@ export { serve } from './expose.js';
66
66
  *
67
67
  * @example
68
68
  * ```typescript
69
- * import { generateOpenApiSpec, swaggerUIPlugin } from '@veloxts/router';
69
+ * import { generateOpenApiSpec, swaggerPlugin } from '@veloxts/router';
70
70
  *
71
71
  * // Generate spec programmatically
72
72
  * const spec = generateOpenApiSpec([userProcedures], {
@@ -74,7 +74,7 @@ export { serve } from './expose.js';
74
74
  * });
75
75
  *
76
76
  * // Or serve Swagger UI
77
- * app.register(swaggerUIPlugin, {
77
+ * app.register(swaggerPlugin, {
78
78
  * routePrefix: '/docs',
79
79
  * collections: [userProcedures],
80
80
  * openapi: { info: { title: 'My API', version: '1.0.0' } },
@@ -82,8 +82,8 @@ 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, createSwaggerUI, 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, registerDocs, removeSchemaProperties, SWAGGER_UI_CDN, schemaHasProperties, swaggerUIPlugin, validateOpenApiSpec, zodSchemaToJsonSchema, } from './openapi/index.js';
86
- export type { AccessLevel, ADMIN, AdminOutput, AdminTaggedContext, ANONYMOUS, AnonymousOutput, AnonymousTaggedContext, AnyResourceOutput, AUTHENTICATED, AuthenticatedOutput, AuthenticatedTaggedContext, BuilderField, ContextTag, ExtractTag, HasTag, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, IsVisibleToTag, OutputForTag, RelationField, ResourceField, ResourceSchema, RuntimeField, TaggedContext, VisibilityLevel, WithTag, } from './resource/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';
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.
89
89
  *
@@ -109,9 +109,9 @@ export type { AccessLevel, ADMIN, AdminOutput, AdminTaggedContext, ANONYMOUS, An
109
109
  * .input(z.object({ id: z.string() }))
110
110
  * .query(async ({ input, ctx }) => {
111
111
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
112
- * return resource(user, UserSchema).forAnonymous();
112
+ * return resource(user, UserSchema).forPublic();
113
113
  * }),
114
114
  * });
115
115
  * ```
116
116
  */
117
- export { getAccessibleLevels, getVisibilityForTag, isResourceSchema, isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder, resource, resourceCollection, resourceSchema, } from './resource/index.js';
117
+ export { defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema, isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder, resource, resourceCollection, resourceSchema, } from './resource/index.js';
package/dist/index.js CHANGED
@@ -71,13 +71,13 @@ buildParameters, convertFromOpenAPIPath, convertToOpenAPIPath,
71
71
  // Security mapper
72
72
  createSecurityRequirement,
73
73
  // Schema converter
74
- createStringSchema,
75
- // Plugin
76
- createSwaggerUI, DEFAULT_GUARD_MAPPINGS, DEFAULT_SECURITY_SCHEMES, DEFAULT_UI_CONFIG, escapeHtml, extractGuardScopes, extractPathParamNames, extractQueryParameters, extractResourceFromPath, extractSchemaProperties, extractUsedSecuritySchemes, filterUsedSecuritySchemes,
74
+ createStringSchema, DEFAULT_GUARD_MAPPINGS, DEFAULT_SECURITY_SCHEMES, DEFAULT_UI_CONFIG, escapeHtml, extractGuardScopes, extractPathParamNames, extractQueryParameters, extractResourceFromPath, extractSchemaProperties, extractUsedSecuritySchemes, filterUsedSecuritySchemes,
77
75
  // Generator
78
76
  generateOpenApiSpec,
79
77
  // HTML Generator
80
- generateSwaggerUIHtml, getOpenApiRouteSummary, getOpenApiSpec, guardsRequireAuth, guardsToSecurity, hasPathParameters, joinPaths, mapGuardToSecurity, mergeSchemas, mergeSecuritySchemes, normalizePath, parsePathParameters, registerDocs, removeSchemaProperties, SWAGGER_UI_CDN, schemaHasProperties, swaggerUIPlugin, validateOpenApiSpec, zodSchemaToJsonSchema, } from './openapi/index.js';
78
+ generateSwaggerUIHtml, getOpenApiRouteSummary, getOpenApiSpec, guardsRequireAuth, guardsToSecurity, hasPathParameters, joinPaths, mapGuardToSecurity, mergeSchemas, mergeSecuritySchemes, normalizePath, parsePathParameters, removeSchemaProperties, SWAGGER_UI_CDN, schemaHasProperties,
79
+ // Plugin
80
+ swaggerPlugin, validateOpenApiSpec, zodSchemaToJsonSchema, } from './openapi/index.js';
81
81
  /**
82
82
  * Resource API for context-dependent output types using phantom types.
83
83
  *
@@ -103,12 +103,14 @@ generateSwaggerUIHtml, getOpenApiRouteSummary, getOpenApiSpec, guardsRequireAuth
103
103
  * .input(z.object({ id: z.string() }))
104
104
  * .query(async ({ input, ctx }) => {
105
105
  * const user = await ctx.db.user.findUnique({ where: { id: input.id } });
106
- * return resource(user, UserSchema).forAnonymous();
106
+ * return resource(user, UserSchema).forPublic();
107
107
  * }),
108
108
  * });
109
109
  * ```
110
110
  */
111
- export { getAccessibleLevels, getVisibilityForTag, isResourceSchema,
111
+ export {
112
+ // Access level configuration
113
+ defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema,
112
114
  // Visibility
113
115
  isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder,
114
116
  // Resource instances
@@ -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, zodSchemaToJsonSchema } from './schema-converter.js';
10
+ import { removeSchemaProperties, resourceSchemaToJsonSchema, zodSchemaToJsonSchema, } from './schema-converter.js';
11
11
  import { extractUsedSecuritySchemes, filterUsedSecuritySchemes, guardsToSecurity, mergeSecuritySchemes, } from './security-mapper.js';
12
12
  // ============================================================================
13
13
  // Main Generator
@@ -110,9 +110,14 @@ function generateOperation(route, namespace, options) {
110
110
  const inputSchema = procedure.inputSchema
111
111
  ? zodSchemaToJsonSchema(procedure.inputSchema)
112
112
  : undefined;
113
- const outputSchema = procedure.outputSchema
114
- ? zodSchemaToJsonSchema(procedure.outputSchema)
115
- : undefined;
113
+ let outputSchema;
114
+ if (procedure.outputSchema) {
115
+ outputSchema = zodSchemaToJsonSchema(procedure.outputSchema);
116
+ }
117
+ else if (procedure._resourceSchema) {
118
+ const level = procedure._resourceLevel ?? 'public';
119
+ outputSchema = resourceSchemaToJsonSchema(procedure._resourceSchema, level);
120
+ }
116
121
  // Build parameters
117
122
  const { pathParams, queryParams, pathParamNames } = buildParameters({
118
123
  path,
@@ -9,8 +9,7 @@
9
9
  * ```typescript
10
10
  * import {
11
11
  * generateOpenApiSpec,
12
- * swaggerUIPlugin,
13
- * createSwaggerUI,
12
+ * swaggerPlugin,
14
13
  * } from '@veloxts/router';
15
14
  *
16
15
  * // Generate spec programmatically
@@ -20,7 +19,7 @@
20
19
  * });
21
20
  *
22
21
  * // Or register Swagger UI plugin
23
- * app.register(swaggerUIPlugin, {
22
+ * app.register(swaggerPlugin, {
24
23
  * routePrefix: '/docs',
25
24
  * collections: [userProcedures],
26
25
  * openapi: {
@@ -30,7 +29,7 @@
30
29
  * ```
31
30
  */
32
31
  export { generateOpenApiSpec, getOpenApiRouteSummary, validateOpenApiSpec, } from './generator.js';
33
- export { createSwaggerUI, getOpenApiSpec, registerDocs, swaggerUIPlugin, } from './plugin.js';
32
+ export { getOpenApiSpec, swaggerPlugin, } from './plugin.js';
34
33
  export { DEFAULT_UI_CONFIG, escapeHtml, generateSwaggerUIHtml, SWAGGER_UI_CDN, type SwaggerUIHtmlOptions, } from './html-generator.js';
35
34
  export { createStringSchema, extractSchemaProperties, mergeSchemas, removeSchemaProperties, type SchemaConversionOptions, schemaHasProperties, zodSchemaToJsonSchema, } from './schema-converter.js';
36
35
  export { type BuildParametersOptions, type BuildParametersResult, buildParameters, convertFromOpenAPIPath, convertToOpenAPIPath, extractPathParamNames, extractQueryParameters, extractResourceFromPath, hasPathParameters, joinPaths, normalizePath, parsePathParameters, type QueryParamExtractionOptions, } from './path-extractor.js';
@@ -9,8 +9,7 @@
9
9
  * ```typescript
10
10
  * import {
11
11
  * generateOpenApiSpec,
12
- * swaggerUIPlugin,
13
- * createSwaggerUI,
12
+ * swaggerPlugin,
14
13
  * } from '@veloxts/router';
15
14
  *
16
15
  * // Generate spec programmatically
@@ -20,7 +19,7 @@
20
19
  * });
21
20
  *
22
21
  * // Or register Swagger UI plugin
23
- * app.register(swaggerUIPlugin, {
22
+ * app.register(swaggerPlugin, {
24
23
  * routePrefix: '/docs',
25
24
  * collections: [userProcedures],
26
25
  * openapi: {
@@ -36,7 +35,7 @@ export { generateOpenApiSpec, getOpenApiRouteSummary, validateOpenApiSpec, } fro
36
35
  // ============================================================================
37
36
  // Plugin
38
37
  // ============================================================================
39
- export { createSwaggerUI, getOpenApiSpec, registerDocs, swaggerUIPlugin, } from './plugin.js';
38
+ export { getOpenApiSpec, swaggerPlugin, } from './plugin.js';
40
39
  // ============================================================================
41
40
  // HTML Generator
42
41
  // ============================================================================
@@ -14,9 +14,9 @@ import type { OpenAPISpec, SwaggerUIPluginOptions } from './types.js';
14
14
  *
15
15
  * @example
16
16
  * ```typescript
17
- * import { swaggerUIPlugin } from '@veloxts/router';
17
+ * import { swaggerPlugin } from '@veloxts/router';
18
18
  *
19
- * app.register(swaggerUIPlugin, {
19
+ * app.register(swaggerPlugin, {
20
20
  * routePrefix: '/docs',
21
21
  * collections: [userProcedures, postProcedures],
22
22
  * openapi: {
@@ -30,51 +30,7 @@ import type { OpenAPISpec, SwaggerUIPluginOptions } from './types.js';
30
30
  * });
31
31
  * ```
32
32
  */
33
- export declare const swaggerUIPlugin: FastifyPluginAsync<SwaggerUIPluginOptions>;
34
- /**
35
- * Creates a Swagger UI plugin with pre-configured options
36
- *
37
- * @param options - Plugin options
38
- * @returns Configured plugin
39
- *
40
- * @example
41
- * ```typescript
42
- * import { createSwaggerUI } from '@veloxts/router';
43
- *
44
- * const docs = createSwaggerUI({
45
- * collections: [userProcedures],
46
- * openapi: {
47
- * info: { title: 'My API', version: '1.0.0' },
48
- * },
49
- * });
50
- *
51
- * app.register(docs);
52
- * ```
53
- */
54
- export declare function createSwaggerUI(options: SwaggerUIPluginOptions): FastifyPluginAsync<SwaggerUIPluginOptions>;
55
- /**
56
- * Registers multiple procedure collections with Swagger UI
57
- *
58
- * Convenience function that sets up both REST routes and documentation.
59
- *
60
- * @param fastify - Fastify instance
61
- * @param options - Documentation options
62
- *
63
- * @example
64
- * ```typescript
65
- * import { registerDocs } from '@veloxts/router';
66
- *
67
- * await registerDocs(app, {
68
- * collections: [userProcedures, postProcedures],
69
- * openapi: {
70
- * info: { title: 'My API', version: '1.0.0' },
71
- * },
72
- * });
73
- * ```
74
- */
75
- export declare function registerDocs(fastify: {
76
- register: (plugin: FastifyPluginAsync<SwaggerUIPluginOptions>, options: SwaggerUIPluginOptions) => Promise<void>;
77
- }, options: SwaggerUIPluginOptions): Promise<void>;
33
+ export declare const swaggerPlugin: FastifyPluginAsync<SwaggerUIPluginOptions>;
78
34
  /**
79
35
  * Gets the generated OpenAPI specification without registering routes
80
36
  *
@@ -17,9 +17,9 @@ import { generateSwaggerUIHtml } from './html-generator.js';
17
17
  *
18
18
  * @example
19
19
  * ```typescript
20
- * import { swaggerUIPlugin } from '@veloxts/router';
20
+ * import { swaggerPlugin } from '@veloxts/router';
21
21
  *
22
- * app.register(swaggerUIPlugin, {
22
+ * app.register(swaggerPlugin, {
23
23
  * routePrefix: '/docs',
24
24
  * collections: [userProcedures, postProcedures],
25
25
  * openapi: {
@@ -33,7 +33,7 @@ import { generateSwaggerUIHtml } from './html-generator.js';
33
33
  * });
34
34
  * ```
35
35
  */
36
- export const swaggerUIPlugin = async (fastify, options) => {
36
+ export const swaggerPlugin = async (fastify, options) => {
37
37
  const { routePrefix = '/docs', specRoute = `${routePrefix}/openapi.json`, uiConfig = {}, openapi, collections, title = 'API Documentation', favicon, } = options;
38
38
  // Generate the OpenAPI specification
39
39
  let spec;
@@ -69,54 +69,6 @@ export const swaggerUIPlugin = async (fastify, options) => {
69
69
  // ============================================================================
70
70
  // Utility Functions
71
71
  // ============================================================================
72
- /**
73
- * Creates a Swagger UI plugin with pre-configured options
74
- *
75
- * @param options - Plugin options
76
- * @returns Configured plugin
77
- *
78
- * @example
79
- * ```typescript
80
- * import { createSwaggerUI } from '@veloxts/router';
81
- *
82
- * const docs = createSwaggerUI({
83
- * collections: [userProcedures],
84
- * openapi: {
85
- * info: { title: 'My API', version: '1.0.0' },
86
- * },
87
- * });
88
- *
89
- * app.register(docs);
90
- * ```
91
- */
92
- export function createSwaggerUI(options) {
93
- return async (fastify) => {
94
- await swaggerUIPlugin(fastify, options);
95
- };
96
- }
97
- /**
98
- * Registers multiple procedure collections with Swagger UI
99
- *
100
- * Convenience function that sets up both REST routes and documentation.
101
- *
102
- * @param fastify - Fastify instance
103
- * @param options - Documentation options
104
- *
105
- * @example
106
- * ```typescript
107
- * import { registerDocs } from '@veloxts/router';
108
- *
109
- * await registerDocs(app, {
110
- * collections: [userProcedures, postProcedures],
111
- * openapi: {
112
- * info: { title: 'My API', version: '1.0.0' },
113
- * },
114
- * });
115
- * ```
116
- */
117
- export async function registerDocs(fastify, options) {
118
- await fastify.register(swaggerUIPlugin, options);
119
- }
120
72
  /**
121
73
  * Gets the generated OpenAPI specification without registering routes
122
74
  *
@@ -6,6 +6,7 @@
6
6
  * @module @veloxts/router/openapi/schema-converter
7
7
  */
8
8
  import { type ZodType } from 'zod';
9
+ import type { ResourceSchema } from '../resource/schema.js';
9
10
  import type { JSONSchema } from './types.js';
10
11
  /**
11
12
  * Options for Zod to JSON Schema conversion
@@ -100,3 +101,15 @@ export declare function createStringSchema(format?: string): JSONSchema;
100
101
  * Checks if a schema has any properties
101
102
  */
102
103
  export declare function schemaHasProperties(schema: JSONSchema | undefined): boolean;
104
+ /**
105
+ * Converts a ResourceSchema to JSON Schema for OpenAPI, filtered by visibility level
106
+ *
107
+ * Iterates the resource's field definitions and includes only fields
108
+ * visible at the given access level. Nested resource schemas are
109
+ * converted recursively.
110
+ *
111
+ * @param schema - The resource schema
112
+ * @param level - The access level to generate documentation for (defaults to 'public')
113
+ * @returns JSON Schema with only the fields visible at the given level
114
+ */
115
+ export declare function resourceSchemaToJsonSchema(schema: ResourceSchema, level?: string): JSONSchema;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { createLogger } from '@veloxts/core';
9
9
  import { z } from 'zod';
10
+ import { isFieldVisibleToLevel } from '../resource/visibility.js';
10
11
  const log = createLogger('router');
11
12
  /**
12
13
  * Maps our target names to Zod 4's native `z.toJSONSchema()` target names.
@@ -257,3 +258,53 @@ export function schemaHasProperties(schema) {
257
258
  return false;
258
259
  return Object.keys(schema.properties).length > 0;
259
260
  }
261
+ // ============================================================================
262
+ // Resource Schema to JSON Schema
263
+ // ============================================================================
264
+ /**
265
+ * Converts a ResourceSchema to JSON Schema for OpenAPI, filtered by visibility level
266
+ *
267
+ * Iterates the resource's field definitions and includes only fields
268
+ * visible at the given access level. Nested resource schemas are
269
+ * converted recursively.
270
+ *
271
+ * @param schema - The resource schema
272
+ * @param level - The access level to generate documentation for (defaults to 'public')
273
+ * @returns JSON Schema with only the fields visible at the given level
274
+ */
275
+ export function resourceSchemaToJsonSchema(schema, level = 'public') {
276
+ const properties = {};
277
+ const required = [];
278
+ for (const field of schema.fields) {
279
+ if (!isFieldVisibleToLevel(field, level)) {
280
+ continue;
281
+ }
282
+ if (field.nestedSchema) {
283
+ // Nested relation — recurse
284
+ const nestedJsonSchema = resourceSchemaToJsonSchema(field.nestedSchema, level);
285
+ if (field.cardinality === 'many') {
286
+ properties[field.name] = { type: 'array', items: nestedJsonSchema };
287
+ }
288
+ else {
289
+ properties[field.name] = { ...nestedJsonSchema, nullable: true };
290
+ }
291
+ }
292
+ else if (field.schema) {
293
+ // Scalar field with Zod schema
294
+ const fieldJsonSchema = zodSchemaToJsonSchema(field.schema);
295
+ if (fieldJsonSchema) {
296
+ properties[field.name] = fieldJsonSchema;
297
+ }
298
+ }
299
+ else {
300
+ // Field without schema — generic
301
+ properties[field.name] = {};
302
+ }
303
+ required.push(field.name);
304
+ }
305
+ return {
306
+ type: 'object',
307
+ properties,
308
+ ...(required.length > 0 ? { required } : {}),
309
+ };
310
+ }
@@ -85,15 +85,26 @@ function createBuilder(state) {
85
85
  });
86
86
  },
87
87
  /**
88
- * Sets the output validation schema
88
+ * Sets the output validation schema (Zod-only)
89
89
  */
90
90
  output(schema) {
91
- // Return new builder with updated output schema
92
91
  return createBuilder({
93
92
  ...state,
94
93
  outputSchema: schema,
95
94
  });
96
95
  },
96
+ /**
97
+ * Sets field-level visibility via a resource schema
98
+ */
99
+ expose(schema) {
100
+ const level = isTaggedResourceSchema(schema) ? schema._level : undefined;
101
+ return createBuilder({
102
+ ...state,
103
+ resourceSchema: schema,
104
+ resourceLevel: level,
105
+ outputSchema: undefined,
106
+ });
107
+ },
97
108
  /**
98
109
  * Adds middleware to the chain
99
110
  */
@@ -151,6 +162,16 @@ function createBuilder(state) {
151
162
  restOverride: config,
152
163
  });
153
164
  },
165
+ /**
166
+ * Configures the procedure as a webhook endpoint
167
+ */
168
+ webhook(path) {
169
+ return createBuilder({
170
+ ...state,
171
+ restOverride: { method: 'POST', path },
172
+ isWebhook: true,
173
+ });
174
+ },
154
175
  /**
155
176
  * Marks the procedure as deprecated
156
177
  */
@@ -200,10 +221,7 @@ function createBuilder(state) {
200
221
  return compileProcedure('mutation', handler, state);
201
222
  },
202
223
  /**
203
- * Sets the output type based on a resource schema
204
- *
205
- * Accepts either a plain `ResourceSchema` or a tagged schema
206
- * (e.g., `UserSchema.authenticated`) for declarative auto-projection.
224
+ * @deprecated Use `.expose()` instead. `.resource()` will be removed in v1.0.
207
225
  */
208
226
  resource(schema) {
209
227
  const level = isTaggedResourceSchema(schema) ? schema._level : undefined;
@@ -211,6 +229,7 @@ function createBuilder(state) {
211
229
  ...state,
212
230
  resourceSchema: schema,
213
231
  resourceLevel: level,
232
+ outputSchema: undefined,
214
233
  });
215
234
  },
216
235
  };
@@ -240,6 +259,7 @@ function compileProcedure(type, handler, state) {
240
259
  restOverride: state.restOverride,
241
260
  deprecated: state.deprecated,
242
261
  deprecationMessage: state.deprecationMessage,
262
+ isWebhook: state.isWebhook,
243
263
  parentResource: state.parentResource,
244
264
  parentResources: state.parentResources,
245
265
  // Store pre-compiled executor for performance
@@ -413,14 +433,14 @@ export async function executeProcedure(procedure, rawInput, ctx) {
413
433
  const message = guard.message ?? `Guard "${guard.name}" check failed`;
414
434
  throw new GuardError(guard.name, message, statusCode);
415
435
  }
416
- // Track highest access level from narrowing guards
436
+ // Track access level from narrowing guards.
437
+ // IMPORTANT: last guard's accessLevel wins. With custom levels that
438
+ // have no inherent hierarchy, ordering of guards matters.
439
+ // Guards without accessLevel (e.g. plain `authenticated`) do NOT
440
+ // update the level — it stays at 'public' for .expose() projection.
417
441
  const guardWithLevel = guard;
418
442
  if (guardWithLevel.accessLevel) {
419
- // Admin > authenticated > public
420
- if (guardWithLevel.accessLevel === 'admin' ||
421
- (guardWithLevel.accessLevel === 'authenticated' && accessLevel === 'public')) {
422
- accessLevel = guardWithLevel.accessLevel;
423
- }
443
+ accessLevel = guardWithLevel.accessLevel;
424
444
  }
425
445
  }
426
446
  }
@@ -451,15 +471,7 @@ export async function executeProcedure(procedure, rawInput, ctx) {
451
471
  // Prefer explicit level from tagged schema over guard-derived level
452
472
  const finalLevel = procedure._resourceLevel ?? accessLevel;
453
473
  const projectOne = (item) => {
454
- const r = new Resource(item, schema);
455
- switch (finalLevel) {
456
- case 'admin':
457
- return r.forAdmin();
458
- case 'authenticated':
459
- return r.forAuthenticated();
460
- default:
461
- return r.forAnonymous();
462
- }
474
+ return new Resource(item, schema).forLevel(finalLevel);
463
475
  };
464
476
  if (Array.isArray(result)) {
465
477
  result = result.map((item) => projectOne(item));