@veloxts/router 0.6.92 → 0.6.94
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 +192 -1
- package/dist/index.d.ts +32 -0
- package/dist/index.js +37 -0
- package/dist/procedure/builder.js +52 -4
- package/dist/procedure/types.d.ts +33 -0
- package/dist/resource/index.d.ts +68 -0
- package/dist/resource/index.js +70 -0
- package/dist/resource/instance.d.ts +184 -0
- package/dist/resource/instance.js +282 -0
- package/dist/resource/schema.d.ts +192 -0
- package/dist/resource/schema.js +153 -0
- package/dist/resource/tags.d.ts +116 -0
- package/dist/resource/tags.js +12 -0
- package/dist/resource/types.d.ts +96 -0
- package/dist/resource/types.js +9 -0
- package/dist/resource/visibility.d.ts +72 -0
- package/dist/resource/visibility.js +81 -0
- package/dist/types.d.ts +20 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @veloxts/router
|
|
2
2
|
|
|
3
|
+
## 0.6.94
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat(client): add tRPC router type support for ClientFromRouter and VeloxHooks
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @veloxts/core@0.6.94
|
|
10
|
+
- @veloxts/validation@0.6.94
|
|
11
|
+
|
|
12
|
+
## 0.6.93
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- feat(router): add Resource API with phantom types for context-dependent outputs
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- @veloxts/core@0.6.93
|
|
19
|
+
- @veloxts/validation@0.6.93
|
|
20
|
+
|
|
3
21
|
## 0.6.92
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/GUIDE.md
CHANGED
|
@@ -28,9 +28,11 @@ 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
|
|
31
|
+
- `.output(schema)` - Validate output with Zod (same fields for all users)
|
|
32
|
+
- `.resource(schema)` - Context-dependent output with field visibility
|
|
32
33
|
- `.use(middleware)` - Add middleware
|
|
33
34
|
- `.guard(guard)` - Add authorization guard
|
|
35
|
+
- `.guardNarrow(guard)` - Add guard with TypeScript type narrowing
|
|
34
36
|
- `.rest({ method, path })` - Override REST path
|
|
35
37
|
- `.query(handler)` - Finalize as read operation
|
|
36
38
|
- `.mutation(handler)` - Finalize as write operation
|
|
@@ -426,6 +428,195 @@ console.table(routes);
|
|
|
426
428
|
// ]
|
|
427
429
|
```
|
|
428
430
|
|
|
431
|
+
## Resource API (Context-Dependent Outputs)
|
|
432
|
+
|
|
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.
|
|
434
|
+
|
|
435
|
+
### Defining a Resource Schema
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
import { resourceSchema } from '@veloxts/router';
|
|
439
|
+
import { z } from '@veloxts/validation';
|
|
440
|
+
|
|
441
|
+
const UserSchema = resourceSchema()
|
|
442
|
+
.public('id', z.string().uuid()) // Visible to everyone
|
|
443
|
+
.public('name', z.string()) // Visible to everyone
|
|
444
|
+
.authenticated('email', z.string().email()) // Visible to logged-in users
|
|
445
|
+
.authenticated('createdAt', z.date()) // Visible to logged-in users
|
|
446
|
+
.admin('internalNotes', z.string().nullable()) // Visible to admins only
|
|
447
|
+
.admin('lastLoginIp', z.string().nullable()) // Visible to admins only
|
|
448
|
+
.build();
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Automatic Projection (Simple Cases)
|
|
452
|
+
|
|
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:
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
import { authenticatedNarrow, adminNarrow } from '@veloxts/auth';
|
|
457
|
+
|
|
458
|
+
export const userProcedures = procedures('users', {
|
|
459
|
+
// Authenticated endpoint - auto-projects { id, name, email, createdAt }
|
|
460
|
+
getProfile: procedure()
|
|
461
|
+
.guardNarrow(authenticatedNarrow)
|
|
462
|
+
.resource(UserSchema)
|
|
463
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
464
|
+
.query(async ({ input, ctx }) => {
|
|
465
|
+
// Just return the full data - projection is automatic!
|
|
466
|
+
return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
|
|
467
|
+
}),
|
|
468
|
+
|
|
469
|
+
// Admin endpoint - auto-projects all fields
|
|
470
|
+
getFullProfile: procedure()
|
|
471
|
+
.guardNarrow(adminNarrow)
|
|
472
|
+
.resource(UserSchema)
|
|
473
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
474
|
+
.query(async ({ input, ctx }) => {
|
|
475
|
+
return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
|
|
476
|
+
}),
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**How it works:**
|
|
481
|
+
|
|
482
|
+
1. The `guardNarrow()` method accepts guards with an `accessLevel` property (`'public'`, `'authenticated'`, or `'admin'`)
|
|
483
|
+
2. After the guard passes, the procedure executor tracks the highest access level
|
|
484
|
+
3. When the handler returns, the executor automatically projects the result using the resource schema
|
|
485
|
+
4. No manual `.forX()` calls needed - the output type is inferred at compile time
|
|
486
|
+
|
|
487
|
+
**Benefits:**
|
|
488
|
+
- Clean, declarative code - no projection logic in handlers
|
|
489
|
+
- Type-safe - return types are inferred from guard + schema
|
|
490
|
+
- Less boilerplate - handlers just return data
|
|
491
|
+
- Consistent - impossible to forget projection
|
|
492
|
+
|
|
493
|
+
### Manual Projection (Complex Cases)
|
|
494
|
+
|
|
495
|
+
For situations where automatic projection doesn't fit, use explicit projection methods:
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
import { resource, resourceCollection } from '@veloxts/router';
|
|
499
|
+
|
|
500
|
+
export const userProcedures = procedures('users', {
|
|
501
|
+
// Public endpoint - explicitly returns { id, name }
|
|
502
|
+
getPublicProfile: procedure()
|
|
503
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
504
|
+
.query(async ({ input, ctx }) => {
|
|
505
|
+
const user = await ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
|
|
506
|
+
return resource(user, UserSchema).forAnonymous();
|
|
507
|
+
}),
|
|
508
|
+
|
|
509
|
+
// Conditional projection based on ownership
|
|
510
|
+
getOwnProfile: procedure()
|
|
511
|
+
.guard(authenticated)
|
|
512
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
513
|
+
.query(async ({ input, ctx }) => {
|
|
514
|
+
const user = await ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
|
|
515
|
+
// Show more fields if viewing own profile
|
|
516
|
+
if (user.id === ctx.user?.id) {
|
|
517
|
+
return resource(user, UserSchema).forAuthenticated();
|
|
518
|
+
}
|
|
519
|
+
return resource(user, UserSchema).forAnonymous();
|
|
520
|
+
}),
|
|
521
|
+
|
|
522
|
+
// Admin with explicit projection
|
|
523
|
+
getUserForAdmin: procedure()
|
|
524
|
+
.guard(hasRole('admin'))
|
|
525
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
526
|
+
.query(async ({ input, ctx }) => {
|
|
527
|
+
const user = await ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
|
|
528
|
+
return resource(user, UserSchema).forAdmin();
|
|
529
|
+
}),
|
|
530
|
+
});
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**When to use manual projection:**
|
|
534
|
+
- Conditional logic (e.g., show more fields for own profile)
|
|
535
|
+
- Mixed access levels in one handler
|
|
536
|
+
- Non-guard-based access decisions
|
|
537
|
+
- Legacy code migration
|
|
538
|
+
|
|
539
|
+
### Resource Collections
|
|
540
|
+
|
|
541
|
+
For arrays of items, use `resourceCollection()`:
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
// Automatic projection (simple cases)
|
|
545
|
+
const listUsers = procedure()
|
|
546
|
+
.guardNarrow(authenticatedNarrow)
|
|
547
|
+
.resource(UserSchema)
|
|
548
|
+
.query(async ({ ctx }) => {
|
|
549
|
+
return ctx.db.user.findMany({ take: 50 });
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Manual projection (when needed)
|
|
553
|
+
const listUsersManual = procedure()
|
|
554
|
+
.guard(authenticated)
|
|
555
|
+
.query(async ({ ctx }) => {
|
|
556
|
+
const users = await ctx.db.user.findMany({ take: 50 });
|
|
557
|
+
return resourceCollection(users, UserSchema).forAuthenticated();
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Context-Aware Projection
|
|
562
|
+
|
|
563
|
+
For manual projection with dynamic access level, use `.for(ctx)`:
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
const getUser = procedure()
|
|
567
|
+
.guardNarrow(authenticatedNarrow)
|
|
568
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
569
|
+
.query(async ({ input, ctx }) => {
|
|
570
|
+
const user = await ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
|
|
571
|
+
// Reads access level from ctx.__accessLevel
|
|
572
|
+
return resource(user, UserSchema).for(ctx);
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Visibility Levels
|
|
577
|
+
|
|
578
|
+
| Method | Fields Included | Use Case |
|
|
579
|
+
|--------|-----------------|----------|
|
|
580
|
+
| `.forAnonymous()` | `public` only | Public APIs, unauthenticated users |
|
|
581
|
+
| `.forAuthenticated()` | `public` + `authenticated` | Logged-in users |
|
|
582
|
+
| `.forAdmin()` | All fields | Admin dashboards, internal tools |
|
|
583
|
+
| `.for(ctx)` | Auto-detected from context | Dynamic access control |
|
|
584
|
+
|
|
585
|
+
### Type Safety
|
|
586
|
+
|
|
587
|
+
The Resource API provides compile-time type safety:
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
// Automatic projection - type inferred from guard
|
|
591
|
+
const getProfile = procedure()
|
|
592
|
+
.guardNarrow(authenticatedNarrow)
|
|
593
|
+
.resource(UserSchema)
|
|
594
|
+
.query(async ({ ctx }) => {
|
|
595
|
+
return ctx.db.user.findFirst();
|
|
596
|
+
});
|
|
597
|
+
// Return type: { id: string; name: string; email: string; createdAt: Date }
|
|
598
|
+
|
|
599
|
+
// Manual projection - type inferred from method
|
|
600
|
+
const result = resource(user, UserSchema).forAnonymous();
|
|
601
|
+
// Type: { id: string; name: string }
|
|
602
|
+
|
|
603
|
+
const authResult = resource(user, UserSchema).forAuthenticated();
|
|
604
|
+
// Type: { id: string; name: string; email: string; createdAt: Date }
|
|
605
|
+
|
|
606
|
+
const adminResult = resource(user, UserSchema).forAdmin();
|
|
607
|
+
// Type: { id: string; name: string; email: string; createdAt: Date; internalNotes: string | null; lastLoginIp: string | null }
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Choosing an Approach
|
|
611
|
+
|
|
612
|
+
| Scenario | Approach |
|
|
613
|
+
|----------|----------|
|
|
614
|
+
| Guard determines access level | **Automatic** (`.guardNarrow().resource()`) |
|
|
615
|
+
| Public endpoints (no guard) | Manual (`.forAnonymous()`) |
|
|
616
|
+
| Conditional/dynamic projection | Manual (`.forX()` in handler) |
|
|
617
|
+
| Simple, declarative code | **Automatic** |
|
|
618
|
+
| Complex access logic | Manual
|
|
619
|
+
|
|
429
620
|
## Middleware
|
|
430
621
|
|
|
431
622
|
```typescript
|
package/dist/index.d.ts
CHANGED
|
@@ -83,3 +83,35 @@ 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, ContextTag, ExtractTag, HasTag, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, IsVisibleToTag, OutputForTag, ResourceField, ResourceSchema, RuntimeField, TaggedContext, VisibilityLevel, WithTag, } from './resource/index.js';
|
|
87
|
+
/**
|
|
88
|
+
* Resource API for context-dependent output types using phantom types.
|
|
89
|
+
*
|
|
90
|
+
* Enables procedures to return different field sets based on user role/auth state
|
|
91
|
+
* while maintaining precise compile-time types.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* import { z } from 'zod';
|
|
96
|
+
* import { resourceSchema, resource, procedures, procedure } from '@veloxts/router';
|
|
97
|
+
*
|
|
98
|
+
* // Define schema with field visibility
|
|
99
|
+
* const UserSchema = resourceSchema()
|
|
100
|
+
* .public('id', z.string())
|
|
101
|
+
* .public('name', z.string())
|
|
102
|
+
* .authenticated('email', z.string())
|
|
103
|
+
* .admin('internalNotes', z.string().nullable())
|
|
104
|
+
* .build();
|
|
105
|
+
*
|
|
106
|
+
* // Use in procedures
|
|
107
|
+
* export const userProcedures = procedures('users', {
|
|
108
|
+
* getPublicProfile: procedure()
|
|
109
|
+
* .input(z.object({ id: z.string() }))
|
|
110
|
+
* .query(async ({ input, ctx }) => {
|
|
111
|
+
* const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
112
|
+
* return resource(user, UserSchema).forAnonymous();
|
|
113
|
+
* }),
|
|
114
|
+
* });
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export { getAccessibleLevels, getVisibilityForTag, isResourceSchema, isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder, resource, resourceCollection, resourceSchema, } from './resource/index.js';
|
package/dist/index.js
CHANGED
|
@@ -78,3 +78,40 @@ createSwaggerUI, DEFAULT_GUARD_MAPPINGS, DEFAULT_SECURITY_SCHEMES, DEFAULT_UI_CO
|
|
|
78
78
|
generateOpenApiSpec,
|
|
79
79
|
// HTML Generator
|
|
80
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';
|
|
81
|
+
/**
|
|
82
|
+
* Resource API for context-dependent output types using phantom types.
|
|
83
|
+
*
|
|
84
|
+
* Enables procedures to return different field sets based on user role/auth state
|
|
85
|
+
* while maintaining precise compile-time types.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* import { z } from 'zod';
|
|
90
|
+
* import { resourceSchema, resource, procedures, procedure } from '@veloxts/router';
|
|
91
|
+
*
|
|
92
|
+
* // Define schema with field visibility
|
|
93
|
+
* const UserSchema = resourceSchema()
|
|
94
|
+
* .public('id', z.string())
|
|
95
|
+
* .public('name', z.string())
|
|
96
|
+
* .authenticated('email', z.string())
|
|
97
|
+
* .admin('internalNotes', z.string().nullable())
|
|
98
|
+
* .build();
|
|
99
|
+
*
|
|
100
|
+
* // Use in procedures
|
|
101
|
+
* export const userProcedures = procedures('users', {
|
|
102
|
+
* getPublicProfile: procedure()
|
|
103
|
+
* .input(z.object({ id: z.string() }))
|
|
104
|
+
* .query(async ({ input, ctx }) => {
|
|
105
|
+
* const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
106
|
+
* return resource(user, UserSchema).forAnonymous();
|
|
107
|
+
* }),
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export { getAccessibleLevels, getVisibilityForTag, isResourceSchema,
|
|
112
|
+
// Visibility
|
|
113
|
+
isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder,
|
|
114
|
+
// Resource instances
|
|
115
|
+
resource, resourceCollection,
|
|
116
|
+
// Schema builder
|
|
117
|
+
resourceSchema, } from './resource/index.js';
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { ConfigurationError, logWarning } from '@veloxts/core';
|
|
11
11
|
import { GuardError } from '../errors.js';
|
|
12
12
|
import { createMiddlewareExecutor, executeMiddlewareChain } from '../middleware/chain.js';
|
|
13
|
+
import { Resource, } from '../resource/index.js';
|
|
13
14
|
import { deriveParentParamName } from '../utils/pluralization.js';
|
|
14
15
|
import { analyzeNamingConvention, isDevelopment, normalizeWarningOption, } from '../warnings.js';
|
|
15
16
|
// ============================================================================
|
|
@@ -49,6 +50,7 @@ export function procedure() {
|
|
|
49
50
|
return createBuilder({
|
|
50
51
|
inputSchema: undefined,
|
|
51
52
|
outputSchema: undefined,
|
|
53
|
+
resourceSchema: undefined,
|
|
52
54
|
middlewares: [],
|
|
53
55
|
guards: [],
|
|
54
56
|
restOverride: undefined,
|
|
@@ -197,6 +199,20 @@ function createBuilder(state) {
|
|
|
197
199
|
mutation(handler) {
|
|
198
200
|
return compileProcedure('mutation', handler, state);
|
|
199
201
|
},
|
|
202
|
+
/**
|
|
203
|
+
* Sets the output type based on a resource schema
|
|
204
|
+
*
|
|
205
|
+
* This method stores the resource schema for potential OpenAPI generation
|
|
206
|
+
* and narrows the output type based on the context's phantom tag.
|
|
207
|
+
*/
|
|
208
|
+
resource(schema) {
|
|
209
|
+
// Store the resource schema for OpenAPI generation
|
|
210
|
+
// The actual output type is computed at the type level
|
|
211
|
+
return createBuilder({
|
|
212
|
+
...state,
|
|
213
|
+
resourceSchema: schema,
|
|
214
|
+
});
|
|
215
|
+
},
|
|
200
216
|
};
|
|
201
217
|
}
|
|
202
218
|
/**
|
|
@@ -230,6 +246,8 @@ function compileProcedure(type, handler, state) {
|
|
|
230
246
|
parentResources: state.parentResources,
|
|
231
247
|
// Store pre-compiled executor for performance
|
|
232
248
|
_precompiledExecutor: precompiledExecutor,
|
|
249
|
+
// Store resource schema for auto-projection
|
|
250
|
+
_resourceSchema: state.resourceSchema,
|
|
233
251
|
};
|
|
234
252
|
}
|
|
235
253
|
/**
|
|
@@ -388,6 +406,8 @@ export const procedures = defineProcedures;
|
|
|
388
406
|
* ```
|
|
389
407
|
*/
|
|
390
408
|
export async function executeProcedure(procedure, rawInput, ctx) {
|
|
409
|
+
// Track the highest access level from narrowing guards
|
|
410
|
+
let accessLevel = 'public';
|
|
391
411
|
// Step 1: Execute guards if any
|
|
392
412
|
if (procedure.guards.length > 0) {
|
|
393
413
|
// Defensive check: ensure request and reply exist in context
|
|
@@ -405,8 +425,20 @@ export async function executeProcedure(procedure, rawInput, ctx) {
|
|
|
405
425
|
const message = guard.message ?? `Guard "${guard.name}" check failed`;
|
|
406
426
|
throw new GuardError(guard.name, message, statusCode);
|
|
407
427
|
}
|
|
428
|
+
// Track highest access level from narrowing guards
|
|
429
|
+
const guardWithLevel = guard;
|
|
430
|
+
if (guardWithLevel.accessLevel) {
|
|
431
|
+
// Admin > authenticated > public
|
|
432
|
+
if (guardWithLevel.accessLevel === 'admin' ||
|
|
433
|
+
(guardWithLevel.accessLevel === 'authenticated' && accessLevel === 'public')) {
|
|
434
|
+
accessLevel = guardWithLevel.accessLevel;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
408
437
|
}
|
|
409
438
|
}
|
|
439
|
+
// Set __accessLevel on context for auto-projection
|
|
440
|
+
const ctxWithLevel = ctx;
|
|
441
|
+
ctxWithLevel.__accessLevel = accessLevel;
|
|
410
442
|
// Step 2: Validate input if schema provided
|
|
411
443
|
const input = procedure.inputSchema
|
|
412
444
|
? procedure.inputSchema.parse(rawInput)
|
|
@@ -415,17 +447,33 @@ export async function executeProcedure(procedure, rawInput, ctx) {
|
|
|
415
447
|
let result;
|
|
416
448
|
if (procedure._precompiledExecutor) {
|
|
417
449
|
// PERFORMANCE: Use pre-compiled middleware chain executor
|
|
418
|
-
result = await procedure._precompiledExecutor(input,
|
|
450
|
+
result = await procedure._precompiledExecutor(input, ctxWithLevel);
|
|
419
451
|
}
|
|
420
452
|
else if (procedure.middlewares.length === 0) {
|
|
421
453
|
// No middleware - execute handler directly
|
|
422
|
-
result = await procedure.handler({ input, ctx });
|
|
454
|
+
result = await procedure.handler({ input, ctx: ctxWithLevel });
|
|
423
455
|
}
|
|
424
456
|
else {
|
|
425
457
|
// Fallback: Build middleware chain dynamically (should not normally happen)
|
|
426
|
-
result = await executeMiddlewareChainFallback(procedure.middlewares, input,
|
|
458
|
+
result = await executeMiddlewareChainFallback(procedure.middlewares, input, ctxWithLevel, async () => procedure.handler({ input, ctx: ctxWithLevel }));
|
|
459
|
+
}
|
|
460
|
+
// Step 4: Auto-project if resource schema is set
|
|
461
|
+
if (procedure._resourceSchema) {
|
|
462
|
+
const schema = procedure._resourceSchema;
|
|
463
|
+
const resourceInstance = new Resource(result, schema);
|
|
464
|
+
// Project based on access level
|
|
465
|
+
switch (accessLevel) {
|
|
466
|
+
case 'admin':
|
|
467
|
+
result = resourceInstance.forAdmin();
|
|
468
|
+
break;
|
|
469
|
+
case 'authenticated':
|
|
470
|
+
result = resourceInstance.forAuthenticated();
|
|
471
|
+
break;
|
|
472
|
+
default:
|
|
473
|
+
result = resourceInstance.forAnonymous();
|
|
474
|
+
}
|
|
427
475
|
}
|
|
428
|
-
// Step
|
|
476
|
+
// Step 5: Validate output if schema provided
|
|
429
477
|
if (procedure.outputSchema) {
|
|
430
478
|
return procedure.outputSchema.parse(result);
|
|
431
479
|
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { BaseContext } from '@veloxts/core';
|
|
13
13
|
import type { ZodType, ZodTypeDef } from 'zod';
|
|
14
|
+
import type { OutputForTag, ResourceSchema } from '../resource/index.js';
|
|
15
|
+
import type { ContextTag, ExtractTag, TaggedContext } from '../resource/tags.js';
|
|
14
16
|
import type { CompiledProcedure, GuardLike, MiddlewareFunction, ParentResourceConfig, ProcedureHandler, RestRouteOverride } from '../types.js';
|
|
15
17
|
/**
|
|
16
18
|
* Internal state type that accumulates type information through the builder chain
|
|
@@ -369,6 +371,35 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
|
|
|
369
371
|
* ```
|
|
370
372
|
*/
|
|
371
373
|
mutation(handler: ProcedureHandler<TInput, TOutput, TContext>): CompiledProcedure<TInput, TOutput, TContext, 'mutation'>;
|
|
374
|
+
/**
|
|
375
|
+
* Sets the output type based on a resource schema and context tag
|
|
376
|
+
*
|
|
377
|
+
* This method enables context-dependent output types using phantom types.
|
|
378
|
+
* The output type is computed based on the fields visible to the context's
|
|
379
|
+
* access level (anonymous, authenticated, or admin).
|
|
380
|
+
*
|
|
381
|
+
* **IMPORTANT**: This method is for type documentation only. You must still
|
|
382
|
+
* call `.forAnonymous()`, `.forAuthenticated()`, or `.forAdmin()` on the
|
|
383
|
+
* resource instance in your handler to perform the actual field filtering.
|
|
384
|
+
*
|
|
385
|
+
* @template TSchema - The resource schema type
|
|
386
|
+
* @param schema - The resource schema defining field visibility
|
|
387
|
+
* @returns New builder with output type set based on context's tag
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```typescript
|
|
391
|
+
* // The output type is automatically computed from the context tag
|
|
392
|
+
* const getUser = procedure()
|
|
393
|
+
* .guardNarrow(authenticatedNarrow) // Tags context with AUTHENTICATED
|
|
394
|
+
* .input(z.object({ id: z.string() }))
|
|
395
|
+
* .resource(UserSchema) // Output type: { id, name, email }
|
|
396
|
+
* .query(async ({ input, ctx }) => {
|
|
397
|
+
* const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
398
|
+
* return resource(user, UserSchema).forAuthenticated();
|
|
399
|
+
* });
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
resource<TSchema extends ResourceSchema>(schema: TSchema): ProcedureBuilder<TInput, TContext extends TaggedContext<infer TTag> ? TTag extends ContextTag ? OutputForTag<TSchema, TTag> : OutputForTag<TSchema, ExtractTag<TContext>> : OutputForTag<TSchema, ExtractTag<TContext>>, TContext>;
|
|
372
403
|
}
|
|
373
404
|
/**
|
|
374
405
|
* Internal runtime state for the procedure builder
|
|
@@ -381,6 +412,8 @@ export interface BuilderRuntimeState {
|
|
|
381
412
|
inputSchema?: ValidSchema;
|
|
382
413
|
/** Output validation schema */
|
|
383
414
|
outputSchema?: ValidSchema;
|
|
415
|
+
/** Resource schema for context-dependent output */
|
|
416
|
+
resourceSchema?: ResourceSchema;
|
|
384
417
|
/** Middleware chain */
|
|
385
418
|
middlewares: MiddlewareFunction<unknown, BaseContext, BaseContext, unknown>[];
|
|
386
419
|
/** Guards to execute before handler */
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource API with phantom types
|
|
3
|
+
*
|
|
4
|
+
* Provides a Laravel-inspired Resource API for defining context-dependent
|
|
5
|
+
* output types using phantom types for compile-time type safety.
|
|
6
|
+
*
|
|
7
|
+
* ## Overview
|
|
8
|
+
*
|
|
9
|
+
* The Resource API solves the problem of returning different field sets
|
|
10
|
+
* based on user role/auth state while maintaining precise types:
|
|
11
|
+
*
|
|
12
|
+
* - Anonymous: `{ id, name }`
|
|
13
|
+
* - Authenticated: `{ id, name, email }`
|
|
14
|
+
* - Admin: `{ id, name, email, internalNotes }`
|
|
15
|
+
*
|
|
16
|
+
* ## Usage
|
|
17
|
+
*
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { z } from 'zod';
|
|
20
|
+
* import { resourceSchema, resource, resourceCollection } from '@veloxts/router';
|
|
21
|
+
*
|
|
22
|
+
* // 1. Define schema with field visibility
|
|
23
|
+
* const UserSchema = resourceSchema()
|
|
24
|
+
* .public('id', z.string().uuid())
|
|
25
|
+
* .public('name', z.string())
|
|
26
|
+
* .authenticated('email', z.string().email())
|
|
27
|
+
* .admin('internalNotes', z.string().nullable())
|
|
28
|
+
* .build();
|
|
29
|
+
*
|
|
30
|
+
* // 2. Use in procedures
|
|
31
|
+
* export const userProcedures = procedures('users', {
|
|
32
|
+
* // Public endpoint → returns { id, name }
|
|
33
|
+
* getPublicProfile: procedure()
|
|
34
|
+
* .input(z.object({ id: z.string() }))
|
|
35
|
+
* .query(async ({ input, ctx }) => {
|
|
36
|
+
* const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
37
|
+
* return resource(user, UserSchema).forAnonymous();
|
|
38
|
+
* }),
|
|
39
|
+
*
|
|
40
|
+
* // Authenticated endpoint → returns { id, name, email }
|
|
41
|
+
* getProfile: procedure()
|
|
42
|
+
* .guardNarrow(authenticatedNarrow)
|
|
43
|
+
* .input(z.object({ id: z.string() }))
|
|
44
|
+
* .query(async ({ input, ctx }) => {
|
|
45
|
+
* const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
46
|
+
* return resource(user, UserSchema).forAuthenticated();
|
|
47
|
+
* }),
|
|
48
|
+
*
|
|
49
|
+
* // Admin endpoint → returns { id, name, email, internalNotes }
|
|
50
|
+
* getFullProfile: procedure()
|
|
51
|
+
* .guardNarrow(adminNarrow)
|
|
52
|
+
* .input(z.object({ id: z.string() }))
|
|
53
|
+
* .query(async ({ input, ctx }) => {
|
|
54
|
+
* const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
55
|
+
* return resource(user, UserSchema).forAdmin();
|
|
56
|
+
* }),
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @module resource
|
|
61
|
+
*/
|
|
62
|
+
export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
|
|
63
|
+
export type { AdminOutput, AnonymousOutput, AuthenticatedOutput, OutputForTag, ResourceField, ResourceSchema, RuntimeField, } from './schema.js';
|
|
64
|
+
export { isResourceSchema, ResourceSchemaBuilder, resourceSchema } from './schema.js';
|
|
65
|
+
export type { AccessLevel, ADMIN, ANONYMOUS, AUTHENTICATED, ContextTag, ExtractTag, HasTag, TaggedContext, WithTag, } from './tags.js';
|
|
66
|
+
export type { AdminTaggedContext, AnonymousTaggedContext, AnyResourceOutput, AuthenticatedTaggedContext, IfAdmin, IfAuthenticated, InferResourceData, InferResourceOutput, } from './types.js';
|
|
67
|
+
export type { IsVisibleToTag, VisibilityLevel } from './visibility.js';
|
|
68
|
+
export { getAccessibleLevels, getVisibilityForTag, isVisibleAtLevel } from './visibility.js';
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource API with phantom types
|
|
3
|
+
*
|
|
4
|
+
* Provides a Laravel-inspired Resource API for defining context-dependent
|
|
5
|
+
* output types using phantom types for compile-time type safety.
|
|
6
|
+
*
|
|
7
|
+
* ## Overview
|
|
8
|
+
*
|
|
9
|
+
* The Resource API solves the problem of returning different field sets
|
|
10
|
+
* based on user role/auth state while maintaining precise types:
|
|
11
|
+
*
|
|
12
|
+
* - Anonymous: `{ id, name }`
|
|
13
|
+
* - Authenticated: `{ id, name, email }`
|
|
14
|
+
* - Admin: `{ id, name, email, internalNotes }`
|
|
15
|
+
*
|
|
16
|
+
* ## Usage
|
|
17
|
+
*
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { z } from 'zod';
|
|
20
|
+
* import { resourceSchema, resource, resourceCollection } from '@veloxts/router';
|
|
21
|
+
*
|
|
22
|
+
* // 1. Define schema with field visibility
|
|
23
|
+
* const UserSchema = resourceSchema()
|
|
24
|
+
* .public('id', z.string().uuid())
|
|
25
|
+
* .public('name', z.string())
|
|
26
|
+
* .authenticated('email', z.string().email())
|
|
27
|
+
* .admin('internalNotes', z.string().nullable())
|
|
28
|
+
* .build();
|
|
29
|
+
*
|
|
30
|
+
* // 2. Use in procedures
|
|
31
|
+
* export const userProcedures = procedures('users', {
|
|
32
|
+
* // Public endpoint → returns { id, name }
|
|
33
|
+
* getPublicProfile: procedure()
|
|
34
|
+
* .input(z.object({ id: z.string() }))
|
|
35
|
+
* .query(async ({ input, ctx }) => {
|
|
36
|
+
* const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
37
|
+
* return resource(user, UserSchema).forAnonymous();
|
|
38
|
+
* }),
|
|
39
|
+
*
|
|
40
|
+
* // Authenticated endpoint → returns { id, name, email }
|
|
41
|
+
* getProfile: procedure()
|
|
42
|
+
* .guardNarrow(authenticatedNarrow)
|
|
43
|
+
* .input(z.object({ id: z.string() }))
|
|
44
|
+
* .query(async ({ input, ctx }) => {
|
|
45
|
+
* const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
46
|
+
* return resource(user, UserSchema).forAuthenticated();
|
|
47
|
+
* }),
|
|
48
|
+
*
|
|
49
|
+
* // Admin endpoint → returns { id, name, email, internalNotes }
|
|
50
|
+
* getFullProfile: procedure()
|
|
51
|
+
* .guardNarrow(adminNarrow)
|
|
52
|
+
* .input(z.object({ id: z.string() }))
|
|
53
|
+
* .query(async ({ input, ctx }) => {
|
|
54
|
+
* const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
55
|
+
* return resource(user, UserSchema).forAdmin();
|
|
56
|
+
* }),
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @module resource
|
|
61
|
+
*/
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Core Exports
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Resource instances
|
|
66
|
+
export { Resource, ResourceCollection, resource, resourceCollection } from './instance.js';
|
|
67
|
+
// Schema builder
|
|
68
|
+
export { isResourceSchema, ResourceSchemaBuilder, resourceSchema } from './schema.js';
|
|
69
|
+
// Visibility
|
|
70
|
+
export { getAccessibleLevels, getVisibilityForTag, isVisibleAtLevel } from './visibility.js';
|