@veloxts/router 0.7.4 → 0.7.6
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 +9 -9
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -2
- package/dist/openapi/generator.js +9 -4
- package/dist/openapi/schema-converter.d.ts +13 -0
- package/dist/openapi/schema-converter.js +51 -0
- package/dist/procedure/builder.js +33 -21
- package/dist/procedure/types.d.ts +67 -28
- package/dist/resource/index.d.ts +7 -5
- package/dist/resource/index.js +3 -2
- package/dist/resource/instance.d.ts +38 -20
- package/dist/resource/instance.js +51 -44
- package/dist/resource/levels.d.ts +71 -0
- package/dist/resource/levels.js +109 -0
- package/dist/resource/schema.d.ts +151 -159
- package/dist/resource/schema.js +132 -124
- package/dist/resource/tags.d.ts +35 -15
- package/dist/resource/tags.js +1 -1
- package/dist/resource/types.d.ts +10 -8
- package/dist/resource/visibility.d.ts +16 -3
- package/dist/resource/visibility.js +16 -0
- package/dist/types.d.ts +28 -5
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @veloxts/router
|
|
2
2
|
|
|
3
|
+
## 0.7.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat(router): custom access levels for the Resource API + advanced Architectural Patterns
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @veloxts/core@0.7.6
|
|
10
|
+
- @veloxts/validation@0.7.6
|
|
11
|
+
|
|
12
|
+
## 0.7.5
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- fix(cli): address sync command review findings
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- @veloxts/core@0.7.5
|
|
19
|
+
- @veloxts/validation@0.7.5
|
|
20
|
+
|
|
3
21
|
## 0.7.4
|
|
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
|
|
32
|
-
- `.
|
|
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
|
|
@@ -430,7 +430,7 @@ console.table(routes);
|
|
|
430
430
|
|
|
431
431
|
## Resource API (Context-Dependent Outputs)
|
|
432
432
|
|
|
433
|
-
The Resource API provides context-dependent output types using phantom types.
|
|
433
|
+
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
434
|
|
|
435
435
|
### Defining a Resource Schema
|
|
436
436
|
|
|
@@ -450,7 +450,7 @@ const UserSchema = resourceSchema()
|
|
|
450
450
|
|
|
451
451
|
### Automatic Projection (Simple Cases)
|
|
452
452
|
|
|
453
|
-
The most elegant approach is to chain `.
|
|
453
|
+
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
454
|
|
|
455
455
|
```typescript
|
|
456
456
|
import { authenticatedNarrow, adminNarrow } from '@veloxts/auth';
|
|
@@ -459,7 +459,7 @@ export const userProcedures = procedures('users', {
|
|
|
459
459
|
// Authenticated endpoint - auto-projects { id, name, email, createdAt }
|
|
460
460
|
getProfile: procedure()
|
|
461
461
|
.guardNarrow(authenticatedNarrow)
|
|
462
|
-
.
|
|
462
|
+
.expose(UserSchema)
|
|
463
463
|
.input(z.object({ id: z.string().uuid() }))
|
|
464
464
|
.query(async ({ input, ctx }) => {
|
|
465
465
|
// Just return the full data - projection is automatic!
|
|
@@ -469,7 +469,7 @@ export const userProcedures = procedures('users', {
|
|
|
469
469
|
// Admin endpoint - auto-projects all fields
|
|
470
470
|
getFullProfile: procedure()
|
|
471
471
|
.guardNarrow(adminNarrow)
|
|
472
|
-
.
|
|
472
|
+
.expose(UserSchema)
|
|
473
473
|
.input(z.object({ id: z.string().uuid() }))
|
|
474
474
|
.query(async ({ input, ctx }) => {
|
|
475
475
|
return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
|
|
@@ -544,7 +544,7 @@ For arrays of items, use `resourceCollection()`:
|
|
|
544
544
|
// Automatic projection (simple cases)
|
|
545
545
|
const listUsers = procedure()
|
|
546
546
|
.guardNarrow(authenticatedNarrow)
|
|
547
|
-
.
|
|
547
|
+
.expose(UserSchema)
|
|
548
548
|
.query(async ({ ctx }) => {
|
|
549
549
|
return ctx.db.user.findMany({ take: 50 });
|
|
550
550
|
});
|
|
@@ -590,7 +590,7 @@ The Resource API provides compile-time type safety:
|
|
|
590
590
|
// Automatic projection - type inferred from guard
|
|
591
591
|
const getProfile = procedure()
|
|
592
592
|
.guardNarrow(authenticatedNarrow)
|
|
593
|
-
.
|
|
593
|
+
.expose(UserSchema)
|
|
594
594
|
.query(async ({ ctx }) => {
|
|
595
595
|
return ctx.db.user.findFirst();
|
|
596
596
|
});
|
|
@@ -611,7 +611,7 @@ const adminResult = resource(user, UserSchema.admin);
|
|
|
611
611
|
|
|
612
612
|
| Scenario | Approach |
|
|
613
613
|
|----------|----------|
|
|
614
|
-
| Guard determines access level | **Automatic** (`.guardNarrow().
|
|
614
|
+
| Guard determines access level | **Automatic** (`.guardNarrow().output()`) |
|
|
615
615
|
| Public endpoints (no guard) | Tagged view (`UserSchema.public`) |
|
|
616
616
|
| Conditional/dynamic projection | Tagged view or `.for(ctx)` in handler |
|
|
617
617
|
| 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';
|
|
@@ -83,7 +83,7 @@ export { serve } from './expose.js';
|
|
|
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
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';
|
|
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).
|
|
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
|
@@ -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).
|
|
106
|
+
* return resource(user, UserSchema).forPublic();
|
|
107
107
|
* }),
|
|
108
108
|
* });
|
|
109
109
|
* ```
|
|
110
110
|
*/
|
|
111
|
-
export {
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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,
|
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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));
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { BaseContext } from '@veloxts/core';
|
|
13
13
|
import type { ZodType } from 'zod';
|
|
14
|
-
import type { OutputForTag, ResourceSchema, TaggedResourceSchema } from '../resource/index.js';
|
|
14
|
+
import type { FilterFieldsByLevel, OutputForTag, ResourceSchema, TaggedResourceSchema } from '../resource/index.js';
|
|
15
15
|
import type { ContextTag, ExtractTag, LevelToTag, TaggedContext } from '../resource/tags.js';
|
|
16
16
|
import type { CompiledProcedure, GuardLike, MiddlewareFunction, ParentResourceConfig, ProcedureHandler, RestRouteOverride } from '../types.js';
|
|
17
17
|
/**
|
|
@@ -94,10 +94,13 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
94
94
|
*/
|
|
95
95
|
input<TSchema extends ValidSchema>(schema: TSchema): ProcedureBuilder<InferSchemaOutput<TSchema>, TOutput, TContext>;
|
|
96
96
|
/**
|
|
97
|
-
* Defines the output validation schema
|
|
97
|
+
* Defines the output validation schema (Zod)
|
|
98
98
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
99
|
+
* Sets a Zod schema that validates the handler's return value.
|
|
100
|
+
* All callers receive the same fields.
|
|
101
|
+
*
|
|
102
|
+
* For field-level visibility (different fields per access level),
|
|
103
|
+
* use `.expose()` with a resource schema instead.
|
|
101
104
|
*
|
|
102
105
|
* @template TSchema - The Zod schema type
|
|
103
106
|
* @param schema - Zod schema for output validation
|
|
@@ -106,14 +109,39 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
106
109
|
* @example
|
|
107
110
|
* ```typescript
|
|
108
111
|
* procedure()
|
|
109
|
-
* .output(z.object({
|
|
110
|
-
*
|
|
111
|
-
* name: z.string(),
|
|
112
|
-
* }))
|
|
113
|
-
* // handler must return { id: string; name: string }
|
|
112
|
+
* .output(z.object({ id: z.string(), name: z.string() }))
|
|
113
|
+
* .query(handler) // handler must return { id: string; name: string }
|
|
114
114
|
* ```
|
|
115
115
|
*/
|
|
116
116
|
output<TSchema extends ValidSchema>(schema: TSchema): ProcedureBuilder<TInput, InferSchemaOutput<TSchema>, TContext>;
|
|
117
|
+
/**
|
|
118
|
+
* Sets field-level visibility via a resource schema
|
|
119
|
+
*
|
|
120
|
+
* Accepts two resource schema variants:
|
|
121
|
+
* 1. **Tagged resource schema** (e.g., `UserSchema.authenticated`) — explicit field projection by access level
|
|
122
|
+
* 2. **Plain resource schema** (e.g., `UserSchema`) — context-derived field projection from `guardNarrow`
|
|
123
|
+
*
|
|
124
|
+
* @template TSchema - The resource schema type
|
|
125
|
+
* @param schema - Resource schema for field projection
|
|
126
|
+
* @returns New builder with updated output type
|
|
127
|
+
*
|
|
128
|
+
* @example Tagged resource schema — explicit projection level
|
|
129
|
+
* ```typescript
|
|
130
|
+
* procedure()
|
|
131
|
+
* .guard(authenticated)
|
|
132
|
+
* .expose(UserSchema.authenticated) // returns { id, name, email }
|
|
133
|
+
* .query(handler)
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* @example Plain resource schema — derives level from guardNarrow
|
|
137
|
+
* ```typescript
|
|
138
|
+
* procedure()
|
|
139
|
+
* .guardNarrow(authenticatedNarrow)
|
|
140
|
+
* .expose(UserSchema) // auto-projects based on guard's accessLevel
|
|
141
|
+
* .query(handler)
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
expose<TSchema extends ResourceSchema>(schema: TSchema): ProcedureBuilder<TInput, TSchema extends TaggedResourceSchema<infer TFields, infer TLevel> ? TLevel extends 'admin' | 'authenticated' | 'public' ? OutputForTag<ResourceSchema<TFields>, LevelToTag<TLevel>> : FilterFieldsByLevel<TFields, TLevel> : TContext extends TaggedContext<infer TTag> ? TTag extends ContextTag ? OutputForTag<TSchema, TTag> : OutputForTag<TSchema, ExtractTag<TContext>> : OutputForTag<TSchema, ExtractTag<TContext>>, TContext>;
|
|
117
145
|
/**
|
|
118
146
|
* Adds middleware to the procedure chain
|
|
119
147
|
*
|
|
@@ -255,6 +283,26 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
255
283
|
* ```
|
|
256
284
|
*/
|
|
257
285
|
rest(config: RestRouteOverride): ProcedureBuilder<TInput, TOutput, TContext>;
|
|
286
|
+
/**
|
|
287
|
+
* Configures the procedure as a webhook endpoint
|
|
288
|
+
*
|
|
289
|
+
* Sugar for `.rest({ method: 'POST', path })` with a webhook flag.
|
|
290
|
+
* Webhook procedures expect raw body access for signature verification.
|
|
291
|
+
*
|
|
292
|
+
* @param path - The webhook endpoint path (e.g., '/webhooks/stripe')
|
|
293
|
+
* @returns New builder with webhook configuration
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```typescript
|
|
297
|
+
* procedure()
|
|
298
|
+
* .webhook('/webhooks/stripe')
|
|
299
|
+
* .mutation(async ({ input, ctx }) => {
|
|
300
|
+
* const isValid = verifySignature(ctx.request.rawBody, ctx.request.headers['stripe-signature']);
|
|
301
|
+
* // ...
|
|
302
|
+
* })
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
webhook(path: string): ProcedureBuilder<TInput, TOutput, TContext>;
|
|
258
306
|
/**
|
|
259
307
|
* Marks the procedure as deprecated
|
|
260
308
|
*
|
|
@@ -377,28 +425,17 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
377
425
|
*/
|
|
378
426
|
mutation(handler: ProcedureHandler<TInput, TOutput, TContext>): CompiledProcedure<TInput, TOutput, TContext, 'mutation'>;
|
|
379
427
|
/**
|
|
380
|
-
*
|
|
428
|
+
* @deprecated Use `.expose()` instead. `.resource()` will be removed in v1.0.
|
|
381
429
|
*
|
|
382
|
-
*
|
|
383
|
-
* explicit auto-projection, or a plain schema for backward compatibility.
|
|
430
|
+
* Sets field-level visibility via a resource schema.
|
|
384
431
|
*
|
|
385
|
-
*
|
|
386
|
-
* tag's access level. When a plain schema is used, the output type is
|
|
387
|
-
* derived from the context's phantom tag (set by `guardNarrow`).
|
|
388
|
-
*
|
|
389
|
-
* @example
|
|
432
|
+
* @example Migration
|
|
390
433
|
* ```typescript
|
|
391
|
-
* //
|
|
392
|
-
* procedure()
|
|
393
|
-
* .guard(authenticated)
|
|
394
|
-
* .resource(UserSchema.authenticated)
|
|
395
|
-
* .query(async ({ ctx }) => ctx.db.user.findUnique(...));
|
|
434
|
+
* // Before
|
|
435
|
+
* procedure().resource(UserSchema.authenticated).query(handler)
|
|
396
436
|
*
|
|
397
|
-
* //
|
|
398
|
-
* procedure()
|
|
399
|
-
* .guardNarrow(authenticatedNarrow)
|
|
400
|
-
* .resource(UserSchema)
|
|
401
|
-
* .query(async ({ ctx }) => ctx.db.user.findUnique(...));
|
|
437
|
+
* // After
|
|
438
|
+
* procedure().expose(UserSchema.authenticated).query(handler)
|
|
402
439
|
* ```
|
|
403
440
|
*/
|
|
404
441
|
resource<TSchema extends ResourceSchema>(schema: TSchema): ProcedureBuilder<TInput, TSchema extends TaggedResourceSchema<infer TFields, infer TLevel> ? OutputForTag<ResourceSchema<TFields>, LevelToTag<TLevel>> : TContext extends TaggedContext<infer TTag> ? TTag extends ContextTag ? OutputForTag<TSchema, TTag> : OutputForTag<TSchema, ExtractTag<TContext>> : OutputForTag<TSchema, ExtractTag<TContext>>, TContext>;
|
|
@@ -417,7 +454,7 @@ export interface BuilderRuntimeState {
|
|
|
417
454
|
/** Resource schema for context-dependent output */
|
|
418
455
|
resourceSchema?: ResourceSchema;
|
|
419
456
|
/** Explicit resource level from tagged schema (e.g., UserSchema.authenticated) */
|
|
420
|
-
resourceLevel?:
|
|
457
|
+
resourceLevel?: string;
|
|
421
458
|
/** Middleware chain */
|
|
422
459
|
middlewares: MiddlewareFunction<unknown, BaseContext, BaseContext, unknown>[];
|
|
423
460
|
/** Guards to execute before handler */
|
|
@@ -432,6 +469,8 @@ export interface BuilderRuntimeState {
|
|
|
432
469
|
deprecated?: boolean;
|
|
433
470
|
/** Deprecation message */
|
|
434
471
|
deprecationMessage?: string;
|
|
472
|
+
/** Whether this procedure is a webhook endpoint (metadata marker) */
|
|
473
|
+
isWebhook?: boolean;
|
|
435
474
|
}
|
|
436
475
|
/**
|
|
437
476
|
* Type for the procedures object passed to defineProcedures
|
package/dist/resource/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* .input(z.object({ id: z.string() }))
|
|
35
35
|
* .query(async ({ input, ctx }) => {
|
|
36
36
|
* const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
37
|
-
* return resource(user, UserSchema).
|
|
37
|
+
* return resource(user, UserSchema).forPublic();
|
|
38
38
|
* }),
|
|
39
39
|
*
|
|
40
40
|
* // Authenticated endpoint → returns { id, name, email }
|
|
@@ -60,9 +60,11 @@
|
|
|
60
60
|
* @module resource
|
|
61
61
|
*/
|
|
62
62
|
export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
|
|
63
|
-
export type {
|
|
63
|
+
export type { AccessLevelConfig } from './levels.js';
|
|
64
|
+
export { defineAccessLevels } from './levels.js';
|
|
65
|
+
export type { AdminOutput, AnonymousOutput, AuthenticatedOutput, BuilderField, CustomResourceSchemaWithViews, CustomSchemaBuilder, FilterFieldsByLevel, OutputForLevel, OutputForTag, PublicOutput, RelationField, ResourceField, ResourceSchema, ResourceSchemaWithViews, RuntimeField, TaggedResourceSchema, } from './schema.js';
|
|
64
66
|
export { isResourceSchema, isTaggedResourceSchema, ResourceSchemaBuilder, resourceSchema, } from './schema.js';
|
|
65
|
-
export type { AccessLevel, ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, HasTag, LevelToTag, TaggedContext, WithTag, } from './tags.js';
|
|
66
|
-
export type { AdminTaggedContext, AnonymousTaggedContext, AnyResourceOutput, AuthenticatedTaggedContext, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, } from './types.js';
|
|
67
|
+
export type { AccessLevel, ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, HasTag, LevelToTag, PUBLIC, TaggedContext, TagToLevel, WithTag, } from './tags.js';
|
|
68
|
+
export type { AdminTaggedContext, AnonymousTaggedContext, AnyResourceOutput, AuthenticatedTaggedContext, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, PublicTaggedContext, } from './types.js';
|
|
67
69
|
export type { IsVisibleToTag, VisibilityLevel } from './visibility.js';
|
|
68
|
-
export { getAccessibleLevels, getVisibilityForTag, isVisibleAtLevel } from './visibility.js';
|
|
70
|
+
export { getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isVisibleAtLevel, } from './visibility.js';
|
package/dist/resource/index.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* .input(z.object({ id: z.string() }))
|
|
35
35
|
* .query(async ({ input, ctx }) => {
|
|
36
36
|
* const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
37
|
-
* return resource(user, UserSchema).
|
|
37
|
+
* return resource(user, UserSchema).forPublic();
|
|
38
38
|
* }),
|
|
39
39
|
*
|
|
40
40
|
* // Authenticated endpoint → returns { id, name, email }
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
// ============================================================================
|
|
65
65
|
// Resource instances
|
|
66
66
|
export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
|
|
67
|
+
export { defineAccessLevels } from './levels.js';
|
|
67
68
|
// Schema builder
|
|
68
69
|
export { isResourceSchema, isTaggedResourceSchema, ResourceSchemaBuilder, resourceSchema, } from './schema.js';
|
|
69
70
|
// Visibility
|
|
70
|
-
export { getAccessibleLevels, getVisibilityForTag, isVisibleAtLevel } from './visibility.js';
|
|
71
|
+
export { getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isVisibleAtLevel, } from './visibility.js';
|