@veloxts/router 0.7.9 → 0.8.1

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,26 @@
1
1
  # @veloxts/router
2
2
 
3
+ ## 0.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - feat: add business logic primitives for B2B SaaS apps
8
+ - Updated dependencies
9
+ - @veloxts/core@0.8.1
10
+ - @veloxts/validation@0.8.1
11
+
12
+ ## 0.8.0
13
+
14
+ ### Minor Changes
15
+
16
+ - feat(router): new simplified procedure builder
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies
21
+ - @veloxts/core@0.8.0
22
+ - @veloxts/validation@0.8.0
23
+
3
24
  ## 0.7.9
4
25
 
5
26
  ### Patch Changes
package/GUIDE.md CHANGED
@@ -28,11 +28,9 @@ 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
- - `.expose(schema)` - Context-dependent output with resource schema
31
+ - `.output(schema)` - Validate output with Zod schema or tagged resource view
33
32
  - `.use(middleware)` - Add middleware
34
33
  - `.guard(guard)` - Add authorization guard
35
- - `.guardNarrow(guard)` - Add guard with TypeScript type narrowing
36
34
  - `.rest({ method, path })` - Override REST path
37
35
  - `.query(handler)` - Finalize as read operation
38
36
  - `.mutation(handler)` - Finalize as write operation
@@ -405,7 +403,7 @@ console.table(routes);
405
403
 
406
404
  ## Resource API (Context-Dependent Outputs)
407
405
 
408
- The Resource API provides context-dependent output types using phantom types. Pass a resource schema to `.output()` instead of a Zod schema to define field visibility per access level.
406
+ The Resource API provides context-dependent output types using phantom types. Pass a tagged resource view to `.output()` instead of a plain Zod schema to define field visibility per access level.
409
407
 
410
408
  ### Defining a Resource Schema
411
409
 
@@ -423,28 +421,27 @@ const UserSchema = resourceSchema()
423
421
  .build();
424
422
  ```
425
423
 
426
- ### Automatic Projection (Simple Cases)
424
+ ### Automatic Projection with `.output()` and Guards
427
425
 
428
- The most elegant approach is to chain `.expose()` with a narrowing guard. The procedure executor automatically projects fields based on the guard's access level:
426
+ The most elegant approach is to chain `.guard()` with `.output()` using a tagged resource view. The procedure builder uses the guard's access level to determine which fields to include:
429
427
 
430
428
  ```typescript
431
- import { authenticatedNarrow, adminNarrow } from '@veloxts/auth';
429
+ import { authenticated, hasRole } from '@veloxts/auth';
432
430
 
433
431
  export const userProcedures = procedures('users', {
434
- // Authenticated endpoint - auto-projects { id, name, email, createdAt }
432
+ // Authenticated endpoint - projects { id, name, email, createdAt }
435
433
  getProfile: procedure()
436
- .guardNarrow(authenticatedNarrow)
437
- .expose(UserSchema)
434
+ .guard(authenticated)
435
+ .output(UserSchema.authenticated)
438
436
  .input(z.object({ id: z.string().uuid() }))
439
437
  .query(async ({ input, ctx }) => {
440
- // Just return the full data - projection is automatic!
441
438
  return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
442
439
  }),
443
440
 
444
- // Admin endpoint - auto-projects all fields
441
+ // Admin endpoint - projects all fields
445
442
  getFullProfile: procedure()
446
- .guardNarrow(adminNarrow)
447
- .expose(UserSchema)
443
+ .guard(hasRole('admin'))
444
+ .output(UserSchema.admin)
448
445
  .input(z.object({ id: z.string().uuid() }))
449
446
  .query(async ({ input, ctx }) => {
450
447
  return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
@@ -454,14 +451,13 @@ export const userProcedures = procedures('users', {
454
451
 
455
452
  **How it works:**
456
453
 
457
- 1. The `guardNarrow()` method accepts guards with an `accessLevel` property (`'public'`, `'authenticated'`, or `'admin'`)
458
- 2. After the guard passes, the procedure executor tracks the highest access level
459
- 3. When the handler returns, the executor automatically projects the result using the resource schema
460
- 4. No manual `.forX()` calls needed - the output type is inferred at compile time
454
+ 1. The `.output()` method accepts both plain Zod schemas and tagged resource views (e.g., `UserSchema.authenticated`)
455
+ 2. When a tagged view is passed, the procedure executor automatically projects the handler's return value
456
+ 3. The output type is inferred at compile time from the tagged view
461
457
 
462
458
  **Benefits:**
463
459
  - Clean, declarative code - no projection logic in handlers
464
- - Type-safe - return types are inferred from guard + schema
460
+ - Type-safe - return types are inferred from the tagged view
465
461
  - Less boilerplate - handlers just return data
466
462
  - Consistent - impossible to forget projection
467
463
 
@@ -516,10 +512,10 @@ export const userProcedures = procedures('users', {
516
512
  For arrays of items, use `resourceCollection()`:
517
513
 
518
514
  ```typescript
519
- // Automatic projection (simple cases)
515
+ // Automatic projection with .output()
520
516
  const listUsers = procedure()
521
- .guardNarrow(authenticatedNarrow)
522
- .expose(UserSchema)
517
+ .guard(authenticated)
518
+ .output(UserSchema.authenticated)
523
519
  .query(async ({ ctx }) => {
524
520
  return ctx.db.user.findMany({ take: 50 });
525
521
  });
@@ -539,7 +535,7 @@ For manual projection with dynamic access level, use `.for(ctx)`:
539
535
 
540
536
  ```typescript
541
537
  const getUser = procedure()
542
- .guardNarrow(authenticatedNarrow)
538
+ .guard(authenticated)
543
539
  .input(z.object({ id: z.string().uuid() }))
544
540
  .query(async ({ input, ctx }) => {
545
541
  const user = await ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
@@ -562,10 +558,10 @@ const getUser = procedure()
562
558
  The Resource API provides compile-time type safety:
563
559
 
564
560
  ```typescript
565
- // Automatic projection - type inferred from guard
561
+ // Automatic projection - type inferred from tagged view
566
562
  const getProfile = procedure()
567
- .guardNarrow(authenticatedNarrow)
568
- .expose(UserSchema)
563
+ .guard(authenticated)
564
+ .output(UserSchema.authenticated)
569
565
  .query(async ({ ctx }) => {
570
566
  return ctx.db.user.findFirst();
571
567
  });
@@ -586,7 +582,7 @@ const adminResult = resource(user, UserSchema.admin);
586
582
 
587
583
  | Scenario | Approach |
588
584
  |----------|----------|
589
- | Guard determines access level | **Automatic** (`.guardNarrow().output()`) |
585
+ | Guard determines access level | **Automatic** (`.guard().output(Schema.level)`) |
590
586
  | Public endpoints (no guard) | Tagged view (`UserSchema.public`) |
591
587
  | Conditional/dynamic projection | Tagged view or `.for(ctx)` in handler |
592
588
  | Simple, declarative code | **Automatic** |
@@ -603,43 +599,29 @@ const getUser = procedure()
603
599
  .query(handler);
604
600
  ```
605
601
 
606
- ## Guard Type Narrowing (Experimental)
602
+ ## Guard Type Narrowing
607
603
 
608
- 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:
604
+ Guards like `authenticated` and `hasRole()` both enforce authorization and narrow the context type. After a guard passes, TypeScript knows `ctx.user` is non-null:
609
605
 
610
606
  ```typescript
611
- import { authenticatedNarrow, hasRoleNarrow } from '@veloxts/auth';
607
+ import { authenticated, hasRole } from '@veloxts/auth';
612
608
 
613
609
  // ctx.user is guaranteed non-null after guard passes
614
610
  const getProfile = procedure()
615
- .guardNarrow(authenticatedNarrow)
611
+ .guard(authenticated)
616
612
  .query(({ ctx }) => {
617
613
  return { email: ctx.user.email }; // No null check needed!
618
614
  });
619
615
 
620
- // Chain multiple narrowing guards
616
+ // Chain multiple guards
621
617
  const adminAction = procedure()
622
- .guardNarrow(authenticatedNarrow)
623
- .guardNarrow(hasRoleNarrow('admin'))
618
+ .guard(authenticated)
619
+ .guard(hasRole('admin'))
624
620
  .mutation(({ ctx }) => {
625
621
  // ctx.user is non-null with roles
626
622
  });
627
623
  ```
628
624
 
629
- **Note**: This API is experimental. The current stable alternative is to use middleware for context extension:
630
-
631
- ```typescript
632
- const getProfile = procedure()
633
- .guard(authenticated)
634
- .use(async ({ ctx, next }) => {
635
- if (!ctx.user) throw new Error('Unreachable');
636
- return next({ ctx: { user: ctx.user } });
637
- })
638
- .query(({ ctx }) => {
639
- // ctx.user is non-null via middleware
640
- });
641
- ```
642
-
643
625
  ## Schema Browser-Safety
644
626
 
645
627
  When building full-stack apps, schemas may be imported on both server and client. Avoid importing server-only dependencies in schema files:
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @veloxts/router
2
2
 
3
- > **Early Access (v0.7.x)**
3
+ > **Early Access (v0.8.x)**
4
4
 
5
5
  Procedure-based routing for VeloxTS Framework - provides hybrid tRPC and REST adapters with naming convention-based HTTP method inference. Learn more at [@veloxts/velox](https://www.npmjs.com/package/@veloxts/velox).
6
6
 
package/dist/index.d.ts CHANGED
@@ -39,14 +39,16 @@
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, Middleware, MiddlewareArgs, MiddlewareFunction, MiddlewareNext, MiddlewareResult, ParentResourceChain, ParentResourceConfig, ProcedureCollection, ProcedureHandler, ProcedureHandlerArgs, ProcedureRecord, ProcedureType, RestRouteOverride, } from './types.js';
42
+ export type { AfterHandler, CompiledProcedure, ContextExtensions, ContextFactory, ExtendedContext, GuardLike, HttpMethod, InferProcedureContext, InferProcedureErrors, InferProcedureInput, InferProcedureOutput, Middleware, MiddlewareArgs, MiddlewareFunction, MiddlewareNext, MiddlewareResult, ParentResourceChain, ParentResourceConfig, PolicyActionLike, ProcedureCollection, ProcedureHandler, ProcedureHandlerArgs, ProcedureRecord, ProcedureType, RestRouteOverride, TransactionalOptions, } 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';
46
46
  export type { DefineProceduresOptions, } from './procedure/builder.js';
47
47
  export { defineProcedures, executeProcedure, isCompiledProcedure, isProcedureCollection, procedure, procedures, } from './procedure/builder.js';
48
+ export type { PipelineStep, RevertAction, StepOptions } from './procedure/pipeline.js';
49
+ export { defineRevert, defineStep } from './procedure/pipeline.js';
48
50
  export { createProcedure, typedProcedure } from './procedure/factory.js';
49
- export type { BuilderRuntimeState, InferProcedures, InferSchemaOutput, ProcedureBuilder, ProcedureBuilderState, ProcedureDefinitions, ValidSchema, } from './procedure/types.js';
51
+ export type { BuilderRuntimeState, InferOutputSchema, InferProcedures, InferSchemaOutput, PostHandlerBuilder, ProcedureBuilder, ProcedureBuilderState, ProcedureDefinitions, ValidOutputSchema, ValidSchema, } from './procedure/types.js';
50
52
  export type { RouterResult } from './router-utils.js';
51
53
  export { createRouter, toRouter } from './router-utils.js';
52
54
  export type { NamingWarning, NamingWarningType, WarningConfig, WarningOption } from './warnings.js';
@@ -114,4 +116,4 @@ export type { AccessLevel, AccessLevelConfig, ADMIN, AdminOutput, AdminTaggedCon
114
116
  * });
115
117
  * ```
116
118
  */
117
- export { defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema, isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder, resource, resourceCollection, resourceSchema, } from './resource/index.js';
119
+ export { defaultAccess, defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema, isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder, resource, resourceCollection, resourceSchema, } from './resource/index.js';
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ export {
51
51
  // Builder functions
52
52
  defineProcedures, executeProcedure, isCompiledProcedure, isProcedureCollection, procedure, procedures, // Short alias for defineProcedures
53
53
  } from './procedure/builder.js';
54
+ export { defineRevert, defineStep } from './procedure/pipeline.js';
54
55
  // ============================================================================
55
56
  // Router Utilities
56
57
  // ============================================================================
@@ -110,7 +111,7 @@ swaggerPlugin, validateOpenApiSpec, zodSchemaToJsonSchema, } from './openapi/ind
110
111
  */
111
112
  export {
112
113
  // Access level configuration
113
- defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema,
114
+ defaultAccess, defineAccessLevels, getAccessibleLevels, getVisibilityForTag, isFieldVisibleToLevel, isResourceSchema,
114
115
  // Visibility
115
116
  isVisibleAtLevel, Resource, ResourceCollection, ResourceSchemaBuilder,
116
117
  // 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, resourceSchemaToJsonSchema, zodSchemaToJsonSchema, } from './schema-converter.js';
10
+ import { removeSchemaProperties, resourceSchemaToJsonSchema, resourceSchemaToJsonSchemaForBranching, zodSchemaToJsonSchema, } from './schema-converter.js';
11
11
  import { extractUsedSecuritySchemes, filterUsedSecuritySchemes, guardsToSecurity, mergeSecuritySchemes, } from './security-mapper.js';
12
12
  // ============================================================================
13
13
  // Main Generator
@@ -150,6 +150,10 @@ function generateOperation(route, namespace, options) {
150
150
  if (procedure.outputSchema) {
151
151
  outputSchema = zodSchemaToJsonSchema(procedure.outputSchema);
152
152
  }
153
+ else if (procedure._handlerMap && procedure._resourceSchema) {
154
+ // Level 3: branched procedure — include all fields, non-public fields optional
155
+ outputSchema = resourceSchemaToJsonSchemaForBranching(procedure._resourceSchema);
156
+ }
153
157
  else if (procedure._resourceSchema) {
154
158
  const level = procedure._resourceLevel ?? 'public';
155
159
  outputSchema = resourceSchemaToJsonSchema(procedure._resourceSchema, level);
@@ -113,3 +113,14 @@ export declare function schemaHasProperties(schema: JSONSchema | undefined): boo
113
113
  * @returns JSON Schema with only the fields visible at the given level
114
114
  */
115
115
  export declare function resourceSchemaToJsonSchema(schema: ResourceSchema, level?: string): JSONSchema;
116
+ /**
117
+ * Converts a resource schema to JSON Schema for Level 3 branched procedures
118
+ *
119
+ * Includes ALL fields from the resource schema. Fields visible at the
120
+ * lowest level (first in the levels array) are marked as required;
121
+ * higher-level-only fields are optional.
122
+ *
123
+ * @param schema - The resource schema (must have _levelConfig for custom levels)
124
+ * @returns JSON Schema with all fields, non-public fields optional
125
+ */
126
+ export declare function resourceSchemaToJsonSchemaForBranching(schema: ResourceSchema): JSONSchema;
@@ -308,3 +308,50 @@ export function resourceSchemaToJsonSchema(schema, level = 'public') {
308
308
  ...(required.length > 0 ? { required } : {}),
309
309
  };
310
310
  }
311
+ /**
312
+ * Converts a resource schema to JSON Schema for Level 3 branched procedures
313
+ *
314
+ * Includes ALL fields from the resource schema. Fields visible at the
315
+ * lowest level (first in the levels array) are marked as required;
316
+ * higher-level-only fields are optional.
317
+ *
318
+ * @param schema - The resource schema (must have _levelConfig for custom levels)
319
+ * @returns JSON Schema with all fields, non-public fields optional
320
+ */
321
+ export function resourceSchemaToJsonSchemaForBranching(schema) {
322
+ const properties = {};
323
+ const required = [];
324
+ // Determine the lowest (most accessible) level
325
+ const schemaWithConfig = schema;
326
+ const lowestLevel = schemaWithConfig._levelConfig?.levels[0] ?? 'public';
327
+ for (const field of schema.fields) {
328
+ const runtimeField = field;
329
+ if (field.nestedSchema) {
330
+ const nestedJsonSchema = resourceSchemaToJsonSchemaForBranching(field.nestedSchema);
331
+ if (field.cardinality === 'many') {
332
+ properties[field.name] = { type: 'array', items: nestedJsonSchema };
333
+ }
334
+ else {
335
+ properties[field.name] = { ...nestedJsonSchema, nullable: true };
336
+ }
337
+ }
338
+ else if (field.schema) {
339
+ const fieldJsonSchema = zodSchemaToJsonSchema(field.schema);
340
+ if (fieldJsonSchema) {
341
+ properties[field.name] = fieldJsonSchema;
342
+ }
343
+ }
344
+ else {
345
+ properties[field.name] = {};
346
+ }
347
+ // Only public/lowest-level fields are required
348
+ if (isFieldVisibleToLevel(runtimeField, lowestLevel)) {
349
+ required.push(field.name);
350
+ }
351
+ }
352
+ return {
353
+ type: 'object',
354
+ properties,
355
+ ...(required.length > 0 ? { required } : {}),
356
+ };
357
+ }
@@ -41,7 +41,7 @@ import type { InferProcedures, ProcedureBuilder, ProcedureDefinitions } from './
41
41
  * });
42
42
  * ```
43
43
  */
44
- export declare function procedure<TContext extends BaseContext = BaseContext>(): ProcedureBuilder<unknown, unknown, TContext>;
44
+ export declare function procedure<TContext extends BaseContext = BaseContext>(): ProcedureBuilder<unknown, unknown, TContext, never>;
45
45
  /**
46
46
  * Options for defining a procedure collection
47
47
  */