@mandujs/core 0.9.2 → 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.
@@ -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
+ }
@@ -1,13 +1,23 @@
1
1
  /**
2
2
  * Mandu Contract Module
3
3
  * Contract-first API 정의 시스템
4
+ *
5
+ * Elysia DNA 패턴 채택:
6
+ * - Contract → Handler 타입 자동 추론
7
+ * - TypedContext로 요청 데이터 접근
8
+ * - z.object({...}) 스키마 기반 검증
4
9
  */
5
10
 
6
11
  export * from "./schema";
7
12
  export * from "./types";
8
13
  export * from "./validator";
14
+ export * from "./handler";
15
+ export * from "./client";
9
16
 
10
- import type { ContractDefinition, ContractInstance } from "./schema";
17
+ import type { ContractDefinition, ContractInstance, ContractSchema } from "./schema";
18
+ import type { ContractHandlers, RouteDefinition } from "./handler";
19
+ import { defineHandler, defineRoute } from "./handler";
20
+ import { createClient, contractFetch, type ClientOptions } from "./client";
11
21
 
12
22
  /**
13
23
  * Create a Mandu API Contract
@@ -61,3 +71,129 @@ export function createContract<T extends ContractDefinition>(definition: T): T &
61
71
  _validated: false,
62
72
  };
63
73
  }
74
+
75
+ /**
76
+ * Mandu Namespace
77
+ *
78
+ * Contract-first API 개발을 위한 메인 인터페이스
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * import { Mandu } from "@mandujs/core";
83
+ * import { z } from "zod";
84
+ *
85
+ * // 1. Contract 정의
86
+ * const userContract = Mandu.contract({
87
+ * request: {
88
+ * GET: { query: z.object({ id: z.string() }) },
89
+ * POST: { body: z.object({ name: z.string(), email: z.string().email() }) },
90
+ * },
91
+ * response: {
92
+ * 200: z.object({ user: z.object({ id: z.string(), name: z.string() }) }),
93
+ * 201: z.object({ user: z.object({ id: z.string(), name: z.string() }) }),
94
+ * },
95
+ * });
96
+ *
97
+ * // 2. Handler 정의 (타입 자동 추론)
98
+ * const handlers = Mandu.handler(userContract, {
99
+ * GET: async (ctx) => {
100
+ * // ctx.query.id는 string 타입으로 자동 추론
101
+ * const user = await db.users.findUnique({ where: { id: ctx.query.id } });
102
+ * return { user };
103
+ * },
104
+ * POST: async (ctx) => {
105
+ * // ctx.body.name, ctx.body.email 자동 추론
106
+ * const user = await db.users.create({ data: ctx.body });
107
+ * return { user };
108
+ * },
109
+ * });
110
+ *
111
+ * // 3. 또는 Route로 한 번에 정의
112
+ * export default Mandu.route({
113
+ * contract: userContract,
114
+ * handler: handlers,
115
+ * });
116
+ * ```
117
+ */
118
+ /**
119
+ * Contract-specific Mandu functions
120
+ * Note: Use `ManduContract` to avoid conflict with other Mandu exports
121
+ */
122
+ export const ManduContract = {
123
+ /**
124
+ * Create a typed Contract
125
+ * Contract 스키마 정의 및 타입 추론
126
+ */
127
+ contract: createContract,
128
+
129
+ /**
130
+ * Create typed handlers for a contract
131
+ * Contract 기반 타입 안전 핸들러 정의
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const handlers = Mandu.handler(contract, {
136
+ * GET: (ctx) => {
137
+ * // ctx.query, ctx.body, ctx.params 모두 타입 추론
138
+ * return { data: ctx.query.id };
139
+ * },
140
+ * });
141
+ * ```
142
+ */
143
+ handler: defineHandler,
144
+
145
+ /**
146
+ * Define a complete route with contract and handler
147
+ * Contract와 Handler를 한 번에 정의
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * export default Mandu.route({
152
+ * contract: {
153
+ * request: { GET: { query: z.object({ id: z.string() }) } },
154
+ * response: { 200: z.object({ data: z.string() }) },
155
+ * },
156
+ * handler: {
157
+ * GET: (ctx) => ({ data: ctx.query.id }),
158
+ * },
159
+ * });
160
+ * ```
161
+ */
162
+ route: defineRoute,
163
+
164
+ /**
165
+ * Create a type-safe API client from contract
166
+ * Contract 기반 타입 안전 클라이언트 생성
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * const client = Mandu.client(userContract, {
171
+ * baseUrl: "http://localhost:3000/api/users",
172
+ * });
173
+ *
174
+ * // Type-safe API calls
175
+ * const users = await client.GET({ query: { page: 1 } });
176
+ * const newUser = await client.POST({ body: { name: "Alice" } });
177
+ * ```
178
+ */
179
+ client: createClient,
180
+
181
+ /**
182
+ * Single type-safe fetch call
183
+ * 단일 타입 안전 fetch 호출
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * const result = await Mandu.fetch(contract, "GET", "/api/users", {
188
+ * query: { page: 1 },
189
+ * });
190
+ * ```
191
+ */
192
+ fetch: contractFetch,
193
+ } as const;
194
+
195
+ /**
196
+ * Alias for backward compatibility within contract module
197
+ * 외부에서는 메인 index.ts의 Mandu를 사용하세요
198
+ */
199
+ export const Mandu = ManduContract;