@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.
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/auto-loader-JZT4AGYX.d.ts +73 -0
- package/dist/bind-zSx7Joxv.d.ts +17 -0
- package/dist/client/index.d.ts +89 -93
- package/dist/client/index.js +77 -85
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/index.d.ts +108 -2
- package/dist/codegen/index.js +667 -131
- package/dist/codegen/index.js.map +1 -1
- package/dist/db/index.d.ts +231 -43
- package/dist/db/index.js +985 -1293
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +497 -0
- package/dist/env/index.js +1129 -0
- package/dist/env/index.js.map +1 -0
- package/dist/index.d.ts +105 -71
- package/dist/index.js +2135 -12015
- package/dist/index.js.map +1 -1
- package/dist/postgres-errors-BJqDsXfG.d.ts +391 -0
- package/dist/route/index.d.ts +4 -49
- package/dist/route/index.js +941 -155
- package/dist/route/index.js.map +1 -1
- package/dist/server/index.d.ts +13 -0
- package/dist/server/index.js +2127 -11999
- package/dist/server/index.js.map +1 -1
- package/dist/types-C0u_SdUv.d.ts +57 -0
- package/package.json +32 -20
- package/dist/auto-loader-C44TcLmM.d.ts +0 -125
- package/dist/bind-pssq1NRT.d.ts +0 -34
- package/dist/postgres-errors-CY_Es8EJ.d.ts +0 -1703
- package/dist/scripts/index.d.ts +0 -24
- package/dist/scripts/index.js +0 -1201
- package/dist/scripts/index.js.map +0 -1
- package/dist/scripts/templates/api-index.template.txt +0 -10
- package/dist/scripts/templates/api-tag.template.txt +0 -11
- package/dist/scripts/templates/contract.template.txt +0 -87
- package/dist/scripts/templates/entity-type.template.txt +0 -31
- package/dist/scripts/templates/entity.template.txt +0 -19
- package/dist/scripts/templates/index.template.txt +0 -10
- package/dist/scripts/templates/repository.template.txt +0 -37
- package/dist/scripts/templates/routes-id.template.txt +0 -59
- package/dist/scripts/templates/routes-index.template.txt +0 -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 };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import '../auto-loader-
|
|
2
|
-
import { R as RouteContract, I as InferContract } from '../types-
|
|
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
|
|
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
|
-
|
|
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>(
|
|
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
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
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
|
-
*
|
|
154
|
-
*
|
|
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
|
|
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
|
-
*
|
|
119
|
+
* Type guard for timeout errors
|
|
161
120
|
*
|
|
162
121
|
* @example
|
|
163
122
|
* ```ts
|
|
164
|
-
*
|
|
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
|
-
*
|
|
167
|
-
*
|
|
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
|
|
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 };
|
package/dist/client/index.js
CHANGED
|
@@ -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(
|
|
32
|
+
async call(contract, options) {
|
|
91
33
|
const baseUrl = options?.baseUrl || this.config.baseUrl;
|
|
92
|
-
const urlPath = buildUrl(
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
`Request timed out after ${this.config.timeout}ms`,
|
|
121
69
|
0,
|
|
122
|
-
|
|
123
|
-
|
|
70
|
+
url,
|
|
71
|
+
void 0,
|
|
72
|
+
"timeout"
|
|
124
73
|
);
|
|
125
74
|
}
|
|
126
75
|
if (error instanceof Error) {
|
|
127
76
|
throw new ApiClientError(
|
|
128
|
-
|
|
77
|
+
`Network error: ${error.message}`,
|
|
129
78
|
0,
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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
|