@veloxts/client 0.8.0 → 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,11 @@
1
1
  # @veloxts/client
2
2
 
3
+ ## 0.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - feat: add business logic primitives for B2B SaaS apps
8
+
3
9
  ## 0.8.0
4
10
 
5
11
  ### Minor Changes
package/dist/errors.d.ts CHANGED
@@ -35,6 +35,18 @@ interface NotFoundErrorResponse extends BaseErrorResponse {
35
35
  resource: string;
36
36
  resourceId?: string;
37
37
  }
38
+ /**
39
+ * Domain error response from server
40
+ *
41
+ * When server-side DomainError.toJSON() is called, it returns:
42
+ * { error, message, statusCode, code, data }
43
+ */
44
+ interface DomainErrorResponse extends BaseErrorResponse {
45
+ error: string;
46
+ statusCode: number;
47
+ code: string;
48
+ data?: unknown;
49
+ }
38
50
  /**
39
51
  * Generic error response from server
40
52
  */
@@ -46,7 +58,7 @@ interface GenericErrorResponse extends BaseErrorResponse {
46
58
  /**
47
59
  * Union of all error response types from server
48
60
  */
49
- export type ErrorResponse = ValidationErrorResponse | NotFoundErrorResponse | GenericErrorResponse;
61
+ export type ErrorResponse = ValidationErrorResponse | NotFoundErrorResponse | DomainErrorResponse | GenericErrorResponse;
50
62
  /**
51
63
  * Base error class for all client errors
52
64
  *
@@ -57,12 +69,19 @@ export declare class VeloxClientError extends Error implements ClientError {
57
69
  readonly statusCode?: number;
58
70
  readonly code?: string;
59
71
  readonly body?: unknown;
72
+ /**
73
+ * Typed domain error payload, extracted from `body.data` when the response
74
+ * contains a `code` field (i.e. it is a DomainError response).
75
+ * This is DISTINCT from `body` which holds the full raw response body.
76
+ */
77
+ readonly data?: unknown;
60
78
  readonly url: string;
61
79
  readonly method: string;
62
80
  constructor(message: string, options: {
63
81
  statusCode?: number;
64
82
  code?: string;
65
83
  body?: unknown;
84
+ data?: unknown;
66
85
  url: string;
67
86
  method: string;
68
87
  });
@@ -124,6 +143,7 @@ export declare class ServerError extends VeloxClientError {
124
143
  url: string;
125
144
  method: string;
126
145
  body?: unknown;
146
+ data?: unknown;
127
147
  });
128
148
  }
129
149
  /**
@@ -154,6 +174,14 @@ export declare function isValidationErrorResponse(response: ErrorResponse): resp
154
174
  * Type guard for not found error response
155
175
  */
156
176
  export declare function isNotFoundErrorResponse(response: ErrorResponse): response is NotFoundErrorResponse;
177
+ /**
178
+ * Type guard for domain error response
179
+ *
180
+ * A domain error response has a `code` field that is not one of the built-in
181
+ * error codes (VALIDATION_ERROR, NOT_FOUND). The server-side DomainError
182
+ * serializes as: { error, message, statusCode, code, data }.
183
+ */
184
+ export declare function isDomainErrorResponse(response: ErrorResponse): response is DomainErrorResponse;
157
185
  /**
158
186
  * Parses an error response from the server and creates appropriate error instance
159
187
  *
package/dist/errors.js CHANGED
@@ -19,6 +19,12 @@ export class VeloxClientError extends Error {
19
19
  statusCode;
20
20
  code;
21
21
  body;
22
+ /**
23
+ * Typed domain error payload, extracted from `body.data` when the response
24
+ * contains a `code` field (i.e. it is a DomainError response).
25
+ * This is DISTINCT from `body` which holds the full raw response body.
26
+ */
27
+ data;
22
28
  url;
23
29
  method;
24
30
  constructor(message, options) {
@@ -27,6 +33,7 @@ export class VeloxClientError extends Error {
27
33
  this.statusCode = options.statusCode;
28
34
  this.code = options.code;
29
35
  this.body = options.body;
36
+ this.data = options.data;
30
37
  this.url = options.url;
31
38
  this.method = options.method;
32
39
  // Maintains proper stack trace for where error was thrown (V8 only)
@@ -161,6 +168,18 @@ export function isValidationErrorResponse(response) {
161
168
  export function isNotFoundErrorResponse(response) {
162
169
  return response.error === 'NotFoundError';
163
170
  }
171
+ /**
172
+ * Type guard for domain error response
173
+ *
174
+ * A domain error response has a `code` field that is not one of the built-in
175
+ * error codes (VALIDATION_ERROR, NOT_FOUND). The server-side DomainError
176
+ * serializes as: { error, message, statusCode, code, data }.
177
+ */
178
+ export function isDomainErrorResponse(response) {
179
+ return (typeof response.code === 'string' &&
180
+ response.code !== 'VALIDATION_ERROR' &&
181
+ response.code !== 'NOT_FOUND');
182
+ }
164
183
  // ============================================================================
165
184
  // Error Parsing
166
185
  // ============================================================================
@@ -192,6 +211,28 @@ export function parseErrorResponse(response, body, url, method) {
192
211
  body,
193
212
  });
194
213
  }
214
+ // Domain error — has a custom `code` and optional `data` payload
215
+ if (isDomainErrorResponse(errorResponse)) {
216
+ const domainData = errorResponse.data;
217
+ if (response.status >= 500) {
218
+ return new ServerError(errorResponse.message, {
219
+ statusCode: errorResponse.statusCode,
220
+ code: errorResponse.code,
221
+ url,
222
+ method,
223
+ body,
224
+ data: domainData,
225
+ });
226
+ }
227
+ return new VeloxClientError(errorResponse.message, {
228
+ statusCode: errorResponse.statusCode,
229
+ code: errorResponse.code,
230
+ url,
231
+ method,
232
+ body,
233
+ data: domainData,
234
+ });
235
+ }
195
236
  // Server error (5xx)
196
237
  if (response.status >= 500) {
197
238
  return new ServerError(errorResponse.message, {
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  * @module @veloxts/client
22
22
  */
23
23
  export { createClient } from './client.js';
24
- export type { ClientConfig, ClientError, ClientFromCollection, ClientFromRouter, ClientMode, ClientProcedure, HttpMethod, InferProcedureInput, InferProcedureOutput, ProcedureCall, ProcedureCollection, ProcedureRecord, } from './types.js';
24
+ export type { ClientConfig, ClientError, ClientFromCollection, ClientFromRouter, ClientMode, ClientProcedure, HttpMethod, InferProcedureErrors, InferProcedureInput, InferProcedureOutput, ProcedureCall, ProcedureCollection, ProcedureRecord, } from './types.js';
25
25
  export { ClientNotFoundError, ClientValidationError, NetworkError, ServerError, VeloxClientError, } from './errors.js';
26
26
  export { isClientNotFoundError, isClientValidationError, isNetworkError, isNotFoundErrorResponse, isServerError, isValidationErrorResponse, isVeloxClientError, } from './errors.js';
27
27
  export type { ErrorResponse } from './errors.js';
@@ -104,4 +104,4 @@ export declare function VeloxProvider<TRouter>({ children, config, queryClient:
104
104
  * ```
105
105
  */
106
106
  export declare function useVeloxContext<TRouter>(): VeloxContextValue<TRouter>;
107
- export type { VeloxProviderProps, VeloxContextValue };
107
+ export type { VeloxContextValue, VeloxProviderProps };
@@ -79,4 +79,4 @@ export interface VeloxProviderProps<_TRouter> {
79
79
  /** Optional pre-configured QueryClient instance */
80
80
  readonly queryClient?: import('@tanstack/react-query').QueryClient;
81
81
  }
82
- export type { InferProcedureInput, InferProcedureOutput, ProcedureCollection, ClientConfig, ClientFromRouter, };
82
+ export type { ClientConfig, ClientFromRouter, InferProcedureInput, InferProcedureOutput, ProcedureCollection, };
package/dist/types.d.ts CHANGED
@@ -23,7 +23,7 @@ export type ProcedureType = 'query' | 'mutation';
23
23
  *
24
24
  * @see {@link https://github.com/veloxts/velox-ts-framework/velox | @veloxts/router CompiledProcedure}
25
25
  */
26
- export interface ClientProcedure<TInput = unknown, TOutput = unknown, TType extends ProcedureType = ProcedureType> {
26
+ export interface ClientProcedure<TInput = unknown, TOutput = unknown, TType extends ProcedureType = ProcedureType, TErrors = never> {
27
27
  /** Whether this is a query or mutation */
28
28
  readonly type: TType;
29
29
  /** The procedure handler function - uses `any` for ctx to enable contravariant matching with CompiledProcedure */
@@ -53,13 +53,15 @@ export interface ClientProcedure<TInput = unknown, TOutput = unknown, TType exte
53
53
  resource: string;
54
54
  param: string;
55
55
  };
56
+ /** Phantom type holder for error types — not used at runtime */
57
+ readonly _errors?: TErrors;
56
58
  }
57
59
  /**
58
60
  * Record of named procedures
59
61
  *
60
62
  * NOTE: Uses `any` for variance compatibility with @veloxts/router's ProcedureRecord
61
63
  */
62
- export type ProcedureRecord = Record<string, ClientProcedure<any, any, any>>;
64
+ export type ProcedureRecord = Record<string, ClientProcedure<any, any, any, any>>;
63
65
  /**
64
66
  * Procedure collection with namespace
65
67
  *
@@ -169,15 +171,39 @@ export type IsTRPCNamespace<T> = T extends Record<string, {
169
171
  $types: unknown;
170
172
  };
171
173
  }> ? true : false;
174
+ /**
175
+ * Extracts the error types from a procedure's `_errors` phantom field
176
+ *
177
+ * Returns the TErrors union directly from the phantom. Works with both
178
+ * `CompiledProcedure` and `ClientProcedure`.
179
+ */
180
+ type ExtractProcedureErrors<T> = T extends {
181
+ readonly _errors?: infer E;
182
+ } ? E : never;
183
+ /**
184
+ * A callable client procedure method with error type information
185
+ *
186
+ * Extends a plain function type with a phantom `_errors` property so that
187
+ * `InferProcedureErrors<typeof client.namespace.method>` can extract the
188
+ * declared domain error types from client callables.
189
+ *
190
+ * @template TInput - The validated input type
191
+ * @template TOutput - The handler output type
192
+ * @template TErrors - Union of domain error types (defaults to never)
193
+ */
194
+ export type ClientCallable<TInput, TOutput, TErrors = never> = ((input: TInput) => Promise<TOutput>) & {
195
+ readonly _errors?: TErrors;
196
+ };
172
197
  /**
173
198
  * Builds a callable client interface from a single procedure collection
174
199
  *
175
- * For each procedure, creates a method that:
200
+ * For each procedure, creates a `ClientCallable` method that:
176
201
  * - Takes the procedure's input type as parameter
177
202
  * - Returns a Promise of the procedure's output type
203
+ * - Carries the procedure's declared error types via a `_errors` phantom
178
204
  */
179
205
  export type ClientFromCollection<TCollection extends ProcedureCollection> = {
180
- [K in keyof TCollection['procedures']]: (input: InferProcedureInput<TCollection['procedures'][K]>) => Promise<InferProcedureOutput<TCollection['procedures'][K]>>;
206
+ [K in keyof TCollection['procedures']]: ClientCallable<InferProcedureInput<TCollection['procedures'][K]>, InferProcedureOutput<TCollection['procedures'][K]>, ExtractProcedureErrors<TCollection['procedures'][K]>>;
181
207
  };
182
208
  /**
183
209
  * Builds a complete client interface from a router (collection of collections)
@@ -385,11 +411,60 @@ export interface ClientError extends Error {
385
411
  code?: string;
386
412
  /** Original response body (if available) */
387
413
  body?: unknown;
414
+ /**
415
+ * Typed domain error payload, extracted from `body.data` when the response
416
+ * is a DomainError (has a `code` field). Distinct from `body`.
417
+ */
418
+ data?: unknown;
388
419
  /** URL that was requested */
389
420
  url: string;
390
421
  /** HTTP method used */
391
422
  method: string;
392
423
  }
424
+ /**
425
+ * Extracts the union of domain error shapes from a procedure or client callable.
426
+ *
427
+ * Supports two extraction paths:
428
+ * 1. **`_errors` phantom** (primary) — extracts `{ code, data }` from the TErrors
429
+ * union carried by `CompiledProcedure`, `ClientProcedure`, or `ClientCallable`.
430
+ * 2. **`errorClasses` constructors** (fallback) — extracts from constructor signatures
431
+ * when the `_errors` phantom is absent or empty.
432
+ *
433
+ * @example From a procedure
434
+ * ```typescript
435
+ * const proc = procedure().throws(InsufficientFundsError, UserBannedError).query(...);
436
+ * type Errors = InferProcedureErrors<typeof proc>;
437
+ * // → { code: 'INSUFFICIENT_FUNDS'; data: { amount: number } }
438
+ * // | { code: 'USER_BANNED'; data: { reason: string } }
439
+ * ```
440
+ *
441
+ * @example From a client callable
442
+ * ```typescript
443
+ * const client = createClient<AppRouter>({ baseUrl: '/api' });
444
+ * type Errors = InferProcedureErrors<typeof client.orders.createOrder>;
445
+ * // Same result — errors are threaded through ClientFromCollection
446
+ * ```
447
+ */
448
+ export type InferProcedureErrors<T> = _ExtractErrorsFromPhantom<T> extends never ? _ExtractErrorsFromClasses<T> : _ExtractErrorsFromPhantom<T>;
449
+ /** @internal Extracts `{ code, data }` shapes from the `_errors` phantom type */
450
+ type _ExtractErrorsFromPhantom<T> = T extends {
451
+ readonly _errors?: infer E;
452
+ } ? E extends {
453
+ readonly code: infer C;
454
+ readonly data: infer D;
455
+ } ? {
456
+ code: C;
457
+ data: D;
458
+ } : never : never;
459
+ /** @internal Fallback: extracts `{ code, data }` from `errorClasses` constructor signatures */
460
+ type _ExtractErrorsFromClasses<T> = T extends {
461
+ readonly errorClasses?: ReadonlyArray<infer E>;
462
+ } ? E extends new (data: infer D) => {
463
+ code: infer C;
464
+ } ? {
465
+ code: C;
466
+ data: D;
467
+ } : never : never;
393
468
  /**
394
469
  * Internal representation of a procedure call
395
470
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veloxts/client",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Type-safe frontend API client for VeloxTS framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,12 +40,12 @@
40
40
  "@testing-library/react": "16.3.2",
41
41
  "@types/react": "19.2.14",
42
42
  "@types/react-dom": "19.2.3",
43
- "@vitest/coverage-v8": "4.0.18",
44
- "jsdom": "28.0.0",
43
+ "@vitest/coverage-v8": "4.1.0",
44
+ "jsdom": "28.1.0",
45
45
  "react": "19.2.4",
46
46
  "react-dom": "19.2.4",
47
47
  "typescript": "5.9.3",
48
- "vitest": "4.0.18"
48
+ "vitest": "4.1.0"
49
49
  },
50
50
  "keywords": [
51
51
  "velox",