@veloxts/router 0.6.67 → 0.6.69

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,61 @@
1
1
  # @veloxts/router
2
2
 
3
+ ## 0.6.69
4
+
5
+ ### Patch Changes
6
+
7
+ - implement user feedback improvements across packages
8
+
9
+ ## Summary
10
+
11
+ Addresses 9 user feedback items to improve DX, reduce boilerplate, and eliminate template duplications.
12
+
13
+ ### Phase 1: Validation Helpers (`@veloxts/validation`)
14
+
15
+ - Add `prismaDecimal()`, `prismaDecimalNullable()`, `prismaDecimalOptional()` for Prisma Decimal → number conversion
16
+ - Add `dateToIso`, `dateToIsoNullable`, `dateToIsoOptional` aliases for consistency
17
+
18
+ ### Phase 2: Template Deduplication (`@veloxts/auth`)
19
+
20
+ - Export `createEnhancedTokenStore()` with token revocation and refresh token reuse detection
21
+ - Export `parseUserRoles()` and `DEFAULT_ALLOWED_ROLES`
22
+ - Fix memory leak: track pending timeouts for proper cleanup on `destroy()`
23
+ - Update templates to import from `@veloxts/auth` instead of duplicating code
24
+ - Fix jwtManager singleton pattern in templates
25
+
26
+ ### Phase 3: Router Helpers (`@veloxts/router`)
27
+
28
+ - Add `createRouter()` returning `{ collections, router }` for DRY setup
29
+ - Add `toRouter()` for router-only use cases
30
+ - Update all router templates to use `createRouter()`
31
+
32
+ ### Phase 4: Guard Type Narrowing - Experimental (`@veloxts/auth`, `@veloxts/router`)
33
+
34
+ - Add `NarrowingGuard` interface with phantom `_narrows` type
35
+ - Add `authenticatedNarrow` and `hasRoleNarrow()` guards
36
+ - Add `guardNarrow()` method to `ProcedureBuilder` for context narrowing
37
+ - Enables `ctx.user` to be non-null after guard passes
38
+
39
+ ### Phase 5: Documentation (`@veloxts/router`)
40
+
41
+ - Document `.rest()` override patterns
42
+ - Document `createRouter()` helper usage
43
+ - Document `guardNarrow()` experimental API
44
+ - Add schema browser-safety patterns for RSC apps
45
+
46
+ - Updated dependencies
47
+ - @veloxts/core@0.6.69
48
+ - @veloxts/validation@0.6.69
49
+
50
+ ## 0.6.68
51
+
52
+ ### Patch Changes
53
+
54
+ - ci: add Claude code review and security review workflows, add GitHub release workflow, remove npm publish job
55
+ - Updated dependencies
56
+ - @veloxts/core@0.6.68
57
+ - @veloxts/validation@0.6.68
58
+
3
59
  ## 0.6.67
4
60
 
5
61
  ### Patch Changes
package/GUIDE.md CHANGED
@@ -48,6 +48,81 @@ Procedure names auto-map to HTTP methods:
48
48
  | `patch*` | PATCH | `/:id` |
49
49
  | `delete*`, `remove*` | DELETE | `/:id` |
50
50
 
51
+ ## REST Overrides with `.rest()`
52
+
53
+ When naming conventions don't fit your use case, use `.rest()` as an escape hatch:
54
+
55
+ ```typescript
56
+ const userProcedures = procedures('users', {
57
+ // Override method only
58
+ activateUser: procedure()
59
+ .input(z.object({ id: z.string().uuid() }))
60
+ .rest({ method: 'POST' }) // Would be PUT by default
61
+ .mutation(async ({ input, ctx }) => {
62
+ return ctx.db.user.update({ where: { id: input.id }, data: { active: true } });
63
+ }),
64
+
65
+ // Override path with parameters
66
+ getUserByEmail: procedure()
67
+ .input(z.object({ email: z.string().email() }))
68
+ .rest({ method: 'GET', path: '/users/by-email/:email' })
69
+ .query(async ({ input, ctx }) => {
70
+ return ctx.db.user.findUnique({ where: { email: input.email } });
71
+ }),
72
+
73
+ // Custom action endpoint
74
+ sendPasswordReset: procedure()
75
+ .input(z.object({ userId: z.string().uuid() }))
76
+ .rest({ method: 'POST', path: '/users/:userId/password-reset' })
77
+ .mutation(async ({ input, ctx }) => {
78
+ // ...
79
+ }),
80
+ });
81
+ ```
82
+
83
+ **Important**: Do NOT include the API prefix in `.rest()` paths:
84
+
85
+ ```typescript
86
+ // Correct - prefix is added automatically
87
+ .rest({ method: 'POST', path: '/users/:id/activate' })
88
+
89
+ // Wrong - results in /api/api/users/:id/activate
90
+ .rest({ method: 'POST', path: '/api/users/:id/activate' })
91
+ ```
92
+
93
+ ## Router Helper (`createRouter`)
94
+
95
+ Use `createRouter()` to eliminate redundancy when defining both collections and router:
96
+
97
+ ```typescript
98
+ import { createRouter, extractRoutes } from '@veloxts/router';
99
+
100
+ // Before (redundant):
101
+ // export const collections = [healthProcedures, userProcedures];
102
+ // export const router = {
103
+ // health: healthProcedures,
104
+ // users: userProcedures,
105
+ // };
106
+
107
+ // After (DRY):
108
+ export const { collections, router } = createRouter(
109
+ healthProcedures,
110
+ userProcedures
111
+ );
112
+
113
+ export type AppRouter = typeof router;
114
+ export const routes = extractRoutes(collections);
115
+ ```
116
+
117
+ If you only need the router object (not collections), use `toRouter()`:
118
+
119
+ ```typescript
120
+ import { toRouter } from '@veloxts/router';
121
+
122
+ export const router = toRouter(healthProcedures, userProcedures);
123
+ // Result: { health: healthProcedures, users: userProcedures }
124
+ ```
125
+
51
126
  ## Registering Routes
52
127
 
53
128
  ```typescript
@@ -320,6 +395,109 @@ const getUser = procedure()
320
395
  .query(handler);
321
396
  ```
322
397
 
398
+ ## Guard Type Narrowing (Experimental)
399
+
400
+ When using guards like `authenticated`, TypeScript doesn't know that `ctx.user` is guaranteed non-null after the guard passes. Use `guardNarrow()` to narrow the context type:
401
+
402
+ ```typescript
403
+ import { authenticatedNarrow, hasRoleNarrow } from '@veloxts/auth';
404
+
405
+ // ctx.user is guaranteed non-null after guard passes
406
+ const getProfile = procedure()
407
+ .guardNarrow(authenticatedNarrow)
408
+ .query(({ ctx }) => {
409
+ return { email: ctx.user.email }; // No null check needed!
410
+ });
411
+
412
+ // Chain multiple narrowing guards
413
+ const adminAction = procedure()
414
+ .guardNarrow(authenticatedNarrow)
415
+ .guardNarrow(hasRoleNarrow('admin'))
416
+ .mutation(({ ctx }) => {
417
+ // ctx.user is non-null with roles
418
+ });
419
+ ```
420
+
421
+ **Note**: This API is experimental. The current stable alternative is to use middleware for context extension:
422
+
423
+ ```typescript
424
+ const getProfile = procedure()
425
+ .guard(authenticated)
426
+ .use(async ({ ctx, next }) => {
427
+ if (!ctx.user) throw new Error('Unreachable');
428
+ return next({ ctx: { user: ctx.user } });
429
+ })
430
+ .query(({ ctx }) => {
431
+ // ctx.user is non-null via middleware
432
+ });
433
+ ```
434
+
435
+ ## Schema Browser-Safety
436
+
437
+ When building full-stack apps, schemas may be imported on both server and client. Avoid importing server-only dependencies in schema files:
438
+
439
+ ### Safe Pattern: Pure Zod Schemas
440
+
441
+ ```typescript
442
+ // src/schemas/user.ts - Safe for browser import
443
+ import { z } from '@veloxts/validation';
444
+
445
+ export const UserSchema = z.object({
446
+ id: z.string().uuid(),
447
+ name: z.string(),
448
+ email: z.string().email(),
449
+ createdAt: z.string().datetime(),
450
+ });
451
+
452
+ export type User = z.infer<typeof UserSchema>;
453
+ ```
454
+
455
+ ### Unsafe Pattern: Server Dependencies in Schemas
456
+
457
+ ```typescript
458
+ // DO NOT import server-only modules in schema files
459
+ import { db } from '@/database'; // BAD - pulls Prisma into client bundle
460
+
461
+ export const UserSchema = z.object({
462
+ // ...
463
+ });
464
+ ```
465
+
466
+ ### Separating Input/Output Schemas
467
+
468
+ Keep input schemas (for mutations) separate from output schemas (with transforms):
469
+
470
+ ```typescript
471
+ // src/schemas/user.input.ts - For mutations
472
+ export const CreateUserInput = z.object({
473
+ name: z.string().min(1),
474
+ email: z.string().email(),
475
+ });
476
+
477
+ // src/schemas/user.output.ts - May have transforms
478
+ import { dateToIso, prismaDecimal } from '@veloxts/validation';
479
+
480
+ export const UserOutput = z.object({
481
+ id: z.string().uuid(),
482
+ name: z.string(),
483
+ email: z.string().email(),
484
+ balance: prismaDecimal(), // Transforms Prisma Decimal
485
+ createdAt: dateToIso(), // Transforms Date to string
486
+ });
487
+ ```
488
+
489
+ ### Type-Only Imports
490
+
491
+ In server actions, use type-only imports to avoid bundling server code:
492
+
493
+ ```typescript
494
+ // GOOD - Type stripped at build time
495
+ import type { User } from '@/schemas/user';
496
+
497
+ // BAD - Pulls in full module graph
498
+ import { User } from '@/schemas/user';
499
+ ```
500
+
323
501
  ## Learn More
324
502
 
325
503
  See [@veloxts/velox](https://www.npmjs.com/package/@veloxts/velox) for complete documentation.
package/dist/index.d.ts CHANGED
@@ -47,6 +47,8 @@ export type { DefineProceduresOptions, } from './procedure/builder.js';
47
47
  export { defineProcedures, executeProcedure, isCompiledProcedure, isProcedureCollection, procedure, procedures, } from './procedure/builder.js';
48
48
  export { createProcedure, typedProcedure } from './procedure/factory.js';
49
49
  export type { BuilderRuntimeState, InferProcedures, InferSchemaOutput, ProcedureBuilder, ProcedureBuilderState, ProcedureDefinitions, ValidSchema, } from './procedure/types.js';
50
+ export type { RouterResult } from './router-utils.js';
51
+ export { createRouter, toRouter } from './router-utils.js';
50
52
  export type { NamingWarning, NamingWarningType, WarningConfig, WarningOption } from './warnings.js';
51
53
  export { analyzeNamingConvention, isDevelopment, normalizeWarningOption } from './warnings.js';
52
54
  export type { ExtractRoutesType, RestAdapterOptions, RestMapping, RestRoute, RouteMap, } from './rest/index.js';
package/dist/index.js CHANGED
@@ -51,8 +51,12 @@ export {
51
51
  // Builder functions
52
52
  defineProcedures, executeProcedure, isCompiledProcedure, isProcedureCollection, procedure, procedures, // Short alias for defineProcedures
53
53
  } from './procedure/builder.js';
54
+ // ============================================================================
55
+ // Router Utilities
56
+ // ============================================================================
54
57
  // Typed procedure factory
55
58
  export { createProcedure, typedProcedure } from './procedure/factory.js';
59
+ export { createRouter, toRouter } from './router-utils.js';
56
60
  export { analyzeNamingConvention, isDevelopment, normalizeWarningOption } from './warnings.js';
57
61
  export { buildNestedRestPath, buildRestPath, extractRoutes, followsNamingConvention, generateRestRoutes, getRouteSummary, inferResourceName, parseNamingConvention, registerRestRoutes, rest, } from './rest/index.js';
58
62
  export {
@@ -175,6 +175,18 @@ function createBuilder(state) {
175
175
  guards: [...state.guards, guardDef],
176
176
  });
177
177
  },
178
+ /**
179
+ * Adds an authorization guard with type narrowing (EXPERIMENTAL)
180
+ *
181
+ * Unlike `guard()`, this method narrows the context type based on
182
+ * what the guard guarantees after it passes.
183
+ */
184
+ guardNarrow(guardDef) {
185
+ return createBuilder({
186
+ ...state,
187
+ guards: [...state.guards, guardDef],
188
+ });
189
+ },
178
190
  /**
179
191
  * Sets REST route override
180
192
  */
@@ -170,6 +170,41 @@ export interface ProcedureBuilder<TInput = unknown, TOutput = unknown, TContext
170
170
  * ```
171
171
  */
172
172
  guard<TGuardContext extends Partial<TContext>>(guard: GuardLike<TGuardContext>): ProcedureBuilder<TInput, TOutput, TContext>;
173
+ /**
174
+ * Adds an authorization guard with type narrowing (EXPERIMENTAL)
175
+ *
176
+ * Unlike `.guard()`, this method narrows the context type based on
177
+ * what the guard guarantees. For example, `authenticatedNarrow` narrows
178
+ * `ctx.user` from `User | undefined` to `User`.
179
+ *
180
+ * **EXPERIMENTAL**: This API may change. Consider using middleware
181
+ * for context type extension as the current stable alternative.
182
+ *
183
+ * @template TNarrowedContext - The context type guaranteed by the guard
184
+ * @param guard - Narrowing guard definition with `_narrows` type
185
+ * @returns New builder with narrowed context type
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * import { authenticatedNarrow, hasRoleNarrow } from '@veloxts/auth';
190
+ *
191
+ * // ctx.user is guaranteed non-null after guard passes
192
+ * procedure()
193
+ * .guardNarrow(authenticatedNarrow)
194
+ * .query(({ ctx }) => {
195
+ * return { email: ctx.user.email }; // No null check needed!
196
+ * });
197
+ *
198
+ * // Chain multiple narrowing guards
199
+ * procedure()
200
+ * .guardNarrow(authenticatedNarrow)
201
+ * .guardNarrow(hasRoleNarrow('admin'))
202
+ * .mutation(({ ctx }) => { ... });
203
+ * ```
204
+ */
205
+ guardNarrow<TNarrowedContext>(guard: GuardLike<Partial<TContext>> & {
206
+ readonly _narrows: TNarrowedContext;
207
+ }): ProcedureBuilder<TInput, TOutput, TContext & TNarrowedContext>;
173
208
  /**
174
209
  * Configures REST route override
175
210
  *
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Router Utility Functions
3
+ *
4
+ * Helpers for creating type-safe router definitions from procedure collections.
5
+ *
6
+ * @module router-utils
7
+ */
8
+ import type { ProcedureCollection, ProcedureRecord } from './types.js';
9
+ /**
10
+ * Extracts the namespace from a ProcedureCollection as a literal type
11
+ */
12
+ type ExtractNamespace<T> = T extends ProcedureCollection<infer _P> & {
13
+ readonly namespace: infer N;
14
+ } ? N extends string ? N : never : never;
15
+ /**
16
+ * Creates a union of namespaces from an array of ProcedureCollections
17
+ */
18
+ type CollectionNamespaces<T extends readonly ProcedureCollection[]> = {
19
+ [K in keyof T]: ExtractNamespace<T[K]>;
20
+ }[number];
21
+ /**
22
+ * Maps namespaces to their corresponding ProcedureCollections
23
+ */
24
+ type RouterFromCollections<T extends readonly ProcedureCollection[]> = {
25
+ [K in CollectionNamespaces<T>]: Extract<T[number], {
26
+ namespace: K;
27
+ }>;
28
+ };
29
+ /**
30
+ * Result type from createRouter
31
+ */
32
+ export interface RouterResult<T extends readonly ProcedureCollection[]> {
33
+ /** Array of procedure collections for routing */
34
+ readonly collections: T;
35
+ /** Object mapping namespaces to procedure collections */
36
+ readonly router: RouterFromCollections<T>;
37
+ }
38
+ /**
39
+ * Creates both collections array and router object from procedure collections.
40
+ *
41
+ * This helper eliminates the redundancy of defining both `collections` and `router`
42
+ * separately. The router object is automatically keyed by each collection's namespace.
43
+ *
44
+ * @param collections - Procedure collections to include in the router
45
+ * @returns Object containing both `collections` array and `router` object
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * import { createRouter, extractRoutes } from '@veloxts/router';
50
+ * import { healthProcedures } from './procedures/health.js';
51
+ * import { userProcedures } from './procedures/users.js';
52
+ *
53
+ * // Before (redundant):
54
+ * // export const collections = [healthProcedures, userProcedures];
55
+ * // export const router = {
56
+ * // health: healthProcedures,
57
+ * // users: userProcedures,
58
+ * // };
59
+ *
60
+ * // After (DRY):
61
+ * export const { collections, router } = createRouter(
62
+ * healthProcedures,
63
+ * userProcedures
64
+ * );
65
+ *
66
+ * export type AppRouter = typeof router;
67
+ * export const routes = extractRoutes(collections);
68
+ * ```
69
+ */
70
+ export declare function createRouter<T extends ProcedureCollection<ProcedureRecord>[]>(...collections: T): RouterResult<T>;
71
+ /**
72
+ * Creates a router object from procedure collections.
73
+ *
74
+ * This is an alternative to `createRouter` when you only need the router object
75
+ * and not the collections array. Useful for frontend-only type imports.
76
+ *
77
+ * @param collections - Procedure collections to include in the router
78
+ * @returns Object mapping namespaces to procedure collections
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * import { toRouter } from '@veloxts/router';
83
+ *
84
+ * export const router = toRouter(healthProcedures, userProcedures);
85
+ * // Result: { health: healthProcedures, users: userProcedures }
86
+ *
87
+ * export type AppRouter = typeof router;
88
+ * ```
89
+ */
90
+ export declare function toRouter<T extends ProcedureCollection<ProcedureRecord>[]>(...collections: T): RouterFromCollections<T>;
91
+ export {};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Router Utility Functions
3
+ *
4
+ * Helpers for creating type-safe router definitions from procedure collections.
5
+ *
6
+ * @module router-utils
7
+ */
8
+ // ============================================================================
9
+ // Router Creation
10
+ // ============================================================================
11
+ /**
12
+ * Creates both collections array and router object from procedure collections.
13
+ *
14
+ * This helper eliminates the redundancy of defining both `collections` and `router`
15
+ * separately. The router object is automatically keyed by each collection's namespace.
16
+ *
17
+ * @param collections - Procedure collections to include in the router
18
+ * @returns Object containing both `collections` array and `router` object
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * import { createRouter, extractRoutes } from '@veloxts/router';
23
+ * import { healthProcedures } from './procedures/health.js';
24
+ * import { userProcedures } from './procedures/users.js';
25
+ *
26
+ * // Before (redundant):
27
+ * // export const collections = [healthProcedures, userProcedures];
28
+ * // export const router = {
29
+ * // health: healthProcedures,
30
+ * // users: userProcedures,
31
+ * // };
32
+ *
33
+ * // After (DRY):
34
+ * export const { collections, router } = createRouter(
35
+ * healthProcedures,
36
+ * userProcedures
37
+ * );
38
+ *
39
+ * export type AppRouter = typeof router;
40
+ * export const routes = extractRoutes(collections);
41
+ * ```
42
+ */
43
+ export function createRouter(...collections) {
44
+ const router = Object.fromEntries(collections.map((collection) => [collection.namespace, collection]));
45
+ return {
46
+ // Cast required: rest params are typed as T[] but we need to preserve
47
+ // the exact tuple type T for proper namespace inference in RouterResult
48
+ collections: collections,
49
+ router,
50
+ };
51
+ }
52
+ /**
53
+ * Creates a router object from procedure collections.
54
+ *
55
+ * This is an alternative to `createRouter` when you only need the router object
56
+ * and not the collections array. Useful for frontend-only type imports.
57
+ *
58
+ * @param collections - Procedure collections to include in the router
59
+ * @returns Object mapping namespaces to procedure collections
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * import { toRouter } from '@veloxts/router';
64
+ *
65
+ * export const router = toRouter(healthProcedures, userProcedures);
66
+ * // Result: { health: healthProcedures, users: userProcedures }
67
+ *
68
+ * export type AppRouter = typeof router;
69
+ * ```
70
+ */
71
+ export function toRouter(...collections) {
72
+ return Object.fromEntries(collections.map((collection) => [collection.namespace, collection]));
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veloxts/router",
3
- "version": "0.6.67",
3
+ "version": "0.6.69",
4
4
  "description": "Procedure definitions with tRPC and REST routing for VeloxTS framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,8 +40,8 @@
40
40
  "@trpc/server": "11.8.0",
41
41
  "fastify": "5.6.2",
42
42
  "zod-to-json-schema": "3.24.5",
43
- "@veloxts/validation": "0.6.67",
44
- "@veloxts/core": "0.6.67"
43
+ "@veloxts/core": "0.6.69",
44
+ "@veloxts/validation": "0.6.69"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@vitest/coverage-v8": "4.0.16",