@mandujs/core 0.9.1 → 0.9.3
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/package.json +1 -1
- package/src/brain/adapters/ollama.ts +1 -1
- package/src/brain/architecture/analyzer.ts +541 -0
- package/src/brain/architecture/index.ts +8 -0
- package/src/brain/architecture/types.ts +195 -0
- package/src/brain/index.ts +6 -2
- package/src/client/index.ts +2 -1
- package/src/contract/client.test.ts +308 -0
- package/src/contract/client.ts +345 -0
- package/src/contract/handler.ts +270 -0
- package/src/contract/index.ts +137 -1
- package/src/contract/infer.test.ts +346 -0
- package/src/contract/types.ts +83 -0
- package/src/filling/filling.ts +5 -1
- package/src/filling/index.ts +1 -1
- package/src/index.ts +75 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Contract Client
|
|
3
|
+
* Contract 기반 타입 안전 클라이언트
|
|
4
|
+
*
|
|
5
|
+
* tRPC/Elysia Eden 패턴 채택:
|
|
6
|
+
* - Contract에서 클라이언트 타입 자동 추론
|
|
7
|
+
* - 타입 안전 fetch 호출
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { z } from "zod";
|
|
11
|
+
import type {
|
|
12
|
+
ContractSchema,
|
|
13
|
+
ContractMethod,
|
|
14
|
+
MethodRequestSchema,
|
|
15
|
+
} from "./schema";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Client options for making requests
|
|
19
|
+
*/
|
|
20
|
+
export interface ClientOptions {
|
|
21
|
+
/** Base URL for API requests */
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
/** Default headers for all requests */
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
/** Custom fetch function (for SSR or testing) */
|
|
26
|
+
fetch?: typeof fetch;
|
|
27
|
+
/** Request timeout in milliseconds */
|
|
28
|
+
timeout?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Request options for a specific call
|
|
33
|
+
*/
|
|
34
|
+
export interface RequestOptions<
|
|
35
|
+
TQuery = unknown,
|
|
36
|
+
TBody = unknown,
|
|
37
|
+
TParams = unknown,
|
|
38
|
+
THeaders = Record<string, string>,
|
|
39
|
+
> {
|
|
40
|
+
query?: TQuery;
|
|
41
|
+
body?: TBody;
|
|
42
|
+
params?: TParams;
|
|
43
|
+
headers?: THeaders;
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Client response wrapper
|
|
49
|
+
*/
|
|
50
|
+
export interface ClientResponse<T> {
|
|
51
|
+
data: T;
|
|
52
|
+
status: number;
|
|
53
|
+
headers: Headers;
|
|
54
|
+
ok: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Infer request options from method schema
|
|
59
|
+
*/
|
|
60
|
+
type InferRequestOptions<T extends MethodRequestSchema | undefined> =
|
|
61
|
+
T extends MethodRequestSchema
|
|
62
|
+
? RequestOptions<
|
|
63
|
+
T["query"] extends z.ZodTypeAny ? z.input<T["query"]> : undefined,
|
|
64
|
+
T["body"] extends z.ZodTypeAny ? z.input<T["body"]> : undefined,
|
|
65
|
+
T["params"] extends z.ZodTypeAny ? z.input<T["params"]> : undefined,
|
|
66
|
+
T["headers"] extends z.ZodTypeAny
|
|
67
|
+
? z.input<T["headers"]>
|
|
68
|
+
: Record<string, string>
|
|
69
|
+
>
|
|
70
|
+
: RequestOptions<undefined, undefined, undefined, Record<string, string>>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Infer success response from contract
|
|
74
|
+
*/
|
|
75
|
+
type InferSuccessResponse<TResponse extends ContractSchema["response"]> =
|
|
76
|
+
TResponse[200] extends z.ZodTypeAny
|
|
77
|
+
? z.infer<TResponse[200]>
|
|
78
|
+
: TResponse[201] extends z.ZodTypeAny
|
|
79
|
+
? z.infer<TResponse[201]>
|
|
80
|
+
: unknown;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Contract client method
|
|
84
|
+
*/
|
|
85
|
+
export type ContractClientMethod<
|
|
86
|
+
T extends MethodRequestSchema | undefined,
|
|
87
|
+
TResponse extends ContractSchema["response"],
|
|
88
|
+
> = (
|
|
89
|
+
options?: InferRequestOptions<T>
|
|
90
|
+
) => Promise<ClientResponse<InferSuccessResponse<TResponse>>>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Contract client interface
|
|
94
|
+
*/
|
|
95
|
+
export type ContractClient<T extends ContractSchema> = {
|
|
96
|
+
[M in Extract<keyof T["request"], ContractMethod>]: ContractClientMethod<
|
|
97
|
+
T["request"][M] extends MethodRequestSchema ? T["request"][M] : undefined,
|
|
98
|
+
T["response"]
|
|
99
|
+
>;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build query string from object
|
|
104
|
+
*/
|
|
105
|
+
function buildQueryString(query: Record<string, unknown> | undefined): string {
|
|
106
|
+
if (!query) return "";
|
|
107
|
+
|
|
108
|
+
const params = new URLSearchParams();
|
|
109
|
+
for (const [key, value] of Object.entries(query)) {
|
|
110
|
+
if (value !== undefined && value !== null) {
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
for (const v of value) {
|
|
113
|
+
params.append(key, String(v));
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
params.append(key, String(value));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const str = params.toString();
|
|
122
|
+
return str ? `?${str}` : "";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Replace path parameters in URL
|
|
127
|
+
*/
|
|
128
|
+
function replacePathParams(
|
|
129
|
+
path: string,
|
|
130
|
+
params: Record<string, unknown> | undefined
|
|
131
|
+
): string {
|
|
132
|
+
if (!params) return path;
|
|
133
|
+
|
|
134
|
+
let result = path;
|
|
135
|
+
for (const [key, value] of Object.entries(params)) {
|
|
136
|
+
result = result.replace(`:${key}`, encodeURIComponent(String(value)));
|
|
137
|
+
result = result.replace(`[${key}]`, encodeURIComponent(String(value)));
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a type-safe client from a contract
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const userContract = Mandu.contract({
|
|
148
|
+
* request: {
|
|
149
|
+
* GET: { query: z.object({ page: z.number() }) },
|
|
150
|
+
* POST: { body: z.object({ name: z.string() }) },
|
|
151
|
+
* },
|
|
152
|
+
* response: {
|
|
153
|
+
* 200: z.object({ users: z.array(UserSchema) }),
|
|
154
|
+
* 201: z.object({ user: UserSchema }),
|
|
155
|
+
* },
|
|
156
|
+
* });
|
|
157
|
+
*
|
|
158
|
+
* const client = createClient(userContract, {
|
|
159
|
+
* baseUrl: "http://localhost:3000/api/users",
|
|
160
|
+
* });
|
|
161
|
+
*
|
|
162
|
+
* // Type-safe calls
|
|
163
|
+
* const users = await client.GET({ query: { page: 1 } });
|
|
164
|
+
* // users.data is typed as { users: User[] }
|
|
165
|
+
*
|
|
166
|
+
* const newUser = await client.POST({ body: { name: "Alice" } });
|
|
167
|
+
* // newUser.data is typed as { user: User }
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export function createClient<T extends ContractSchema>(
|
|
171
|
+
_contract: T,
|
|
172
|
+
options: ClientOptions
|
|
173
|
+
): ContractClient<T> {
|
|
174
|
+
const {
|
|
175
|
+
baseUrl,
|
|
176
|
+
headers: defaultHeaders = {},
|
|
177
|
+
fetch: customFetch = fetch,
|
|
178
|
+
timeout = 30000,
|
|
179
|
+
} = options;
|
|
180
|
+
|
|
181
|
+
const methods: ContractMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
182
|
+
|
|
183
|
+
const client = {} as ContractClient<T>;
|
|
184
|
+
|
|
185
|
+
for (const method of methods) {
|
|
186
|
+
// @ts-expect-error - Dynamic method assignment
|
|
187
|
+
client[method] = async (
|
|
188
|
+
requestOptions: RequestOptions = {}
|
|
189
|
+
): Promise<ClientResponse<unknown>> => {
|
|
190
|
+
const { query, body, params, headers = {}, signal } = requestOptions;
|
|
191
|
+
|
|
192
|
+
// Build URL
|
|
193
|
+
let url = replacePathParams(baseUrl, params as Record<string, unknown>);
|
|
194
|
+
url += buildQueryString(query as Record<string, unknown>);
|
|
195
|
+
|
|
196
|
+
// Build request options
|
|
197
|
+
const fetchOptions: RequestInit = {
|
|
198
|
+
method,
|
|
199
|
+
headers: {
|
|
200
|
+
...defaultHeaders,
|
|
201
|
+
...headers,
|
|
202
|
+
},
|
|
203
|
+
signal,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Add body for non-GET methods
|
|
207
|
+
if (body && method !== "GET") {
|
|
208
|
+
fetchOptions.headers = {
|
|
209
|
+
...fetchOptions.headers,
|
|
210
|
+
"Content-Type": "application/json",
|
|
211
|
+
};
|
|
212
|
+
fetchOptions.body = JSON.stringify(body);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Add timeout
|
|
216
|
+
const controller = new AbortController();
|
|
217
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
218
|
+
|
|
219
|
+
if (!signal) {
|
|
220
|
+
fetchOptions.signal = controller.signal;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const response = await customFetch(url, fetchOptions);
|
|
225
|
+
clearTimeout(timeoutId);
|
|
226
|
+
|
|
227
|
+
let data: unknown;
|
|
228
|
+
const contentType = response.headers.get("content-type") || "";
|
|
229
|
+
|
|
230
|
+
if (contentType.includes("application/json")) {
|
|
231
|
+
data = await response.json();
|
|
232
|
+
} else {
|
|
233
|
+
data = await response.text();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
data,
|
|
238
|
+
status: response.status,
|
|
239
|
+
headers: response.headers,
|
|
240
|
+
ok: response.ok,
|
|
241
|
+
};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
clearTimeout(timeoutId);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return client;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Type-safe fetch wrapper for a single endpoint
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```typescript
|
|
257
|
+
* const result = await contractFetch(userContract, "GET", "/api/users", {
|
|
258
|
+
* query: { page: 1, limit: 10 },
|
|
259
|
+
* });
|
|
260
|
+
* // result.data is typed based on contract response
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
export async function contractFetch<
|
|
264
|
+
T extends ContractSchema,
|
|
265
|
+
M extends Extract<keyof T["request"], ContractMethod>,
|
|
266
|
+
>(
|
|
267
|
+
_contract: T,
|
|
268
|
+
method: M,
|
|
269
|
+
url: string,
|
|
270
|
+
options: InferRequestOptions<
|
|
271
|
+
T["request"][M] extends MethodRequestSchema ? T["request"][M] : undefined
|
|
272
|
+
> = {} as InferRequestOptions<
|
|
273
|
+
T["request"][M] extends MethodRequestSchema ? T["request"][M] : undefined
|
|
274
|
+
>,
|
|
275
|
+
clientOptions: Partial<ClientOptions> = {}
|
|
276
|
+
): Promise<ClientResponse<InferSuccessResponse<T["response"]>>> {
|
|
277
|
+
const {
|
|
278
|
+
query,
|
|
279
|
+
body,
|
|
280
|
+
params,
|
|
281
|
+
headers = {},
|
|
282
|
+
signal,
|
|
283
|
+
} = options as RequestOptions;
|
|
284
|
+
|
|
285
|
+
const {
|
|
286
|
+
headers: defaultHeaders = {},
|
|
287
|
+
fetch: customFetch = fetch,
|
|
288
|
+
timeout = 30000,
|
|
289
|
+
} = clientOptions;
|
|
290
|
+
|
|
291
|
+
// Build URL
|
|
292
|
+
let finalUrl = replacePathParams(url, params as Record<string, unknown>);
|
|
293
|
+
finalUrl += buildQueryString(query as Record<string, unknown>);
|
|
294
|
+
|
|
295
|
+
// Build request options
|
|
296
|
+
const fetchOptions: RequestInit = {
|
|
297
|
+
method,
|
|
298
|
+
headers: {
|
|
299
|
+
...defaultHeaders,
|
|
300
|
+
...(headers as Record<string, string>),
|
|
301
|
+
},
|
|
302
|
+
signal,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Add body for non-GET methods
|
|
306
|
+
if (body && method !== "GET") {
|
|
307
|
+
fetchOptions.headers = {
|
|
308
|
+
...fetchOptions.headers,
|
|
309
|
+
"Content-Type": "application/json",
|
|
310
|
+
};
|
|
311
|
+
fetchOptions.body = JSON.stringify(body);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Add timeout
|
|
315
|
+
const controller = new AbortController();
|
|
316
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
317
|
+
|
|
318
|
+
if (!signal) {
|
|
319
|
+
fetchOptions.signal = controller.signal;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const response = await customFetch(finalUrl, fetchOptions);
|
|
324
|
+
clearTimeout(timeoutId);
|
|
325
|
+
|
|
326
|
+
let data: unknown;
|
|
327
|
+
const contentType = response.headers.get("content-type") || "";
|
|
328
|
+
|
|
329
|
+
if (contentType.includes("application/json")) {
|
|
330
|
+
data = await response.json();
|
|
331
|
+
} else {
|
|
332
|
+
data = await response.text();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
data: data as InferSuccessResponse<T["response"]>,
|
|
337
|
+
status: response.status,
|
|
338
|
+
headers: response.headers,
|
|
339
|
+
ok: response.ok,
|
|
340
|
+
};
|
|
341
|
+
} catch (error) {
|
|
342
|
+
clearTimeout(timeoutId);
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Contract Handler
|
|
3
|
+
* Contract 기반 타입 안전 핸들러 정의
|
|
4
|
+
*
|
|
5
|
+
* Elysia 패턴 채택: Contract → Handler 타입 자동 추론
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { z } from "zod";
|
|
9
|
+
import type {
|
|
10
|
+
ContractSchema,
|
|
11
|
+
ContractMethod,
|
|
12
|
+
MethodRequestSchema,
|
|
13
|
+
} from "./schema";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Typed request context for a handler
|
|
17
|
+
* Contract에서 추론된 타입으로 요청 컨텍스트 제공
|
|
18
|
+
*/
|
|
19
|
+
export interface TypedContext<
|
|
20
|
+
TQuery = unknown,
|
|
21
|
+
TBody = unknown,
|
|
22
|
+
TParams = unknown,
|
|
23
|
+
THeaders = unknown,
|
|
24
|
+
> {
|
|
25
|
+
/** Parsed and validated query parameters */
|
|
26
|
+
query: TQuery;
|
|
27
|
+
/** Parsed and validated request body */
|
|
28
|
+
body: TBody;
|
|
29
|
+
/** Parsed and validated path parameters */
|
|
30
|
+
params: TParams;
|
|
31
|
+
/** Parsed and validated headers */
|
|
32
|
+
headers: THeaders;
|
|
33
|
+
/** Original Request object */
|
|
34
|
+
request: Request;
|
|
35
|
+
/** Route path (e.g., "/users/:id") */
|
|
36
|
+
path: string;
|
|
37
|
+
/** HTTP method */
|
|
38
|
+
method: ContractMethod;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Infer context type from method schema
|
|
43
|
+
*/
|
|
44
|
+
type InferContextFromMethod<T extends MethodRequestSchema | undefined> =
|
|
45
|
+
T extends MethodRequestSchema
|
|
46
|
+
? TypedContext<
|
|
47
|
+
T["query"] extends z.ZodTypeAny ? z.infer<T["query"]> : undefined,
|
|
48
|
+
T["body"] extends z.ZodTypeAny ? z.infer<T["body"]> : undefined,
|
|
49
|
+
T["params"] extends z.ZodTypeAny ? z.infer<T["params"]> : undefined,
|
|
50
|
+
T["headers"] extends z.ZodTypeAny ? z.infer<T["headers"]> : undefined
|
|
51
|
+
>
|
|
52
|
+
: TypedContext<undefined, undefined, undefined, undefined>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Handler function type for a specific method
|
|
56
|
+
*/
|
|
57
|
+
export type HandlerFn<TContext, TResponse> = (
|
|
58
|
+
ctx: TContext
|
|
59
|
+
) => TResponse | Promise<TResponse>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Infer response type union from contract response schema
|
|
63
|
+
*/
|
|
64
|
+
type InferResponseUnion<TResponse extends ContractSchema["response"]> = {
|
|
65
|
+
[K in keyof TResponse]: TResponse[K] extends z.ZodTypeAny
|
|
66
|
+
? z.infer<TResponse[K]>
|
|
67
|
+
: never;
|
|
68
|
+
}[keyof TResponse];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Handler definition for all methods in a contract
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const contract = Mandu.contract({
|
|
76
|
+
* request: {
|
|
77
|
+
* GET: { query: z.object({ page: z.number() }) },
|
|
78
|
+
* POST: { body: z.object({ name: z.string() }) },
|
|
79
|
+
* },
|
|
80
|
+
* response: {
|
|
81
|
+
* 200: z.object({ users: z.array(z.string()) }),
|
|
82
|
+
* 201: z.object({ user: z.string() }),
|
|
83
|
+
* },
|
|
84
|
+
* });
|
|
85
|
+
*
|
|
86
|
+
* // handlers is typed: { GET: (ctx) => ..., POST: (ctx) => ... }
|
|
87
|
+
* const handlers = Mandu.handler(contract, {
|
|
88
|
+
* GET: (ctx) => {
|
|
89
|
+
* // ctx.query is { page: number }
|
|
90
|
+
* return { users: [] };
|
|
91
|
+
* },
|
|
92
|
+
* POST: (ctx) => {
|
|
93
|
+
* // ctx.body is { name: string }
|
|
94
|
+
* return { user: ctx.body.name };
|
|
95
|
+
* },
|
|
96
|
+
* });
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export type ContractHandlers<T extends ContractSchema> = {
|
|
100
|
+
[M in Extract<keyof T["request"], ContractMethod>]?: HandlerFn<
|
|
101
|
+
InferContextFromMethod<
|
|
102
|
+
T["request"][M] extends MethodRequestSchema ? T["request"][M] : undefined
|
|
103
|
+
>,
|
|
104
|
+
InferResponseUnion<T["response"]>
|
|
105
|
+
>;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Define type-safe handlers for a contract
|
|
110
|
+
*
|
|
111
|
+
* @param contract - The contract schema
|
|
112
|
+
* @param handlers - Handler implementations for each method
|
|
113
|
+
* @returns Typed handler object
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* const handlers = defineHandler(userContract, {
|
|
118
|
+
* GET: async (ctx) => {
|
|
119
|
+
* const { page, limit } = ctx.query; // Typed!
|
|
120
|
+
* const users = await db.users.findMany({ skip: page * limit, take: limit });
|
|
121
|
+
* return { data: users };
|
|
122
|
+
* },
|
|
123
|
+
* POST: async (ctx) => {
|
|
124
|
+
* const user = await db.users.create({ data: ctx.body }); // Typed!
|
|
125
|
+
* return { data: user };
|
|
126
|
+
* },
|
|
127
|
+
* });
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export function defineHandler<T extends ContractSchema>(
|
|
131
|
+
_contract: T,
|
|
132
|
+
handlers: ContractHandlers<T>
|
|
133
|
+
): ContractHandlers<T> {
|
|
134
|
+
return handlers;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handler result with status code
|
|
139
|
+
* 응답에 상태 코드를 명시적으로 지정
|
|
140
|
+
*/
|
|
141
|
+
export interface HandlerResult<T = unknown> {
|
|
142
|
+
status: number;
|
|
143
|
+
data: T;
|
|
144
|
+
headers?: Record<string, string>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create a typed response with status code
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* const handler = defineHandler(contract, {
|
|
153
|
+
* POST: async (ctx) => {
|
|
154
|
+
* const user = await createUser(ctx.body);
|
|
155
|
+
* return response(201, { data: user });
|
|
156
|
+
* },
|
|
157
|
+
* });
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function response<T>(
|
|
161
|
+
status: number,
|
|
162
|
+
data: T,
|
|
163
|
+
headers?: Record<string, string>
|
|
164
|
+
): HandlerResult<T> {
|
|
165
|
+
return { status, data, headers };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Type guard for HandlerResult
|
|
170
|
+
*/
|
|
171
|
+
export function isHandlerResult(value: unknown): value is HandlerResult {
|
|
172
|
+
return (
|
|
173
|
+
typeof value === "object" &&
|
|
174
|
+
value !== null &&
|
|
175
|
+
"status" in value &&
|
|
176
|
+
"data" in value
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Extract method-specific handler type from contract
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* type GetHandler = ExtractHandler<typeof userContract, "GET">;
|
|
186
|
+
* // (ctx: { query: { page: number }, ... }) => Promise<{ data: User[] }>
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export type ExtractHandler<
|
|
190
|
+
T extends ContractSchema,
|
|
191
|
+
M extends ContractMethod,
|
|
192
|
+
> = M extends keyof T["request"]
|
|
193
|
+
? HandlerFn<
|
|
194
|
+
InferContextFromMethod<
|
|
195
|
+
T["request"][M] extends MethodRequestSchema
|
|
196
|
+
? T["request"][M]
|
|
197
|
+
: undefined
|
|
198
|
+
>,
|
|
199
|
+
InferResponseUnion<T["response"]>
|
|
200
|
+
>
|
|
201
|
+
: never;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Utility to create a handler context from raw request
|
|
205
|
+
* 런타임에서 Request → TypedContext 변환
|
|
206
|
+
*/
|
|
207
|
+
export async function createContext<
|
|
208
|
+
TQuery = unknown,
|
|
209
|
+
TBody = unknown,
|
|
210
|
+
TParams = unknown,
|
|
211
|
+
THeaders = unknown,
|
|
212
|
+
>(
|
|
213
|
+
request: Request,
|
|
214
|
+
path: string,
|
|
215
|
+
method: ContractMethod,
|
|
216
|
+
parsedData: {
|
|
217
|
+
query?: TQuery;
|
|
218
|
+
body?: TBody;
|
|
219
|
+
params?: TParams;
|
|
220
|
+
headers?: THeaders;
|
|
221
|
+
} = {}
|
|
222
|
+
): Promise<TypedContext<TQuery, TBody, TParams, THeaders>> {
|
|
223
|
+
return {
|
|
224
|
+
query: parsedData.query as TQuery,
|
|
225
|
+
body: parsedData.body as TBody,
|
|
226
|
+
params: parsedData.params as TParams,
|
|
227
|
+
headers: parsedData.headers as THeaders,
|
|
228
|
+
request,
|
|
229
|
+
path,
|
|
230
|
+
method,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Combined contract + handler definition
|
|
236
|
+
* Contract와 Handler를 한 번에 정의
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* export default Mandu.route({
|
|
241
|
+
* contract: {
|
|
242
|
+
* request: {
|
|
243
|
+
* GET: { query: z.object({ id: z.string() }) },
|
|
244
|
+
* },
|
|
245
|
+
* response: {
|
|
246
|
+
* 200: z.object({ user: UserSchema }),
|
|
247
|
+
* },
|
|
248
|
+
* },
|
|
249
|
+
* handler: {
|
|
250
|
+
* GET: async (ctx) => {
|
|
251
|
+
* const user = await db.users.findUnique({ where: { id: ctx.query.id } });
|
|
252
|
+
* return { user };
|
|
253
|
+
* },
|
|
254
|
+
* },
|
|
255
|
+
* });
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
export interface RouteDefinition<T extends ContractSchema> {
|
|
259
|
+
contract: T;
|
|
260
|
+
handler: ContractHandlers<T>;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Define a complete route with contract and handler
|
|
265
|
+
*/
|
|
266
|
+
export function defineRoute<T extends ContractSchema>(
|
|
267
|
+
definition: RouteDefinition<T>
|
|
268
|
+
): RouteDefinition<T> {
|
|
269
|
+
return definition;
|
|
270
|
+
}
|