@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.
@@ -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;
@@ -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
+ });
@@ -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"]];
@@ -293,7 +293,11 @@ export class ManduFilling<TLoaderData = unknown> {
293
293
  }
294
294
  }
295
295
 
296
- export const Mandu = {
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
  },
@@ -4,7 +4,7 @@
4
4
 
5
5
  export { ManduContext, ValidationError, CookieManager } from "./context";
6
6
  export type { CookieOptions } from "./context";
7
- export { ManduFilling, Mandu, LoaderTimeoutError } from "./filling";
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;