@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.
- package/package.json +1 -1
- 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,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Contract Type Inference Tests
|
|
3
|
+
*
|
|
4
|
+
* 이 테스트는 타입 추론이 올바르게 동작하는지 검증합니다.
|
|
5
|
+
* - Contract → Handler 타입 추론
|
|
6
|
+
* - TypedContext 타입 추론
|
|
7
|
+
* - Response 타입 추론
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from "bun:test";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { Mandu, type InferContract, type InferQuery, type InferBody, type InferResponse } from "./index";
|
|
13
|
+
|
|
14
|
+
// === Test Schemas ===
|
|
15
|
+
const UserSchema = z.object({
|
|
16
|
+
id: z.string().uuid(),
|
|
17
|
+
email: z.string().email(),
|
|
18
|
+
name: z.string().min(2),
|
|
19
|
+
createdAt: z.string().datetime(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
|
|
23
|
+
|
|
24
|
+
const PaginationSchema = z.object({
|
|
25
|
+
page: z.coerce.number().default(1),
|
|
26
|
+
limit: z.coerce.number().default(10),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// === Test Contract ===
|
|
30
|
+
const userContract = Mandu.contract({
|
|
31
|
+
description: "User Management API",
|
|
32
|
+
tags: ["users"],
|
|
33
|
+
request: {
|
|
34
|
+
GET: {
|
|
35
|
+
query: PaginationSchema,
|
|
36
|
+
},
|
|
37
|
+
POST: {
|
|
38
|
+
body: CreateUserSchema,
|
|
39
|
+
},
|
|
40
|
+
PUT: {
|
|
41
|
+
params: z.object({ id: z.string().uuid() }),
|
|
42
|
+
body: CreateUserSchema.partial(),
|
|
43
|
+
},
|
|
44
|
+
DELETE: {
|
|
45
|
+
params: z.object({ id: z.string().uuid() }),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
response: {
|
|
49
|
+
200: z.object({
|
|
50
|
+
data: z.array(UserSchema),
|
|
51
|
+
total: z.number(),
|
|
52
|
+
}),
|
|
53
|
+
201: z.object({
|
|
54
|
+
data: UserSchema,
|
|
55
|
+
}),
|
|
56
|
+
204: z.undefined(),
|
|
57
|
+
400: z.object({
|
|
58
|
+
error: z.string(),
|
|
59
|
+
details: z.array(z.string()).optional(),
|
|
60
|
+
}),
|
|
61
|
+
404: z.object({
|
|
62
|
+
error: z.string(),
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// === Type Inference Tests ===
|
|
68
|
+
describe("Contract Type Inference", () => {
|
|
69
|
+
it("should infer contract types correctly", () => {
|
|
70
|
+
// Type-level test: InferContract
|
|
71
|
+
type ContractTypes = InferContract<typeof userContract>;
|
|
72
|
+
|
|
73
|
+
// These are compile-time checks - if they compile, types are correct
|
|
74
|
+
type _GetQuery = ContractTypes["request"]["GET"]["query"];
|
|
75
|
+
type _PostBody = ContractTypes["request"]["POST"]["body"];
|
|
76
|
+
|
|
77
|
+
// Runtime check
|
|
78
|
+
expect(userContract.description).toBe("User Management API");
|
|
79
|
+
expect(userContract.tags).toEqual(["users"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should infer query types for specific methods", () => {
|
|
83
|
+
// Type-level test: InferQuery
|
|
84
|
+
type GetQuery = InferQuery<typeof userContract, "GET">;
|
|
85
|
+
|
|
86
|
+
// Verify at runtime that schema exists
|
|
87
|
+
expect(userContract.request.GET?.query).toBeDefined();
|
|
88
|
+
|
|
89
|
+
// Test schema validation
|
|
90
|
+
const querySchema = userContract.request.GET?.query;
|
|
91
|
+
if (querySchema) {
|
|
92
|
+
const result = querySchema.safeParse({ page: "2", limit: "20" });
|
|
93
|
+
expect(result.success).toBe(true);
|
|
94
|
+
if (result.success) {
|
|
95
|
+
expect(result.data.page).toBe(2);
|
|
96
|
+
expect(result.data.limit).toBe(20);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should infer body types for specific methods", () => {
|
|
102
|
+
// Type-level test: InferBody
|
|
103
|
+
type PostBody = InferBody<typeof userContract, "POST">;
|
|
104
|
+
|
|
105
|
+
// Verify at runtime that schema exists
|
|
106
|
+
expect(userContract.request.POST?.body).toBeDefined();
|
|
107
|
+
|
|
108
|
+
// Test schema validation
|
|
109
|
+
const bodySchema = userContract.request.POST?.body;
|
|
110
|
+
if (bodySchema) {
|
|
111
|
+
const validData = {
|
|
112
|
+
email: "test@example.com",
|
|
113
|
+
name: "Test User",
|
|
114
|
+
};
|
|
115
|
+
const result = bodySchema.safeParse(validData);
|
|
116
|
+
expect(result.success).toBe(true);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should infer response types for specific status codes", () => {
|
|
121
|
+
// Type-level test: InferResponse
|
|
122
|
+
type Success200 = InferResponse<typeof userContract, 200>;
|
|
123
|
+
type Created201 = InferResponse<typeof userContract, 201>;
|
|
124
|
+
type Error400 = InferResponse<typeof userContract, 400>;
|
|
125
|
+
|
|
126
|
+
// Verify at runtime that schemas exist
|
|
127
|
+
expect(userContract.response[200]).toBeDefined();
|
|
128
|
+
expect(userContract.response[201]).toBeDefined();
|
|
129
|
+
expect(userContract.response[400]).toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// === Handler Type Inference Tests ===
|
|
134
|
+
describe("Handler Type Inference", () => {
|
|
135
|
+
it("should create typed handlers with correct context types", () => {
|
|
136
|
+
const handlers = Mandu.handler(userContract, {
|
|
137
|
+
GET: async (ctx) => {
|
|
138
|
+
// ctx.query should be typed as { page: number, limit: number }
|
|
139
|
+
const { page, limit } = ctx.query;
|
|
140
|
+
expect(typeof page).toBe("undefined"); // Not parsed yet at this level
|
|
141
|
+
|
|
142
|
+
// Return type should match response schema
|
|
143
|
+
return {
|
|
144
|
+
data: [],
|
|
145
|
+
total: 0,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
POST: async (ctx) => {
|
|
149
|
+
// ctx.body should be typed as { email: string, name: string }
|
|
150
|
+
// Return type should match 201 response schema
|
|
151
|
+
return {
|
|
152
|
+
data: {
|
|
153
|
+
id: "123e4567-e89b-12d3-a456-426614174000",
|
|
154
|
+
email: "test@example.com",
|
|
155
|
+
name: "Test",
|
|
156
|
+
createdAt: new Date().toISOString(),
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(handlers.GET).toBeDefined();
|
|
163
|
+
expect(handlers.POST).toBeDefined();
|
|
164
|
+
expect(typeof handlers.GET).toBe("function");
|
|
165
|
+
expect(typeof handlers.POST).toBe("function");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should allow partial handler implementation", () => {
|
|
169
|
+
// Only implement some methods
|
|
170
|
+
const partialHandlers = Mandu.handler(userContract, {
|
|
171
|
+
GET: (ctx) => ({
|
|
172
|
+
data: [],
|
|
173
|
+
total: 0,
|
|
174
|
+
}),
|
|
175
|
+
// POST, PUT, DELETE not implemented - should be allowed
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(partialHandlers.GET).toBeDefined();
|
|
179
|
+
expect(partialHandlers.POST).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// === Route Definition Tests ===
|
|
184
|
+
describe("Route Definition", () => {
|
|
185
|
+
it("should define route with contract and handler", () => {
|
|
186
|
+
const route = Mandu.route({
|
|
187
|
+
contract: userContract,
|
|
188
|
+
handler: {
|
|
189
|
+
GET: (ctx) => ({
|
|
190
|
+
data: [],
|
|
191
|
+
total: 0,
|
|
192
|
+
}),
|
|
193
|
+
POST: (ctx) => ({
|
|
194
|
+
data: {
|
|
195
|
+
id: "123e4567-e89b-12d3-a456-426614174000",
|
|
196
|
+
email: ctx.body.email,
|
|
197
|
+
name: ctx.body.name,
|
|
198
|
+
createdAt: new Date().toISOString(),
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(route.contract).toBe(userContract);
|
|
205
|
+
expect(route.handler.GET).toBeDefined();
|
|
206
|
+
expect(route.handler.POST).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should preserve contract metadata in route", () => {
|
|
210
|
+
const route = Mandu.route({
|
|
211
|
+
contract: {
|
|
212
|
+
description: "Test Route",
|
|
213
|
+
tags: ["test"],
|
|
214
|
+
request: {
|
|
215
|
+
GET: { query: z.object({ id: z.string() }) },
|
|
216
|
+
},
|
|
217
|
+
response: {
|
|
218
|
+
200: z.object({ result: z.string() }),
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
handler: {
|
|
222
|
+
GET: (ctx) => ({ result: ctx.query.id }),
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(route.contract.description).toBe("Test Route");
|
|
227
|
+
expect(route.contract.tags).toEqual(["test"]);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// === Integration with Validator Tests ===
|
|
232
|
+
describe("Contract + Validator Integration", () => {
|
|
233
|
+
it("should validate request against contract schema", async () => {
|
|
234
|
+
const { ContractValidator } = await import("./validator");
|
|
235
|
+
|
|
236
|
+
const validator = new ContractValidator(userContract);
|
|
237
|
+
|
|
238
|
+
// Create a mock request
|
|
239
|
+
const request = new Request("http://localhost/users?page=1&limit=10", {
|
|
240
|
+
method: "GET",
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const result = await validator.validateRequest(request, "GET");
|
|
244
|
+
expect(result.success).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should reject invalid request", async () => {
|
|
248
|
+
const { ContractValidator } = await import("./validator");
|
|
249
|
+
|
|
250
|
+
const validator = new ContractValidator(userContract);
|
|
251
|
+
|
|
252
|
+
// Create request with invalid body
|
|
253
|
+
const request = new Request("http://localhost/users", {
|
|
254
|
+
method: "POST",
|
|
255
|
+
headers: { "Content-Type": "application/json" },
|
|
256
|
+
body: JSON.stringify({ email: "invalid-email" }), // Missing name, invalid email
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const result = await validator.validateRequest(request, "POST");
|
|
260
|
+
expect(result.success).toBe(false);
|
|
261
|
+
expect(result.errors).toBeDefined();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// === Complex Type Scenarios ===
|
|
266
|
+
describe("Complex Type Scenarios", () => {
|
|
267
|
+
it("should handle nested schemas", () => {
|
|
268
|
+
const nestedContract = Mandu.contract({
|
|
269
|
+
request: {
|
|
270
|
+
POST: {
|
|
271
|
+
body: z.object({
|
|
272
|
+
user: z.object({
|
|
273
|
+
profile: z.object({
|
|
274
|
+
bio: z.string(),
|
|
275
|
+
avatar: z.string().url().optional(),
|
|
276
|
+
}),
|
|
277
|
+
}),
|
|
278
|
+
}),
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
response: {
|
|
282
|
+
200: z.object({ success: z.boolean() }),
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const handlers = Mandu.handler(nestedContract, {
|
|
287
|
+
POST: (ctx) => {
|
|
288
|
+
// Deep nested access should be typed
|
|
289
|
+
const bio = ctx.body.user.profile.bio;
|
|
290
|
+
return { success: true };
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(handlers.POST).toBeDefined();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should handle union types in response", () => {
|
|
298
|
+
const unionContract = Mandu.contract({
|
|
299
|
+
request: {
|
|
300
|
+
GET: { query: z.object({ id: z.string() }) },
|
|
301
|
+
},
|
|
302
|
+
response: {
|
|
303
|
+
200: z.object({ found: z.literal(true), data: z.string() }),
|
|
304
|
+
404: z.object({ found: z.literal(false), message: z.string() }),
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const handlers = Mandu.handler(unionContract, {
|
|
309
|
+
GET: (ctx) => {
|
|
310
|
+
// Can return either 200 or 404 response shape
|
|
311
|
+
if (ctx.query.id === "not-found") {
|
|
312
|
+
return { found: false as const, message: "Not found" };
|
|
313
|
+
}
|
|
314
|
+
return { found: true as const, data: "Found!" };
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(handlers.GET).toBeDefined();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("should handle optional fields correctly", () => {
|
|
322
|
+
const optionalContract = Mandu.contract({
|
|
323
|
+
request: {
|
|
324
|
+
POST: {
|
|
325
|
+
body: z.object({
|
|
326
|
+
required: z.string(),
|
|
327
|
+
optional: z.string().optional(),
|
|
328
|
+
withDefault: z.string().default("default"),
|
|
329
|
+
}),
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
response: {
|
|
333
|
+
200: z.object({ result: z.string() }),
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const handlers = Mandu.handler(optionalContract, {
|
|
338
|
+
POST: (ctx) => {
|
|
339
|
+
const { required, optional, withDefault } = ctx.body;
|
|
340
|
+
return { result: required + (optional ?? "") + withDefault };
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
expect(handlers.POST).toBeDefined();
|
|
345
|
+
});
|
|
346
|
+
});
|
package/src/contract/types.ts
CHANGED
|
@@ -132,3 +132,86 @@ export type ContractMethods<T extends ContractSchema> = Extract<
|
|
|
132
132
|
* Helper type to get all defined status codes in a contract
|
|
133
133
|
*/
|
|
134
134
|
export type ContractStatusCodes<T extends ContractSchema> = keyof T["response"];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Helper type to check if a method exists in contract
|
|
138
|
+
*/
|
|
139
|
+
export type HasMethod<
|
|
140
|
+
T extends ContractSchema,
|
|
141
|
+
M extends string
|
|
142
|
+
> = M extends keyof T["request"] ? true : false;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Extract all required fields from a method schema
|
|
146
|
+
*/
|
|
147
|
+
export type RequiredFields<
|
|
148
|
+
T extends ContractSchema,
|
|
149
|
+
M extends keyof T["request"]
|
|
150
|
+
> = T["request"][M] extends MethodRequestSchema
|
|
151
|
+
? {
|
|
152
|
+
query: T["request"][M]["query"] extends z.ZodTypeAny ? true : false;
|
|
153
|
+
body: T["request"][M]["body"] extends z.ZodTypeAny ? true : false;
|
|
154
|
+
params: T["request"][M]["params"] extends z.ZodTypeAny ? true : false;
|
|
155
|
+
headers: T["request"][M]["headers"] extends z.ZodTypeAny ? true : false;
|
|
156
|
+
}
|
|
157
|
+
: never;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get the success response type (200 or 201)
|
|
161
|
+
*/
|
|
162
|
+
export type SuccessResponse<T extends ContractSchema> = T["response"][200] extends z.ZodTypeAny
|
|
163
|
+
? z.infer<T["response"][200]>
|
|
164
|
+
: T["response"][201] extends z.ZodTypeAny
|
|
165
|
+
? z.infer<T["response"][201]>
|
|
166
|
+
: never;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get the error response type (400, 404, 500, etc.)
|
|
170
|
+
*/
|
|
171
|
+
export type ErrorResponse<T extends ContractSchema> =
|
|
172
|
+
| (T["response"][400] extends z.ZodTypeAny ? z.infer<T["response"][400]> : never)
|
|
173
|
+
| (T["response"][401] extends z.ZodTypeAny ? z.infer<T["response"][401]> : never)
|
|
174
|
+
| (T["response"][403] extends z.ZodTypeAny ? z.infer<T["response"][403]> : never)
|
|
175
|
+
| (T["response"][404] extends z.ZodTypeAny ? z.infer<T["response"][404]> : never)
|
|
176
|
+
| (T["response"][500] extends z.ZodTypeAny ? z.infer<T["response"][500]> : never);
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Utility type for strict contract enforcement
|
|
180
|
+
* Contract에 정의된 메서드만 허용
|
|
181
|
+
*/
|
|
182
|
+
export type StrictContractMethods<T extends ContractSchema> = {
|
|
183
|
+
[M in ContractMethods<T>]: true;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Type-safe fetch options derived from contract
|
|
188
|
+
* 클라이언트에서 Contract 기반 fetch 호출에 사용
|
|
189
|
+
*/
|
|
190
|
+
export type ContractFetchOptions<
|
|
191
|
+
T extends ContractSchema,
|
|
192
|
+
M extends keyof T["request"]
|
|
193
|
+
> = T["request"][M] extends MethodRequestSchema
|
|
194
|
+
? {
|
|
195
|
+
query?: T["request"][M]["query"] extends z.ZodTypeAny
|
|
196
|
+
? z.input<T["request"][M]["query"]>
|
|
197
|
+
: never;
|
|
198
|
+
body?: T["request"][M]["body"] extends z.ZodTypeAny
|
|
199
|
+
? z.input<T["request"][M]["body"]>
|
|
200
|
+
: never;
|
|
201
|
+
params?: T["request"][M]["params"] extends z.ZodTypeAny
|
|
202
|
+
? z.input<T["request"][M]["params"]>
|
|
203
|
+
: never;
|
|
204
|
+
headers?: T["request"][M]["headers"] extends z.ZodTypeAny
|
|
205
|
+
? z.input<T["request"][M]["headers"]>
|
|
206
|
+
: never;
|
|
207
|
+
}
|
|
208
|
+
: never;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Response type union for a contract
|
|
212
|
+
*/
|
|
213
|
+
export type ContractResponseUnion<T extends ContractSchema> = {
|
|
214
|
+
[K in keyof T["response"]]: T["response"][K] extends z.ZodTypeAny
|
|
215
|
+
? { status: K; data: z.infer<T["response"][K]> }
|
|
216
|
+
: never;
|
|
217
|
+
}[keyof T["response"]];
|
package/src/filling/filling.ts
CHANGED
|
@@ -293,7 +293,11 @@ export class ManduFilling<TLoaderData = unknown> {
|
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Mandu Filling factory functions
|
|
298
|
+
* Note: These are also available via the main `Mandu` namespace
|
|
299
|
+
*/
|
|
300
|
+
export const ManduFillingFactory = {
|
|
297
301
|
filling<TLoaderData = unknown>(): ManduFilling<TLoaderData> {
|
|
298
302
|
return new ManduFilling<TLoaderData>();
|
|
299
303
|
},
|
package/src/filling/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
export { ManduContext, ValidationError, CookieManager } from "./context";
|
|
6
6
|
export type { CookieOptions } from "./context";
|
|
7
|
-
export { ManduFilling,
|
|
7
|
+
export { ManduFilling, ManduFillingFactory, LoaderTimeoutError } from "./filling";
|
|
8
8
|
export type { Handler, Guard, HttpMethod, Loader, LoaderOptions } from "./filling";
|
|
9
9
|
|
|
10
10
|
// Auth Guards
|
package/src/index.ts
CHANGED
|
@@ -12,3 +12,78 @@ export * from "./contract";
|
|
|
12
12
|
export * from "./openapi";
|
|
13
13
|
export * from "./brain";
|
|
14
14
|
export * from "./watcher";
|
|
15
|
+
|
|
16
|
+
// Consolidated Mandu namespace
|
|
17
|
+
import { ManduFilling, ManduContext, ManduFillingFactory } from "./filling";
|
|
18
|
+
import { createContract, defineHandler, defineRoute, createClient, contractFetch } from "./contract";
|
|
19
|
+
import type { ContractDefinition, ContractInstance, ContractSchema } from "./contract";
|
|
20
|
+
import type { ContractHandlers, ClientOptions } from "./contract";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mandu - Unified Namespace
|
|
24
|
+
*
|
|
25
|
+
* 통합된 Mandu API 인터페이스
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import { Mandu } from "@mandujs/core";
|
|
30
|
+
* import { z } from "zod";
|
|
31
|
+
*
|
|
32
|
+
* // Filling (Handler) API
|
|
33
|
+
* export default Mandu.filling()
|
|
34
|
+
* .get(async (ctx) => ctx.json({ message: "Hello" }));
|
|
35
|
+
*
|
|
36
|
+
* // Contract API
|
|
37
|
+
* const contract = Mandu.contract({
|
|
38
|
+
* request: { GET: { query: z.object({ id: z.string() }) } },
|
|
39
|
+
* response: { 200: z.object({ data: z.string() }) },
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* // Handler API (with type inference)
|
|
43
|
+
* const handlers = Mandu.handler(contract, {
|
|
44
|
+
* GET: (ctx) => ({ data: ctx.query.id }),
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* // Client API (type-safe fetch)
|
|
48
|
+
* const client = Mandu.client(contract, { baseUrl: "/api" });
|
|
49
|
+
* const result = await client.GET({ query: { id: "123" } });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export const Mandu = {
|
|
53
|
+
// === Filling (Handler) API ===
|
|
54
|
+
/**
|
|
55
|
+
* Create a new filling (handler chain)
|
|
56
|
+
*/
|
|
57
|
+
filling: ManduFillingFactory.filling,
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a ManduContext from a Request
|
|
61
|
+
*/
|
|
62
|
+
context: ManduFillingFactory.context,
|
|
63
|
+
|
|
64
|
+
// === Contract API ===
|
|
65
|
+
/**
|
|
66
|
+
* Define a typed API contract
|
|
67
|
+
*/
|
|
68
|
+
contract: createContract,
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create typed handlers for a contract
|
|
72
|
+
*/
|
|
73
|
+
handler: defineHandler,
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Define a complete route (contract + handler)
|
|
77
|
+
*/
|
|
78
|
+
route: defineRoute,
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a type-safe API client
|
|
82
|
+
*/
|
|
83
|
+
client: createClient,
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Make a type-safe fetch call
|
|
87
|
+
*/
|
|
88
|
+
fetch: contractFetch,
|
|
89
|
+
} as const;
|