@spfn/core 0.1.0-alpha.4 → 0.1.0-alpha.40

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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/auto-loader-JZT4AGYX.d.ts +73 -0
  4. package/dist/bind-zSx7Joxv.d.ts +17 -0
  5. package/dist/client/index.d.ts +89 -93
  6. package/dist/client/index.js +77 -85
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/codegen/index.d.ts +108 -2
  9. package/dist/codegen/index.js +667 -131
  10. package/dist/codegen/index.js.map +1 -1
  11. package/dist/db/index.d.ts +231 -43
  12. package/dist/db/index.js +985 -1293
  13. package/dist/db/index.js.map +1 -1
  14. package/dist/env/index.d.ts +497 -0
  15. package/dist/env/index.js +1129 -0
  16. package/dist/env/index.js.map +1 -0
  17. package/dist/index.d.ts +105 -71
  18. package/dist/index.js +2135 -12015
  19. package/dist/index.js.map +1 -1
  20. package/dist/postgres-errors-BJqDsXfG.d.ts +391 -0
  21. package/dist/route/index.d.ts +4 -49
  22. package/dist/route/index.js +941 -155
  23. package/dist/route/index.js.map +1 -1
  24. package/dist/server/index.d.ts +13 -0
  25. package/dist/server/index.js +2127 -11999
  26. package/dist/server/index.js.map +1 -1
  27. package/dist/types-C0u_SdUv.d.ts +57 -0
  28. package/package.json +32 -20
  29. package/dist/auto-loader-C44TcLmM.d.ts +0 -125
  30. package/dist/bind-pssq1NRT.d.ts +0 -34
  31. package/dist/postgres-errors-CY_Es8EJ.d.ts +0 -1703
  32. package/dist/scripts/index.d.ts +0 -24
  33. package/dist/scripts/index.js +0 -1201
  34. package/dist/scripts/index.js.map +0 -1
  35. package/dist/scripts/templates/api-index.template.txt +0 -10
  36. package/dist/scripts/templates/api-tag.template.txt +0 -11
  37. package/dist/scripts/templates/contract.template.txt +0 -87
  38. package/dist/scripts/templates/entity-type.template.txt +0 -31
  39. package/dist/scripts/templates/entity.template.txt +0 -19
  40. package/dist/scripts/templates/index.template.txt +0 -10
  41. package/dist/scripts/templates/repository.template.txt +0 -37
  42. package/dist/scripts/templates/routes-id.template.txt +0 -59
  43. package/dist/scripts/templates/routes-index.template.txt +0 -44
  44. package/dist/types-SlzTr8ZO.d.ts +0 -143
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 INFLIKE Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -485,6 +485,30 @@ Server configuration and lifecycle management.
485
485
 
486
486
  **[→ Read Server Documentation](./src/server/README.md)**
487
487
 
488
+ ### 📝 Logger
489
+ High-performance logging with multiple transports, sensitive data masking, and automatic validation.
490
+
491
+ **[→ Read Logger Documentation](./src/logger/README.md)**
492
+
493
+ **Key Features:**
494
+ - Adapter pattern (Pino for production, custom for full control)
495
+ - Sensitive data masking (passwords, tokens, API keys)
496
+ - File rotation (date and size-based) with automatic cleanup
497
+ - Configuration validation with clear error messages
498
+ - Multiple transports (Console, File, Slack, Email)
499
+
500
+ ### ⚙️ Code Generation
501
+ Automatic code generation with pluggable generators and centralized file watching.
502
+
503
+ **[→ Read Codegen Documentation](./src/codegen/README.md)**
504
+
505
+ **Key Features:**
506
+ - Orchestrator pattern for managing multiple generators
507
+ - Built-in contract generator for type-safe API clients
508
+ - Configuration-based setup (`.spfnrc.json` or `package.json`)
509
+ - Watch mode integrated into `spfn dev`
510
+ - Extensible with custom generators
511
+
488
512
  ## Module Exports
489
513
 
490
514
  ### Main Export
@@ -522,6 +546,11 @@ import {
522
546
  import { initRedis, getRedis, getRedisRead } from '@spfn/core';
523
547
  ```
524
548
 
549
+ ### Logger
550
+ ```typescript
551
+ import { logger } from '@spfn/core';
552
+ ```
553
+
525
554
  ### Client (for frontend)
526
555
  ```typescript
527
556
  import { ContractClient, createClient } from '@spfn/core/client';
@@ -545,6 +574,17 @@ REDIS_READ_URL=redis://replica:6379
545
574
  PORT=8790
546
575
  HOST=localhost
547
576
  NODE_ENV=development
577
+
578
+ # Server Timeouts (optional, in milliseconds)
579
+ SERVER_TIMEOUT=120000 # Request timeout (default: 120000)
580
+ SERVER_KEEPALIVE_TIMEOUT=65000 # Keep-alive timeout (default: 65000)
581
+ SERVER_HEADERS_TIMEOUT=60000 # Headers timeout (default: 60000)
582
+ SHUTDOWN_TIMEOUT=30000 # Graceful shutdown timeout (default: 30000)
583
+
584
+ # Logger (optional)
585
+ LOGGER_ADAPTER=pino # pino | custom (default: pino)
586
+ LOGGER_FILE_ENABLED=true # Enable file logging (production only)
587
+ LOG_DIR=/var/log/myapp # Log directory (required when file logging enabled)
548
588
  ```
549
589
 
550
590
  ## Requirements
@@ -574,6 +614,8 @@ npm test -- --coverage # With coverage
574
614
  - [Error Handling](./src/errors/README.md)
575
615
  - [Middleware](./src/middleware/README.md)
576
616
  - [Server Configuration](./src/server/README.md)
617
+ - [Logger](./src/logger/README.md)
618
+ - [Code Generation](./src/codegen/README.md)
577
619
 
578
620
  ### API Reference
579
621
  - See module-specific README files linked above
@@ -0,0 +1,73 @@
1
+ import { MiddlewareHandler, Hono } from 'hono';
2
+
3
+ declare module 'hono' {
4
+ interface ContextVariableMap {
5
+ _skipMiddlewares?: string[];
6
+ }
7
+ }
8
+ /**
9
+ * AutoRouteLoader: Simplified File-based Routing System
10
+ *
11
+ * Features:
12
+ * - Auto-discovery: Scans routes directory and auto-registers
13
+ * - Dynamic routes: [id] → :id, [...slug] → *
14
+ * - Statistics: Route registration stats for dashboard
15
+ * - Grouping: Natural grouping by directory structure
16
+ */
17
+ type RouteInfo = {
18
+ path: string;
19
+ file: string;
20
+ meta?: {
21
+ description?: string;
22
+ tags?: string[];
23
+ auth?: boolean;
24
+ [key: string]: unknown;
25
+ };
26
+ priority: number;
27
+ };
28
+ type RouteStats = {
29
+ total: number;
30
+ byPriority: {
31
+ static: number;
32
+ dynamic: number;
33
+ catchAll: number;
34
+ };
35
+ byTag: Record<string, number>;
36
+ routes: RouteInfo[];
37
+ };
38
+ declare class AutoRouteLoader {
39
+ private routesDir;
40
+ private routes;
41
+ private registeredRoutes;
42
+ private debug;
43
+ private readonly middlewares;
44
+ constructor(routesDir: string, debug?: boolean, middlewares?: Array<{
45
+ name: string;
46
+ handler: MiddlewareHandler;
47
+ }>);
48
+ load(app: Hono): Promise<RouteStats>;
49
+ getStats(): RouteStats;
50
+ private scanFiles;
51
+ private isValidRouteFile;
52
+ private loadRoute;
53
+ private validateModule;
54
+ private checkRouteConflict;
55
+ private registerContractBasedMiddlewares;
56
+ private registerFileBasedMiddlewares;
57
+ private categorizeAndLogError;
58
+ private fileToPath;
59
+ private calculatePriority;
60
+ private normalizePath;
61
+ private logRegistrationOrder;
62
+ private logStats;
63
+ }
64
+ declare function loadRoutes(app: Hono, options?: {
65
+ routesDir?: string;
66
+ debug?: boolean;
67
+ middlewares?: Array<{
68
+ name: string;
69
+ handler: MiddlewareHandler;
70
+ }>;
71
+ }): Promise<RouteStats>;
72
+
73
+ export { AutoRouteLoader as A, type RouteInfo as R, type RouteStats as a, loadRoutes as l };
@@ -0,0 +1,17 @@
1
+ import { Context } from 'hono';
2
+ import { R as RouteContract, c as RouteContext } from './types-C0u_SdUv.js';
3
+
4
+ /**
5
+ * Contract-based Route Handler Wrapper
6
+ *
7
+ * Binds a contract to a route handler, providing automatic validation
8
+ * and type-safe context creation.
9
+ *
10
+ * Features:
11
+ * - Automatic params/query/body validation using TypeBox
12
+ * - Type-safe RouteContext with contract-based inference
13
+ * - Clean separation: bind() for validation, Hono for middleware
14
+ */
15
+ declare function bind<TContract extends RouteContract>(contract: TContract, handler: (c: RouteContext<TContract>) => Response | Promise<Response>): (rawContext: Context) => Promise<Response>;
16
+
17
+ export { bind as b };
@@ -1,5 +1,5 @@
1
- import '../auto-loader-C44TcLmM.js';
2
- import { R as RouteContract, I as InferContract } from '../types-SlzTr8ZO.js';
1
+ import '../auto-loader-JZT4AGYX.js';
2
+ import { R as RouteContract, I as InferContract } from '../types-C0u_SdUv.js';
3
3
  import 'hono';
4
4
  import 'hono/utils/http-status';
5
5
  import '@sinclair/typebox';
@@ -8,31 +8,12 @@ import '@sinclair/typebox';
8
8
  * Contract-Based API Client
9
9
  *
10
10
  * Type-safe HTTP client that works with RouteContract for full end-to-end type safety
11
- *
12
- * @example
13
- * ```ts
14
- * import { createClient } from '@spfn/core/client';
15
- * import { getUserContract } from './contracts';
16
- *
17
- * const client = createClient({ baseUrl: 'http://localhost:4000' });
18
- * const user = await client.call(getUserContract, { params: { id: '123' } });
19
- * // ✅ user is fully typed based on contract.response
20
- * ```
21
11
  */
22
12
 
23
- /**
24
- * Request interceptor function
25
- *
26
- * Allows modifying request before it's sent
27
- */
28
13
  type RequestInterceptor = (url: string, init: RequestInit) => Promise<RequestInit> | RequestInit;
29
- /**
30
- * Client configuration
31
- */
32
14
  interface ClientConfig {
33
15
  /**
34
16
  * API base URL (e.g., http://localhost:4000)
35
- * Can be overridden per request
36
17
  */
37
18
  baseUrl?: string;
38
19
  /**
@@ -44,33 +25,15 @@ interface ClientConfig {
44
25
  */
45
26
  timeout?: number;
46
27
  /**
47
- * Custom fetch implementation (for testing or custom behavior)
28
+ * Custom fetch implementation
48
29
  */
49
30
  fetch?: typeof fetch;
50
31
  }
51
- /**
52
- * Request options for API calls
53
- */
54
32
  interface CallOptions<TContract extends RouteContract> {
55
- /**
56
- * Path parameters (for dynamic routes like /users/:id)
57
- */
58
33
  params?: InferContract<TContract>['params'];
59
- /**
60
- * Query parameters (for URL query strings)
61
- */
62
34
  query?: InferContract<TContract>['query'];
63
- /**
64
- * Request body (for POST, PUT, PATCH)
65
- */
66
35
  body?: InferContract<TContract>['body'];
67
- /**
68
- * Additional headers for this specific request
69
- */
70
36
  headers?: Record<string, string>;
71
- /**
72
- * Override base URL for this request
73
- */
74
37
  baseUrl?: string;
75
38
  }
76
39
  /**
@@ -80,7 +43,8 @@ declare class ApiClientError extends Error {
80
43
  readonly status: number;
81
44
  readonly url: string;
82
45
  readonly response?: unknown | undefined;
83
- constructor(message: string, status: number, url: string, response?: unknown | undefined);
46
+ readonly errorType?: "timeout" | "network" | "http" | undefined;
47
+ constructor(message: string, status: number, url: string, response?: unknown | undefined, errorType?: "timeout" | "network" | "http" | undefined);
84
48
  }
85
49
  /**
86
50
  * Contract-based API Client
@@ -91,83 +55,115 @@ declare class ContractClient {
91
55
  constructor(config?: ClientConfig);
92
56
  /**
93
57
  * Add request interceptor
94
- *
95
- * Interceptors are executed in the order they are added
96
- *
97
- * @example
98
- * ```ts
99
- * client.use(async (url, init) => {
100
- * // Add auth header
101
- * return {
102
- * ...init,
103
- * headers: {
104
- * ...init.headers,
105
- * Authorization: `Bearer ${token}`
106
- * }
107
- * };
108
- * });
109
- * ```
110
58
  */
111
59
  use(interceptor: RequestInterceptor): void;
112
60
  /**
113
61
  * Make a type-safe API call using a contract
114
- *
115
- * @example
116
- * ```ts
117
- * const getUserContract = {
118
- * params: Type.Object({ id: Type.String() }),
119
- * response: Type.Object({ id: Type.Number(), name: Type.String() })
120
- * } as const satisfies RouteContract;
121
- *
122
- * const user = await client.call('/users/:id', getUserContract, {
123
- * params: { id: '123' }
124
- * });
125
- * // ✅ user.name is typed as string
126
- * ```
127
62
  */
128
- call<TContract extends RouteContract>(path: string, contract: TContract, options?: CallOptions<TContract>): Promise<InferContract<TContract>['response']>;
63
+ call<TContract extends RouteContract>(contract: TContract, options?: CallOptions<TContract>): Promise<InferContract<TContract>['response']>;
129
64
  /**
130
65
  * Create a new client with merged configuration
131
- *
132
- * Useful for creating clients with specific auth tokens or custom headers
133
- *
134
- * @example
135
- * ```ts
136
- * const authClient = client.withConfig({
137
- * headers: { Authorization: `Bearer ${token}` }
138
- * });
139
- * ```
140
66
  */
141
67
  withConfig(config: Partial<ClientConfig>): ContractClient;
68
+ private static buildUrl;
69
+ private static buildQuery;
70
+ private static getHttpMethod;
71
+ private static isFormData;
142
72
  }
143
73
  /**
144
74
  * Create a new contract-based API client
75
+ */
76
+ declare function createClient(config?: ClientConfig): ContractClient;
77
+ /**
78
+ * Configure the global client instance
79
+ *
80
+ * Call this in your app initialization to set default configuration
81
+ * for all auto-generated API calls.
145
82
  *
146
83
  * @example
147
84
  * ```ts
148
- * const client = createClient({
149
- * baseUrl: 'http://localhost:4000',
150
- * headers: { 'X-Custom': 'header' }
85
+ * // In app initialization (layout.tsx, _app.tsx, etc)
86
+ * import { configureClient } from '@spfn/core/client';
87
+ *
88
+ * configureClient({
89
+ * baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',
90
+ * timeout: 60000,
91
+ * headers: {
92
+ * 'X-App-Version': '1.0.0'
93
+ * }
151
94
  * });
152
95
  *
153
- * const user = await client.call('/users/:id', getUserContract, {
154
- * params: { id: '123' }
96
+ * // Add interceptors
97
+ * import { client } from '@spfn/core/client';
98
+ * client.use(async (url, init) => {
99
+ * // Add auth header
100
+ * return {
101
+ * ...init,
102
+ * headers: {
103
+ * ...init.headers,
104
+ * Authorization: `Bearer ${getToken()}`
105
+ * }
106
+ * };
155
107
  * });
156
108
  * ```
157
109
  */
158
- declare function createClient(config?: ClientConfig): ContractClient;
110
+ declare function configureClient(config: ClientConfig): void;
111
+ /**
112
+ * Global client singleton with Proxy
113
+ *
114
+ * This client can be configured using configureClient() before use.
115
+ * Used by auto-generated API client code.
116
+ */
117
+ declare const client: ContractClient;
159
118
  /**
160
- * Default client instance
119
+ * Type guard for timeout errors
161
120
  *
162
121
  * @example
163
122
  * ```ts
164
- * import { client } from '@spfn/core/client';
123
+ * try {
124
+ * await api.users.getById({ params: { id: '123' } });
125
+ * } catch (error) {
126
+ * if (isTimeoutError(error)) {
127
+ * console.error('Request timed out, retrying...');
128
+ * // Implement retry logic
129
+ * }
130
+ * }
131
+ * ```
132
+ */
133
+ declare function isTimeoutError(error: unknown): error is ApiClientError;
134
+ /**
135
+ * Type guard for network errors
165
136
  *
166
- * const user = await client.call('/users/:id', getUserContract, {
167
- * params: { id: '123' }
168
- * });
137
+ * @example
138
+ * ```ts
139
+ * try {
140
+ * await api.users.list();
141
+ * } catch (error) {
142
+ * if (isNetworkError(error)) {
143
+ * showOfflineMessage();
144
+ * }
145
+ * }
169
146
  * ```
170
147
  */
171
- declare const client: ContractClient;
148
+ declare function isNetworkError(error: unknown): error is ApiClientError;
149
+ /**
150
+ * Type guard for HTTP errors (4xx, 5xx)
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * try {
155
+ * await api.users.create({ body: userData });
156
+ * } catch (error) {
157
+ * if (isHttpError(error)) {
158
+ * if (error.status === 401) {
159
+ * redirectToLogin();
160
+ * } else if (error.status === 404) {
161
+ * showNotFoundMessage();
162
+ * }
163
+ * }
164
+ * }
165
+ * ```
166
+ */
167
+ declare function isHttpError(error: unknown): error is ApiClientError;
172
168
 
173
- export { ApiClientError, type CallOptions, type ClientConfig, ContractClient, type RequestInterceptor, client, createClient };
169
+ export { ApiClientError, type CallOptions, type ClientConfig, ContractClient, type RequestInterceptor, client, configureClient, createClient, isHttpError, isNetworkError, isTimeoutError };
@@ -1,43 +1,14 @@
1
1
  // src/client/contract-client.ts
2
2
  var ApiClientError = class extends Error {
3
- constructor(message, status, url, response) {
3
+ constructor(message, status, url, response, errorType) {
4
4
  super(message);
5
5
  this.status = status;
6
6
  this.url = url;
7
7
  this.response = response;
8
+ this.errorType = errorType;
8
9
  this.name = "ApiClientError";
9
10
  }
10
11
  };
11
- function buildUrl(path, params) {
12
- if (!params) return path;
13
- let url = path;
14
- for (const [key, value] of Object.entries(params)) {
15
- url = url.replace(`:${key}`, String(value));
16
- }
17
- return url;
18
- }
19
- function buildQuery(query) {
20
- if (!query || Object.keys(query).length === 0) return "";
21
- const params = new URLSearchParams();
22
- for (const [key, value] of Object.entries(query)) {
23
- if (Array.isArray(value)) {
24
- value.forEach((v) => params.append(key, String(v)));
25
- } else if (value !== void 0 && value !== null) {
26
- params.append(key, String(value));
27
- }
28
- }
29
- const queryString = params.toString();
30
- return queryString ? `?${queryString}` : "";
31
- }
32
- function getHttpMethod(contract, options) {
33
- if ("method" in contract && typeof contract.method === "string") {
34
- return contract.method.toUpperCase();
35
- }
36
- if (options?.body !== void 0) {
37
- return "POST";
38
- }
39
- return "GET";
40
- }
41
12
  var ContractClient = class _ContractClient {
42
13
  config;
43
14
  interceptors = [];
@@ -46,58 +17,35 @@ var ContractClient = class _ContractClient {
46
17
  baseUrl: config.baseUrl || process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000",
47
18
  headers: config.headers || {},
48
19
  timeout: config.timeout || 3e4,
49
- fetch: config.fetch || globalThis.fetch
20
+ fetch: config.fetch || globalThis.fetch.bind(globalThis)
50
21
  };
51
22
  }
52
23
  /**
53
24
  * Add request interceptor
54
- *
55
- * Interceptors are executed in the order they are added
56
- *
57
- * @example
58
- * ```ts
59
- * client.use(async (url, init) => {
60
- * // Add auth header
61
- * return {
62
- * ...init,
63
- * headers: {
64
- * ...init.headers,
65
- * Authorization: `Bearer ${token}`
66
- * }
67
- * };
68
- * });
69
- * ```
70
25
  */
71
26
  use(interceptor) {
72
27
  this.interceptors.push(interceptor);
73
28
  }
74
29
  /**
75
30
  * Make a type-safe API call using a contract
76
- *
77
- * @example
78
- * ```ts
79
- * const getUserContract = {
80
- * params: Type.Object({ id: Type.String() }),
81
- * response: Type.Object({ id: Type.Number(), name: Type.String() })
82
- * } as const satisfies RouteContract;
83
- *
84
- * const user = await client.call('/users/:id', getUserContract, {
85
- * params: { id: '123' }
86
- * });
87
- * // ✅ user.name is typed as string
88
- * ```
89
31
  */
90
- async call(path, contract, options) {
32
+ async call(contract, options) {
91
33
  const baseUrl = options?.baseUrl || this.config.baseUrl;
92
- const urlPath = buildUrl(path, options?.params);
93
- const queryString = buildQuery(options?.query);
34
+ const urlPath = _ContractClient.buildUrl(
35
+ contract.path,
36
+ options?.params
37
+ );
38
+ const queryString = _ContractClient.buildQuery(
39
+ options?.query
40
+ );
94
41
  const url = `${baseUrl}${urlPath}${queryString}`;
95
- const method = getHttpMethod(contract, options);
42
+ const method = _ContractClient.getHttpMethod(contract, options);
96
43
  const headers = {
97
44
  ...this.config.headers,
98
45
  ...options?.headers
99
46
  };
100
- if (options?.body !== void 0 && !headers["Content-Type"]) {
47
+ const isFormData = _ContractClient.isFormData(options?.body);
48
+ if (options?.body !== void 0 && !isFormData && !headers["Content-Type"]) {
101
49
  headers["Content-Type"] = "application/json";
102
50
  }
103
51
  let init = {
@@ -105,7 +53,7 @@ var ContractClient = class _ContractClient {
105
53
  headers
106
54
  };
107
55
  if (options?.body !== void 0) {
108
- init.body = JSON.stringify(options.body);
56
+ init.body = isFormData ? options.body : JSON.stringify(options.body);
109
57
  }
110
58
  const controller = new AbortController();
111
59
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -117,18 +65,20 @@ var ContractClient = class _ContractClient {
117
65
  clearTimeout(timeoutId);
118
66
  if (error instanceof Error && error.name === "AbortError") {
119
67
  throw new ApiClientError(
120
- `${method} ${urlPath} timed out after ${this.config.timeout}ms`,
68
+ `Request timed out after ${this.config.timeout}ms`,
121
69
  0,
122
- "Timeout",
123
- url
70
+ url,
71
+ void 0,
72
+ "timeout"
124
73
  );
125
74
  }
126
75
  if (error instanceof Error) {
127
76
  throw new ApiClientError(
128
- `${method} ${urlPath} network error: ${error.message}`,
77
+ `Network error: ${error.message}`,
129
78
  0,
130
- "Network Error",
131
- url
79
+ url,
80
+ void 0,
81
+ "network"
132
82
  );
133
83
  }
134
84
  throw error;
@@ -140,7 +90,8 @@ var ContractClient = class _ContractClient {
140
90
  `${method} ${urlPath} failed: ${response.status} ${response.statusText}`,
141
91
  response.status,
142
92
  url,
143
- errorBody
93
+ errorBody,
94
+ "http"
144
95
  );
145
96
  }
146
97
  const data = await response.json();
@@ -148,15 +99,6 @@ var ContractClient = class _ContractClient {
148
99
  }
149
100
  /**
150
101
  * Create a new client with merged configuration
151
- *
152
- * Useful for creating clients with specific auth tokens or custom headers
153
- *
154
- * @example
155
- * ```ts
156
- * const authClient = client.withConfig({
157
- * headers: { Authorization: `Bearer ${token}` }
158
- * });
159
- * ```
160
102
  */
161
103
  withConfig(config) {
162
104
  return new _ContractClient({
@@ -166,12 +108,62 @@ var ContractClient = class _ContractClient {
166
108
  fetch: config.fetch || this.config.fetch
167
109
  });
168
110
  }
111
+ static buildUrl(path, params) {
112
+ if (!params) return path;
113
+ let url = path;
114
+ for (const [key, value] of Object.entries(params)) {
115
+ url = url.replace(`:${key}`, String(value));
116
+ }
117
+ return url;
118
+ }
119
+ static buildQuery(query) {
120
+ if (!query || Object.keys(query).length === 0) return "";
121
+ const params = new URLSearchParams();
122
+ for (const [key, value] of Object.entries(query)) {
123
+ if (Array.isArray(value)) {
124
+ value.forEach((v) => params.append(key, String(v)));
125
+ } else if (value !== void 0 && value !== null) {
126
+ params.append(key, String(value));
127
+ }
128
+ }
129
+ const queryString = params.toString();
130
+ return queryString ? `?${queryString}` : "";
131
+ }
132
+ static getHttpMethod(contract, options) {
133
+ if ("method" in contract && typeof contract.method === "string") {
134
+ return contract.method.toUpperCase();
135
+ }
136
+ if (options?.body !== void 0) {
137
+ return "POST";
138
+ }
139
+ return "GET";
140
+ }
141
+ static isFormData(body) {
142
+ return body instanceof FormData;
143
+ }
169
144
  };
170
145
  function createClient(config) {
171
146
  return new ContractClient(config);
172
147
  }
173
- var client = createClient();
148
+ var _clientInstance = new ContractClient();
149
+ function configureClient(config) {
150
+ _clientInstance = new ContractClient(config);
151
+ }
152
+ var client = new Proxy({}, {
153
+ get(_target, prop) {
154
+ return _clientInstance[prop];
155
+ }
156
+ });
157
+ function isTimeoutError(error) {
158
+ return error instanceof ApiClientError && error.errorType === "timeout";
159
+ }
160
+ function isNetworkError(error) {
161
+ return error instanceof ApiClientError && error.errorType === "network";
162
+ }
163
+ function isHttpError(error) {
164
+ return error instanceof ApiClientError && error.errorType === "http";
165
+ }
174
166
 
175
- export { ApiClientError, ContractClient, client, createClient };
167
+ export { ApiClientError, ContractClient, client, configureClient, createClient, isHttpError, isNetworkError, isTimeoutError };
176
168
  //# sourceMappingURL=index.js.map
177
169
  //# sourceMappingURL=index.js.map