@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 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, ctx);
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, ctx, async () => procedure.handler({ input, ctx }));
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 4: Validate output if schema provided
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';