@spfn/core 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +580 -0
- package/dist/auto-loader-C44TcLmM.d.ts +125 -0
- package/dist/bind-pssq1NRT.d.ts +34 -0
- package/dist/client/index.d.ts +174 -0
- package/dist/client/index.js +179 -0
- package/dist/client/index.js.map +1 -0
- package/dist/codegen/index.d.ts +126 -0
- package/dist/codegen/index.js +970 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/db/index.d.ts +83 -0
- package/dist/db/index.js +2099 -0
- package/dist/db/index.js.map +1 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.js +13042 -0
- package/dist/index.js.map +1 -0
- package/dist/postgres-errors-CY_Es8EJ.d.ts +1703 -0
- package/dist/route/index.d.ts +72 -0
- package/dist/route/index.js +442 -0
- package/dist/route/index.js.map +1 -0
- package/dist/scripts/index.d.ts +24 -0
- package/dist/scripts/index.js +1157 -0
- package/dist/scripts/index.js.map +1 -0
- package/dist/scripts/templates/api-index.template.txt +10 -0
- package/dist/scripts/templates/api-tag.template.txt +11 -0
- package/dist/scripts/templates/contract.template.txt +87 -0
- package/dist/scripts/templates/entity-type.template.txt +31 -0
- package/dist/scripts/templates/entity.template.txt +19 -0
- package/dist/scripts/templates/index.template.txt +10 -0
- package/dist/scripts/templates/repository.template.txt +37 -0
- package/dist/scripts/templates/routes-id.template.txt +59 -0
- package/dist/scripts/templates/routes-index.template.txt +44 -0
- package/dist/server/index.d.ts +303 -0
- package/dist/server/index.js +12923 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types-SlzTr8ZO.d.ts +143 -0
- package/package.json +119 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extend Hono Context to support skipMiddlewares metadata
|
|
5
|
+
*/
|
|
6
|
+
declare module 'hono' {
|
|
7
|
+
interface ContextVariableMap {
|
|
8
|
+
_skipMiddlewares?: string[];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* AutoRouteLoader: Simplified File-based Routing System
|
|
13
|
+
*
|
|
14
|
+
* ## Features
|
|
15
|
+
* - 📁 Auto-discovery: Scans routes directory and auto-registers
|
|
16
|
+
* - 🔄 Dynamic routes: [id] → :id, [...slug] → *
|
|
17
|
+
* - 📊 Statistics: Route registration stats for dashboard
|
|
18
|
+
* - 🏷️ Grouping: Natural grouping by directory structure
|
|
19
|
+
*
|
|
20
|
+
* ## Usage
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const app = new Hono();
|
|
23
|
+
* await loadRoutes(app);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
type RouteInfo = {
|
|
27
|
+
/** URL path (e.g., /users/:id) */
|
|
28
|
+
path: string;
|
|
29
|
+
/** File path relative to routes dir */
|
|
30
|
+
file: string;
|
|
31
|
+
/** Route metadata from export */
|
|
32
|
+
meta?: {
|
|
33
|
+
description?: string;
|
|
34
|
+
tags?: string[];
|
|
35
|
+
auth?: boolean;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
};
|
|
38
|
+
/** Priority (1=static, 2=dynamic, 3=catch-all) */
|
|
39
|
+
priority: number;
|
|
40
|
+
};
|
|
41
|
+
type RouteStats = {
|
|
42
|
+
total: number;
|
|
43
|
+
byPriority: {
|
|
44
|
+
static: number;
|
|
45
|
+
dynamic: number;
|
|
46
|
+
catchAll: number;
|
|
47
|
+
};
|
|
48
|
+
byTag: Record<string, number>;
|
|
49
|
+
routes: RouteInfo[];
|
|
50
|
+
};
|
|
51
|
+
declare class AutoRouteLoader {
|
|
52
|
+
private routesDir;
|
|
53
|
+
private routes;
|
|
54
|
+
private registeredRoutes;
|
|
55
|
+
private debug;
|
|
56
|
+
private readonly middlewares;
|
|
57
|
+
constructor(routesDir: string, debug?: boolean, middlewares?: Array<{
|
|
58
|
+
name: string;
|
|
59
|
+
handler: any;
|
|
60
|
+
}>);
|
|
61
|
+
/**
|
|
62
|
+
* Load all routes from directory
|
|
63
|
+
*/
|
|
64
|
+
load(app: Hono): Promise<RouteStats>;
|
|
65
|
+
/**
|
|
66
|
+
* Get route statistics
|
|
67
|
+
*/
|
|
68
|
+
getStats(): RouteStats;
|
|
69
|
+
/**
|
|
70
|
+
* Recursively scan directory for .ts files
|
|
71
|
+
*/
|
|
72
|
+
private scanFiles;
|
|
73
|
+
/**
|
|
74
|
+
* Check if file is a valid route file
|
|
75
|
+
*/
|
|
76
|
+
private isValidRouteFile;
|
|
77
|
+
/**
|
|
78
|
+
* Load and register a single route
|
|
79
|
+
* Returns true if successful, false if failed
|
|
80
|
+
*/
|
|
81
|
+
private loadRoute;
|
|
82
|
+
/**
|
|
83
|
+
* Convert file path to URL path
|
|
84
|
+
*
|
|
85
|
+
* Examples:
|
|
86
|
+
* - users/index.ts → /users
|
|
87
|
+
* - users/[id].ts → /users/:id
|
|
88
|
+
* - posts/[...slug].ts → /posts/*
|
|
89
|
+
*/
|
|
90
|
+
private fileToPath;
|
|
91
|
+
/**
|
|
92
|
+
* Calculate route priority
|
|
93
|
+
* 1 = static, 2 = dynamic, 3 = catch-all
|
|
94
|
+
*/
|
|
95
|
+
private calculatePriority;
|
|
96
|
+
/**
|
|
97
|
+
* Normalize path for conflict detection
|
|
98
|
+
*
|
|
99
|
+
* Converts dynamic parameter names to generic placeholders:
|
|
100
|
+
* - /users/:id → /users/:param
|
|
101
|
+
* - /users/:userId → /users/:param (conflict!)
|
|
102
|
+
* - /posts/* → /posts/* (unchanged)
|
|
103
|
+
*
|
|
104
|
+
* This allows detection of routes with different param names
|
|
105
|
+
* that would match the same URL patterns.
|
|
106
|
+
*/
|
|
107
|
+
private normalizePath;
|
|
108
|
+
/**
|
|
109
|
+
* Log statistics
|
|
110
|
+
*/
|
|
111
|
+
private logStats;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Load routes from default location (src/server/routes)
|
|
115
|
+
*/
|
|
116
|
+
declare function loadRoutes(app: Hono, options?: {
|
|
117
|
+
routesDir?: string;
|
|
118
|
+
debug?: boolean;
|
|
119
|
+
middlewares?: Array<{
|
|
120
|
+
name: string;
|
|
121
|
+
handler: any;
|
|
122
|
+
}>;
|
|
123
|
+
}): Promise<RouteStats>;
|
|
124
|
+
|
|
125
|
+
export { AutoRouteLoader as A, type RouteInfo as R, type RouteStats as a, loadRoutes as l };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { R as RouteContract, c as RouteContext } from './types-SlzTr8ZO.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
|
+
* ## Usage
|
|
16
|
+
*
|
|
17
|
+
* ```typescript
|
|
18
|
+
* // Basic usage
|
|
19
|
+
* export const GET = bind(contract, async (c) => {
|
|
20
|
+
* return c.json({ data: 'public' });
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* // For middleware, use Hono's app-level or route-level middleware:
|
|
24
|
+
* // app.use('/api/*', authMiddleware);
|
|
25
|
+
* // app.get('/users/:id', authMiddleware, bind(contract, handler));
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @param contract - Route contract defining params, query, body, response schemas
|
|
29
|
+
* @param handler - Route handler function
|
|
30
|
+
* @returns Hono-compatible handler function
|
|
31
|
+
*/
|
|
32
|
+
declare function bind<TContract extends RouteContract>(contract: TContract, handler: (c: RouteContext<TContract>) => Response | Promise<Response>): (rawContext: Context) => Promise<Response>;
|
|
33
|
+
|
|
34
|
+
export { bind as b };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import '../auto-loader-C44TcLmM.js';
|
|
2
|
+
import { R as RouteContract, I as InferContract } from '../types-SlzTr8ZO.js';
|
|
3
|
+
import 'hono';
|
|
4
|
+
import 'hono/utils/http-status';
|
|
5
|
+
import '@sinclair/typebox';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Contract-Based API Client
|
|
9
|
+
*
|
|
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
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Request interceptor function
|
|
25
|
+
*
|
|
26
|
+
* Allows modifying request before it's sent
|
|
27
|
+
*/
|
|
28
|
+
type RequestInterceptor = (url: string, init: RequestInit) => Promise<RequestInit> | RequestInit;
|
|
29
|
+
/**
|
|
30
|
+
* Client configuration
|
|
31
|
+
*/
|
|
32
|
+
interface ClientConfig {
|
|
33
|
+
/**
|
|
34
|
+
* API base URL (e.g., http://localhost:4000)
|
|
35
|
+
* Can be overridden per request
|
|
36
|
+
*/
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Default headers to include in all requests
|
|
40
|
+
*/
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* Request timeout in milliseconds
|
|
44
|
+
*/
|
|
45
|
+
timeout?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Custom fetch implementation (for testing or custom behavior)
|
|
48
|
+
*/
|
|
49
|
+
fetch?: typeof fetch;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Request options for API calls
|
|
53
|
+
*/
|
|
54
|
+
interface CallOptions<TContract extends RouteContract> {
|
|
55
|
+
/**
|
|
56
|
+
* Path parameters (for dynamic routes like /users/:id)
|
|
57
|
+
*/
|
|
58
|
+
params?: InferContract<TContract>['params'];
|
|
59
|
+
/**
|
|
60
|
+
* Query parameters (for URL query strings)
|
|
61
|
+
*/
|
|
62
|
+
query?: InferContract<TContract>['query'];
|
|
63
|
+
/**
|
|
64
|
+
* Request body (for POST, PUT, PATCH)
|
|
65
|
+
*/
|
|
66
|
+
body?: InferContract<TContract>['body'];
|
|
67
|
+
/**
|
|
68
|
+
* Additional headers for this specific request
|
|
69
|
+
*/
|
|
70
|
+
headers?: Record<string, string>;
|
|
71
|
+
/**
|
|
72
|
+
* Override base URL for this request
|
|
73
|
+
*/
|
|
74
|
+
baseUrl?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* API Client Error
|
|
78
|
+
*/
|
|
79
|
+
declare class ApiClientError extends Error {
|
|
80
|
+
readonly status: number;
|
|
81
|
+
readonly statusText: string;
|
|
82
|
+
readonly url: string;
|
|
83
|
+
readonly response?: unknown | undefined;
|
|
84
|
+
constructor(message: string, status: number, statusText: string, url: string, response?: unknown | undefined);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Contract-based API Client
|
|
88
|
+
*/
|
|
89
|
+
declare class ContractClient {
|
|
90
|
+
private readonly config;
|
|
91
|
+
private readonly interceptors;
|
|
92
|
+
constructor(config?: ClientConfig);
|
|
93
|
+
/**
|
|
94
|
+
* Add request interceptor
|
|
95
|
+
*
|
|
96
|
+
* Interceptors are executed in the order they are added
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* client.use(async (url, init) => {
|
|
101
|
+
* // Add auth header
|
|
102
|
+
* return {
|
|
103
|
+
* ...init,
|
|
104
|
+
* headers: {
|
|
105
|
+
* ...init.headers,
|
|
106
|
+
* Authorization: `Bearer ${token}`
|
|
107
|
+
* }
|
|
108
|
+
* };
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
use(interceptor: RequestInterceptor): void;
|
|
113
|
+
/**
|
|
114
|
+
* Make a type-safe API call using a contract
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* const getUserContract = {
|
|
119
|
+
* params: Type.Object({ id: Type.String() }),
|
|
120
|
+
* response: Type.Object({ id: Type.Number(), name: Type.String() })
|
|
121
|
+
* } as const satisfies RouteContract;
|
|
122
|
+
*
|
|
123
|
+
* const user = await client.call('/users/:id', getUserContract, {
|
|
124
|
+
* params: { id: '123' }
|
|
125
|
+
* });
|
|
126
|
+
* // ✅ user.name is typed as string
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
call<TContract extends RouteContract>(path: string, contract: TContract, options?: CallOptions<TContract>): Promise<InferContract<TContract>['response']>;
|
|
130
|
+
/**
|
|
131
|
+
* Create a new client with merged configuration
|
|
132
|
+
*
|
|
133
|
+
* Useful for creating clients with specific auth tokens or custom headers
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* const authClient = client.withConfig({
|
|
138
|
+
* headers: { Authorization: `Bearer ${token}` }
|
|
139
|
+
* });
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
withConfig(config: Partial<ClientConfig>): ContractClient;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Create a new contract-based API client
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const client = createClient({
|
|
150
|
+
* baseUrl: 'http://localhost:4000',
|
|
151
|
+
* headers: { 'X-Custom': 'header' }
|
|
152
|
+
* });
|
|
153
|
+
*
|
|
154
|
+
* const user = await client.call('/users/:id', getUserContract, {
|
|
155
|
+
* params: { id: '123' }
|
|
156
|
+
* });
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
declare function createClient(config?: ClientConfig): ContractClient;
|
|
160
|
+
/**
|
|
161
|
+
* Default client instance
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* import { client } from '@spfn/core/client';
|
|
166
|
+
*
|
|
167
|
+
* const user = await client.call('/users/:id', getUserContract, {
|
|
168
|
+
* params: { id: '123' }
|
|
169
|
+
* });
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
declare const client: ContractClient;
|
|
173
|
+
|
|
174
|
+
export { ApiClientError, type CallOptions, type ClientConfig, ContractClient, type RequestInterceptor, client, createClient };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// src/client/contract-client.ts
|
|
2
|
+
var ApiClientError = class extends Error {
|
|
3
|
+
constructor(message, status, statusText, url, response) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.statusText = statusText;
|
|
7
|
+
this.url = url;
|
|
8
|
+
this.response = response;
|
|
9
|
+
this.name = "ApiClientError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function buildUrl(path, params) {
|
|
13
|
+
if (!params) return path;
|
|
14
|
+
let url = path;
|
|
15
|
+
for (const [key, value] of Object.entries(params)) {
|
|
16
|
+
url = url.replace(`:${key}`, String(value));
|
|
17
|
+
}
|
|
18
|
+
return url;
|
|
19
|
+
}
|
|
20
|
+
function buildQuery(query) {
|
|
21
|
+
if (!query || Object.keys(query).length === 0) return "";
|
|
22
|
+
const params = new URLSearchParams();
|
|
23
|
+
for (const [key, value] of Object.entries(query)) {
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
value.forEach((v) => params.append(key, String(v)));
|
|
26
|
+
} else if (value !== void 0 && value !== null) {
|
|
27
|
+
params.append(key, String(value));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const queryString = params.toString();
|
|
31
|
+
return queryString ? `?${queryString}` : "";
|
|
32
|
+
}
|
|
33
|
+
function getHttpMethod(contract, options) {
|
|
34
|
+
if ("method" in contract && typeof contract.method === "string") {
|
|
35
|
+
return contract.method.toUpperCase();
|
|
36
|
+
}
|
|
37
|
+
if (options?.body !== void 0) {
|
|
38
|
+
return "POST";
|
|
39
|
+
}
|
|
40
|
+
return "GET";
|
|
41
|
+
}
|
|
42
|
+
var ContractClient = class _ContractClient {
|
|
43
|
+
config;
|
|
44
|
+
interceptors = [];
|
|
45
|
+
constructor(config = {}) {
|
|
46
|
+
this.config = {
|
|
47
|
+
baseUrl: config.baseUrl || process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000",
|
|
48
|
+
headers: config.headers || {},
|
|
49
|
+
timeout: config.timeout || 3e4,
|
|
50
|
+
fetch: config.fetch || globalThis.fetch
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Add request interceptor
|
|
55
|
+
*
|
|
56
|
+
* Interceptors are executed in the order they are added
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* client.use(async (url, init) => {
|
|
61
|
+
* // Add auth header
|
|
62
|
+
* return {
|
|
63
|
+
* ...init,
|
|
64
|
+
* headers: {
|
|
65
|
+
* ...init.headers,
|
|
66
|
+
* Authorization: `Bearer ${token}`
|
|
67
|
+
* }
|
|
68
|
+
* };
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
use(interceptor) {
|
|
73
|
+
this.interceptors.push(interceptor);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Make a type-safe API call using a contract
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* const getUserContract = {
|
|
81
|
+
* params: Type.Object({ id: Type.String() }),
|
|
82
|
+
* response: Type.Object({ id: Type.Number(), name: Type.String() })
|
|
83
|
+
* } as const satisfies RouteContract;
|
|
84
|
+
*
|
|
85
|
+
* const user = await client.call('/users/:id', getUserContract, {
|
|
86
|
+
* params: { id: '123' }
|
|
87
|
+
* });
|
|
88
|
+
* // ✅ user.name is typed as string
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
async call(path, contract, options) {
|
|
92
|
+
const baseUrl = options?.baseUrl || this.config.baseUrl;
|
|
93
|
+
const urlPath = buildUrl(path, options?.params);
|
|
94
|
+
const queryString = buildQuery(options?.query);
|
|
95
|
+
const url = `${baseUrl}${urlPath}${queryString}`;
|
|
96
|
+
const method = getHttpMethod(contract, options);
|
|
97
|
+
const headers = {
|
|
98
|
+
...this.config.headers,
|
|
99
|
+
...options?.headers
|
|
100
|
+
};
|
|
101
|
+
if (options?.body !== void 0 && !headers["Content-Type"]) {
|
|
102
|
+
headers["Content-Type"] = "application/json";
|
|
103
|
+
}
|
|
104
|
+
let init = {
|
|
105
|
+
method,
|
|
106
|
+
headers
|
|
107
|
+
};
|
|
108
|
+
if (options?.body !== void 0) {
|
|
109
|
+
init.body = JSON.stringify(options.body);
|
|
110
|
+
}
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
113
|
+
init.signal = controller.signal;
|
|
114
|
+
for (const interceptor of this.interceptors) {
|
|
115
|
+
init = await interceptor(url, init);
|
|
116
|
+
}
|
|
117
|
+
const response = await this.config.fetch(url, init).catch((error) => {
|
|
118
|
+
clearTimeout(timeoutId);
|
|
119
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
120
|
+
throw new ApiClientError(
|
|
121
|
+
`${method} ${urlPath} timed out after ${this.config.timeout}ms`,
|
|
122
|
+
0,
|
|
123
|
+
"Timeout",
|
|
124
|
+
url
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (error instanceof Error) {
|
|
128
|
+
throw new ApiClientError(
|
|
129
|
+
`${method} ${urlPath} network error: ${error.message}`,
|
|
130
|
+
0,
|
|
131
|
+
"Network Error",
|
|
132
|
+
url
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
});
|
|
137
|
+
clearTimeout(timeoutId);
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const errorBody = await response.json().catch(() => null);
|
|
140
|
+
throw new ApiClientError(
|
|
141
|
+
`${method} ${urlPath} failed: ${response.status} ${response.statusText}`,
|
|
142
|
+
response.status,
|
|
143
|
+
response.statusText,
|
|
144
|
+
url,
|
|
145
|
+
errorBody
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
return data;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create a new client with merged configuration
|
|
153
|
+
*
|
|
154
|
+
* Useful for creating clients with specific auth tokens or custom headers
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* const authClient = client.withConfig({
|
|
159
|
+
* headers: { Authorization: `Bearer ${token}` }
|
|
160
|
+
* });
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
withConfig(config) {
|
|
164
|
+
return new _ContractClient({
|
|
165
|
+
baseUrl: config.baseUrl || this.config.baseUrl,
|
|
166
|
+
headers: { ...this.config.headers, ...config.headers },
|
|
167
|
+
timeout: config.timeout || this.config.timeout,
|
|
168
|
+
fetch: config.fetch || this.config.fetch
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
function createClient(config) {
|
|
173
|
+
return new ContractClient(config);
|
|
174
|
+
}
|
|
175
|
+
var client = createClient();
|
|
176
|
+
|
|
177
|
+
export { ApiClientError, ContractClient, client, createClient };
|
|
178
|
+
//# sourceMappingURL=index.js.map
|
|
179
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/contract-client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Contract-Based API Client\n *\n * Type-safe HTTP client that works with RouteContract for full end-to-end type safety\n *\n * @example\n * ```ts\n * import { createClient } from '@spfn/core/client';\n * import { getUserContract } from './contracts';\n *\n * const client = createClient({ baseUrl: 'http://localhost:4000' });\n * const user = await client.call(getUserContract, { params: { id: '123' } });\n * // ✅ user is fully typed based on contract.response\n * ```\n */\n\nimport type { RouteContract, InferContract } from '../route';\n\n/**\n * Request interceptor function\n *\n * Allows modifying request before it's sent\n */\nexport type RequestInterceptor = (\n url: string,\n init: RequestInit\n) => Promise<RequestInit> | RequestInit;\n\n/**\n * Client configuration\n */\nexport interface ClientConfig\n{\n /**\n * API base URL (e.g., http://localhost:4000)\n * Can be overridden per request\n */\n baseUrl?: string;\n\n /**\n * Default headers to include in all requests\n */\n headers?: Record<string, string>;\n\n /**\n * Request timeout in milliseconds\n */\n timeout?: number;\n\n /**\n * Custom fetch implementation (for testing or custom behavior)\n */\n fetch?: typeof fetch;\n}\n\n/**\n * Request options for API calls\n */\nexport interface CallOptions<TContract extends RouteContract>\n{\n /**\n * Path parameters (for dynamic routes like /users/:id)\n */\n params?: InferContract<TContract>['params'];\n\n /**\n * Query parameters (for URL query strings)\n */\n query?: InferContract<TContract>['query'];\n\n /**\n * Request body (for POST, PUT, PATCH)\n */\n body?: InferContract<TContract>['body'];\n\n /**\n * Additional headers for this specific request\n */\n headers?: Record<string, string>;\n\n /**\n * Override base URL for this request\n */\n baseUrl?: string;\n}\n\n/**\n * API Client Error\n */\nexport class ApiClientError extends Error\n{\n constructor(\n message: string,\n public readonly status: number,\n public readonly statusText: string,\n public readonly url: string,\n public readonly response?: unknown\n )\n {\n super(message);\n this.name = 'ApiClientError';\n }\n}\n\n/**\n * Build URL with path parameters replaced\n *\n * @example\n * buildUrl('/users/:id', { id: '123' }) → '/users/123'\n * buildUrl('/posts/:postId/comments/:id', { postId: '1', id: '2' }) → '/posts/1/comments/2'\n */\nfunction buildUrl(path: string, params?: Record<string, string | number>): string\n{\n if (!params) return path;\n\n let url = path;\n for (const [key, value] of Object.entries(params))\n {\n url = url.replace(`:${key}`, String(value));\n }\n\n return url;\n}\n\n/**\n * Build query string from object\n *\n * @example\n * buildQuery({ page: '1', limit: '10' }) → '?page=1&limit=10'\n */\nfunction buildQuery(query?: Record<string, string | string[] | number | boolean>): string\n{\n if (!query || Object.keys(query).length === 0) return '';\n\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query))\n {\n if (Array.isArray(value))\n {\n value.forEach((v) => params.append(key, String(v)));\n }\n else if (value !== undefined && value !== null)\n {\n params.append(key, String(value));\n }\n }\n\n const queryString = params.toString();\n return queryString ? `?${queryString}` : '';\n}\n\n/**\n * Extract HTTP method from contract or infer from request type\n */\nfunction getHttpMethod<TContract extends RouteContract>(\n contract: TContract,\n options?: CallOptions<TContract>\n): string\n{\n // If contract has explicit method, use it\n if ('method' in contract && typeof contract.method === 'string')\n {\n return contract.method.toUpperCase();\n }\n\n // Infer from presence of body\n if (options?.body !== undefined)\n {\n return 'POST';\n }\n\n // Default to GET\n return 'GET';\n}\n\n/**\n * Contract-based API Client\n */\nexport class ContractClient\n{\n private readonly config: Required<ClientConfig>;\n private readonly interceptors: RequestInterceptor[] = [];\n\n constructor(config: ClientConfig = {})\n {\n this.config = {\n baseUrl: config.baseUrl || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',\n headers: config.headers || {},\n timeout: config.timeout || 30000,\n fetch: config.fetch || globalThis.fetch,\n };\n }\n\n /**\n * Add request interceptor\n *\n * Interceptors are executed in the order they are added\n *\n * @example\n * ```ts\n * client.use(async (url, init) => {\n * // Add auth header\n * return {\n * ...init,\n * headers: {\n * ...init.headers,\n * Authorization: `Bearer ${token}`\n * }\n * };\n * });\n * ```\n */\n use(interceptor: RequestInterceptor): void\n {\n this.interceptors.push(interceptor);\n }\n\n /**\n * Make a type-safe API call using a contract\n *\n * @example\n * ```ts\n * const getUserContract = {\n * params: Type.Object({ id: Type.String() }),\n * response: Type.Object({ id: Type.Number(), name: Type.String() })\n * } as const satisfies RouteContract;\n *\n * const user = await client.call('/users/:id', getUserContract, {\n * params: { id: '123' }\n * });\n * // ✅ user.name is typed as string\n * ```\n */\n async call<TContract extends RouteContract>(\n path: string,\n contract: TContract,\n options?: CallOptions<TContract>\n ): Promise<InferContract<TContract>['response']>\n {\n // Build URL\n const baseUrl = options?.baseUrl || this.config.baseUrl;\n const urlPath = buildUrl(path, options?.params as Record<string, string | number>);\n const queryString = buildQuery(options?.query as Record<string, string | string[] | number | boolean>);\n const url = `${baseUrl}${urlPath}${queryString}`;\n\n // Determine HTTP method\n const method = getHttpMethod(contract, options);\n\n // Build headers\n const headers: Record<string, string> = {\n ...this.config.headers,\n ...options?.headers,\n };\n\n // Add Content-Type for requests with body\n if (options?.body !== undefined && !headers['Content-Type'])\n {\n headers['Content-Type'] = 'application/json';\n }\n\n // Build request init\n let init: RequestInit = {\n method,\n headers,\n };\n\n // Add body for POST/PUT/PATCH\n if (options?.body !== undefined)\n {\n init.body = JSON.stringify(options.body);\n }\n\n // Create abort controller for timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n init.signal = controller.signal;\n\n // Execute interceptors\n for (const interceptor of this.interceptors)\n {\n init = await interceptor(url, init);\n }\n\n // Make request\n const response = await this.config.fetch(url, init).catch((error) =>\n {\n clearTimeout(timeoutId);\n\n // Handle abort (timeout)\n if (error instanceof Error && error.name === 'AbortError')\n {\n throw new ApiClientError(\n `${method} ${urlPath} timed out after ${this.config.timeout}ms`,\n 0,\n 'Timeout',\n url\n );\n }\n\n // Handle network errors\n if (error instanceof Error)\n {\n throw new ApiClientError(\n `${method} ${urlPath} network error: ${error.message}`,\n 0,\n 'Network Error',\n url\n );\n }\n\n // Unknown error\n throw error;\n });\n\n // Clear timeout\n clearTimeout(timeoutId);\n\n // Handle non-OK responses\n if (!response.ok)\n {\n const errorBody = await response.json().catch(() => null);\n throw new ApiClientError(\n `${method} ${urlPath} failed: ${response.status} ${response.statusText}`,\n response.status,\n response.statusText,\n url,\n errorBody\n );\n }\n\n // Parse and return response\n const data = await response.json();\n return data as InferContract<TContract>['response'];\n }\n\n /**\n * Create a new client with merged configuration\n *\n * Useful for creating clients with specific auth tokens or custom headers\n *\n * @example\n * ```ts\n * const authClient = client.withConfig({\n * headers: { Authorization: `Bearer ${token}` }\n * });\n * ```\n */\n withConfig(config: Partial<ClientConfig>): ContractClient\n {\n return new ContractClient({\n baseUrl: config.baseUrl || this.config.baseUrl,\n headers: { ...this.config.headers, ...config.headers },\n timeout: config.timeout || this.config.timeout,\n fetch: config.fetch || this.config.fetch,\n });\n }\n}\n\n/**\n * Create a new contract-based API client\n *\n * @example\n * ```ts\n * const client = createClient({\n * baseUrl: 'http://localhost:4000',\n * headers: { 'X-Custom': 'header' }\n * });\n *\n * const user = await client.call('/users/:id', getUserContract, {\n * params: { id: '123' }\n * });\n * ```\n */\nexport function createClient(config?: ClientConfig): ContractClient\n{\n return new ContractClient(config);\n}\n\n/**\n * Default client instance\n *\n * @example\n * ```ts\n * import { client } from '@spfn/core/client';\n *\n * const user = await client.call('/users/:id', getUserContract, {\n * params: { id: '123' }\n * });\n * ```\n */\nexport const client = createClient();"]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { d as HttpMethod } from '../types-SlzTr8ZO.js';
|
|
2
|
+
import 'hono';
|
|
3
|
+
import 'hono/utils/http-status';
|
|
4
|
+
import '@sinclair/typebox';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Code Generation Types
|
|
8
|
+
*
|
|
9
|
+
* Types for contract detection and client code generation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Route-Contract mapping extracted from contract files
|
|
14
|
+
*/
|
|
15
|
+
interface RouteContractMapping {
|
|
16
|
+
/** HTTP method (GET, POST, etc.) */
|
|
17
|
+
method: HttpMethod;
|
|
18
|
+
/** URL path (e.g., /users/:id) */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Contract variable name (e.g., getUserContract) */
|
|
21
|
+
contractName: string;
|
|
22
|
+
/** Import path for the contract (e.g., @/contracts/users) */
|
|
23
|
+
contractImportPath: string;
|
|
24
|
+
/** Route file path */
|
|
25
|
+
routeFile: string;
|
|
26
|
+
/** Contract source file path (resolved) */
|
|
27
|
+
contractFile?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Grouped routes by resource
|
|
31
|
+
*/
|
|
32
|
+
interface ResourceRoutes {
|
|
33
|
+
[resource: string]: RouteContractMapping[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Client generation options
|
|
37
|
+
*/
|
|
38
|
+
interface ClientGenerationOptions {
|
|
39
|
+
/** Routes directory to scan */
|
|
40
|
+
routesDir: string;
|
|
41
|
+
/** Output file path for generated client */
|
|
42
|
+
outputPath: string;
|
|
43
|
+
/** Base URL for the API client */
|
|
44
|
+
baseUrl?: string;
|
|
45
|
+
/** Include type imports? */
|
|
46
|
+
includeTypes?: boolean;
|
|
47
|
+
/** Generate JSDoc comments? */
|
|
48
|
+
includeJsDoc?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Generation statistics
|
|
52
|
+
*/
|
|
53
|
+
interface GenerationStats {
|
|
54
|
+
/** Total routes scanned */
|
|
55
|
+
routesScanned: number;
|
|
56
|
+
/** Routes with contracts found */
|
|
57
|
+
contractsFound: number;
|
|
58
|
+
/** Unique contract files */
|
|
59
|
+
contractFiles: number;
|
|
60
|
+
/** Resources generated */
|
|
61
|
+
resourcesGenerated: number;
|
|
62
|
+
/** Total methods generated */
|
|
63
|
+
methodsGenerated: number;
|
|
64
|
+
/** Generation time in ms */
|
|
65
|
+
duration: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Contract Scanner
|
|
70
|
+
*
|
|
71
|
+
* Scans server/contracts directory and extracts exported contracts
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Scan routes directory for contract.ts files and extract contract exports
|
|
76
|
+
*
|
|
77
|
+
* @param routesDir - Path to server/routes directory
|
|
78
|
+
* @returns Array of contract-to-route mappings
|
|
79
|
+
*/
|
|
80
|
+
declare function scanContracts(routesDir: string): Promise<RouteContractMapping[]>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Route Scanner Utilities
|
|
84
|
+
*
|
|
85
|
+
* Helper functions for grouping and organizing route-contract mappings
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Group mappings by resource
|
|
90
|
+
*/
|
|
91
|
+
declare function groupByResource(mappings: RouteContractMapping[]): Record<string, RouteContractMapping[]>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Client Code Generator
|
|
95
|
+
*
|
|
96
|
+
* Generates type-safe API client code from route-contract mappings
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate API client code
|
|
101
|
+
*/
|
|
102
|
+
declare function generateClient(mappings: RouteContractMapping[], options: ClientGenerationOptions): Promise<GenerationStats>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Contract Watcher & Client Generator
|
|
106
|
+
*
|
|
107
|
+
* Watches contract files and regenerates client code
|
|
108
|
+
*/
|
|
109
|
+
interface WatchGenerateOptions {
|
|
110
|
+
/** Routes directory (default: src/server/routes) */
|
|
111
|
+
routesDir?: string;
|
|
112
|
+
/** Output path for generated client (default: src/lib/api/client.ts) */
|
|
113
|
+
outputPath?: string;
|
|
114
|
+
/** Base URL for API client */
|
|
115
|
+
baseUrl?: string;
|
|
116
|
+
/** Enable debug logging */
|
|
117
|
+
debug?: boolean;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Watch contracts and generate client code
|
|
121
|
+
*
|
|
122
|
+
* This file is meant to be run with tsx --watch
|
|
123
|
+
*/
|
|
124
|
+
declare function watchAndGenerate(options?: WatchGenerateOptions): Promise<void>;
|
|
125
|
+
|
|
126
|
+
export { type ClientGenerationOptions, type GenerationStats, HttpMethod, type ResourceRoutes, type RouteContractMapping, generateClient, groupByResource, scanContracts, watchAndGenerate };
|