@mandujs/core 0.8.1 → 0.8.2
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/README.ko.md +200 -200
- package/README.md +200 -200
- package/package.json +41 -41
- package/src/bundler/build.ts +30 -2
- package/src/bundler/dev.ts +98 -52
- package/src/client/Link.tsx +209 -209
- package/src/client/hooks.ts +267 -267
- package/src/client/router.ts +387 -387
- package/src/client/serialize.ts +404 -404
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +438 -438
- package/src/filling/filling.ts +306 -306
- package/src/filling/index.ts +21 -21
- package/src/generator/index.ts +3 -3
- package/src/report/index.ts +1 -1
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/index.ts +3 -3
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/ssr.ts +321 -321
- package/src/runtime/trace.ts +144 -144
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
package/src/filling/filling.ts
CHANGED
|
@@ -1,306 +1,306 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Filling - 만두소 🥟
|
|
3
|
-
* 체이닝 API로 비즈니스 로직 정의
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { ManduContext, ValidationError } from "./context";
|
|
7
|
-
import { AuthenticationError, AuthorizationError } from "./auth";
|
|
8
|
-
import { ErrorClassifier, formatErrorResponse, ErrorCode } from "../error";
|
|
9
|
-
import { createContract, type ContractDefinition, type ContractInstance } from "../contract";
|
|
10
|
-
import {
|
|
11
|
-
type Middleware as RuntimeMiddleware,
|
|
12
|
-
type MiddlewareEntry,
|
|
13
|
-
compose,
|
|
14
|
-
} from "../runtime/compose";
|
|
15
|
-
import {
|
|
16
|
-
type LifecycleStore,
|
|
17
|
-
type OnRequestHandler,
|
|
18
|
-
type OnParseHandler,
|
|
19
|
-
type BeforeHandleHandler,
|
|
20
|
-
type AfterHandleHandler,
|
|
21
|
-
type MapResponseHandler,
|
|
22
|
-
type OnErrorHandler,
|
|
23
|
-
type AfterResponseHandler,
|
|
24
|
-
createLifecycleStore,
|
|
25
|
-
executeLifecycle,
|
|
26
|
-
type ExecuteOptions,
|
|
27
|
-
} from "../runtime/lifecycle";
|
|
28
|
-
|
|
29
|
-
/** Handler function type */
|
|
30
|
-
export type Handler = (ctx: ManduContext) => Response | Promise<Response>;
|
|
31
|
-
|
|
32
|
-
/** Guard function type (alias of BeforeHandle) */
|
|
33
|
-
export type Guard = BeforeHandleHandler;
|
|
34
|
-
|
|
35
|
-
/** HTTP methods */
|
|
36
|
-
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
37
|
-
|
|
38
|
-
/** Loader function type - SSR 데이터 로딩 */
|
|
39
|
-
export type Loader<T = unknown> = (ctx: ManduContext) => T | Promise<T>;
|
|
40
|
-
|
|
41
|
-
/** Loader 실행 옵션 */
|
|
42
|
-
export interface LoaderOptions<T = unknown> {
|
|
43
|
-
/** 타임아웃 (ms), 기본값 5000 */
|
|
44
|
-
timeout?: number;
|
|
45
|
-
/** 타임아웃 또는 에러 시 반환할 fallback 데이터 */
|
|
46
|
-
fallback?: T;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Loader 타임아웃 에러 */
|
|
50
|
-
export class LoaderTimeoutError extends Error {
|
|
51
|
-
constructor(timeout: number) {
|
|
52
|
-
super(`Loader timed out after ${timeout}ms`);
|
|
53
|
-
this.name = "LoaderTimeoutError";
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface FillingConfig<TLoaderData = unknown> {
|
|
58
|
-
handlers: Map<HttpMethod, Handler>;
|
|
59
|
-
loader?: Loader<TLoaderData>;
|
|
60
|
-
lifecycle: LifecycleStore;
|
|
61
|
-
middleware: MiddlewareEntry[];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export class ManduFilling<TLoaderData = unknown> {
|
|
65
|
-
private config: FillingConfig<TLoaderData> = {
|
|
66
|
-
handlers: new Map(),
|
|
67
|
-
lifecycle: createLifecycleStore(),
|
|
68
|
-
middleware: [],
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
loader(loaderFn: Loader<TLoaderData>): this {
|
|
72
|
-
this.config.loader = loaderFn;
|
|
73
|
-
return this;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async executeLoader(
|
|
77
|
-
ctx: ManduContext,
|
|
78
|
-
options: LoaderOptions<TLoaderData> = {}
|
|
79
|
-
): Promise<TLoaderData | undefined> {
|
|
80
|
-
if (!this.config.loader) {
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
const { timeout = 5000, fallback } = options;
|
|
84
|
-
try {
|
|
85
|
-
const loaderPromise = Promise.resolve(this.config.loader(ctx));
|
|
86
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
87
|
-
setTimeout(() => reject(new LoaderTimeoutError(timeout)), timeout);
|
|
88
|
-
});
|
|
89
|
-
return await Promise.race([loaderPromise, timeoutPromise]);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
if (fallback !== undefined) {
|
|
92
|
-
console.warn(`[Mandu] Loader failed, using fallback:`, error instanceof Error ? error.message : String(error));
|
|
93
|
-
return fallback;
|
|
94
|
-
}
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
hasLoader(): boolean {
|
|
100
|
-
return !!this.config.loader;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
get(handler: Handler): this {
|
|
104
|
-
this.config.handlers.set("GET", handler);
|
|
105
|
-
return this;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
post(handler: Handler): this {
|
|
109
|
-
this.config.handlers.set("POST", handler);
|
|
110
|
-
return this;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
put(handler: Handler): this {
|
|
114
|
-
this.config.handlers.set("PUT", handler);
|
|
115
|
-
return this;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
patch(handler: Handler): this {
|
|
119
|
-
this.config.handlers.set("PATCH", handler);
|
|
120
|
-
return this;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
delete(handler: Handler): this {
|
|
124
|
-
this.config.handlers.set("DELETE", handler);
|
|
125
|
-
return this;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
head(handler: Handler): this {
|
|
129
|
-
this.config.handlers.set("HEAD", handler);
|
|
130
|
-
return this;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
options(handler: Handler): this {
|
|
134
|
-
this.config.handlers.set("OPTIONS", handler);
|
|
135
|
-
return this;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
all(handler: Handler): this {
|
|
139
|
-
const methods: HttpMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
140
|
-
methods.forEach((method) => this.config.handlers.set(method, handler));
|
|
141
|
-
return this;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 요청 시작 훅
|
|
146
|
-
*/
|
|
147
|
-
onRequest(fn: OnRequestHandler): this {
|
|
148
|
-
this.config.lifecycle.onRequest.push({ fn, scope: "local" });
|
|
149
|
-
return this;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Compose-style middleware (Hono/Koa 스타일)
|
|
154
|
-
* lifecycle의 handler 단계에서 실행됨
|
|
155
|
-
*/
|
|
156
|
-
middleware(fn: RuntimeMiddleware, name?: string): this {
|
|
157
|
-
this.config.middleware.push({
|
|
158
|
-
fn,
|
|
159
|
-
name: name || fn.name || `middleware_${this.config.middleware.length}`,
|
|
160
|
-
isAsync: fn.constructor.name === "AsyncFunction",
|
|
161
|
-
});
|
|
162
|
-
return this;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* 바디 파싱 훅
|
|
167
|
-
* body를 읽을 때는 req.clone() 사용 권장
|
|
168
|
-
*/
|
|
169
|
-
onParse(fn: OnParseHandler): this {
|
|
170
|
-
this.config.lifecycle.onParse.push({ fn, scope: "local" });
|
|
171
|
-
return this;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
beforeHandle(fn: BeforeHandleHandler): this {
|
|
175
|
-
this.config.lifecycle.beforeHandle.push({ fn, scope: "local" });
|
|
176
|
-
return this;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Guard alias (beforeHandle와 동일)
|
|
181
|
-
* 인증/인가, 요청 차단 등에 사용
|
|
182
|
-
*/
|
|
183
|
-
guard(fn: Guard): this {
|
|
184
|
-
return this.beforeHandle(fn);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Middleware alias (guard와 동일)
|
|
189
|
-
*/
|
|
190
|
-
use(fn: Guard): this {
|
|
191
|
-
return this.guard(fn);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* 핸들러 후 훅
|
|
196
|
-
*/
|
|
197
|
-
afterHandle(fn: AfterHandleHandler): this {
|
|
198
|
-
this.config.lifecycle.afterHandle.push({ fn, scope: "local" });
|
|
199
|
-
return this;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* 최종 응답 매핑 훅
|
|
204
|
-
*/
|
|
205
|
-
mapResponse(fn: MapResponseHandler): this {
|
|
206
|
-
this.config.lifecycle.mapResponse.push({ fn, scope: "local" });
|
|
207
|
-
return this;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* 에러 핸들링 훅
|
|
212
|
-
*/
|
|
213
|
-
onError(fn: OnErrorHandler): this {
|
|
214
|
-
this.config.lifecycle.onError.push({ fn, scope: "local" });
|
|
215
|
-
return this;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* 응답 후 훅 (비동기)
|
|
220
|
-
*/
|
|
221
|
-
afterResponse(fn: AfterResponseHandler): this {
|
|
222
|
-
this.config.lifecycle.afterResponse.push({ fn, scope: "local" });
|
|
223
|
-
return this;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async handle(
|
|
227
|
-
request: Request,
|
|
228
|
-
params: Record<string, string> = {},
|
|
229
|
-
routeContext?: { routeId: string; pattern: string },
|
|
230
|
-
options?: ExecuteOptions
|
|
231
|
-
): Promise<Response> {
|
|
232
|
-
const ctx = new ManduContext(request, params);
|
|
233
|
-
const method = request.method.toUpperCase() as HttpMethod;
|
|
234
|
-
const handler = this.config.handlers.get(method);
|
|
235
|
-
if (!handler) {
|
|
236
|
-
return ctx.json({ status: "error", message: `Method ${method} not allowed`, allowed: Array.from(this.config.handlers.keys()) }, 405);
|
|
237
|
-
}
|
|
238
|
-
const lifecycleWithDefaults = this.createLifecycleWithDefaults(routeContext);
|
|
239
|
-
const runHandler = async () => {
|
|
240
|
-
if (this.config.middleware.length === 0) {
|
|
241
|
-
return handler(ctx);
|
|
242
|
-
}
|
|
243
|
-
const chain: MiddlewareEntry[] = [
|
|
244
|
-
...this.config.middleware,
|
|
245
|
-
{
|
|
246
|
-
fn: async (innerCtx) => handler(innerCtx),
|
|
247
|
-
name: "handler",
|
|
248
|
-
isAsync: true,
|
|
249
|
-
},
|
|
250
|
-
];
|
|
251
|
-
const composed = compose(chain);
|
|
252
|
-
return composed(ctx);
|
|
253
|
-
};
|
|
254
|
-
return executeLifecycle(lifecycleWithDefaults, ctx, runHandler, options);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
private createLifecycleWithDefaults(routeContext?: { routeId: string; pattern: string }): LifecycleStore {
|
|
258
|
-
const lifecycle: LifecycleStore = {
|
|
259
|
-
onRequest: [...this.config.lifecycle.onRequest],
|
|
260
|
-
onParse: [...this.config.lifecycle.onParse],
|
|
261
|
-
beforeHandle: [...this.config.lifecycle.beforeHandle],
|
|
262
|
-
afterHandle: [...this.config.lifecycle.afterHandle],
|
|
263
|
-
mapResponse: [...this.config.lifecycle.mapResponse],
|
|
264
|
-
afterResponse: [...this.config.lifecycle.afterResponse],
|
|
265
|
-
onError: [...this.config.lifecycle.onError],
|
|
266
|
-
};
|
|
267
|
-
const defaultErrorHandler: OnErrorHandler = (ctx, error) => {
|
|
268
|
-
if (error instanceof AuthenticationError) {
|
|
269
|
-
return ctx.json({ errorType: "AUTH_ERROR", code: "AUTHENTICATION_REQUIRED", message: error.message, summary: "인증 필요 - 로그인 후 다시 시도하세요", timestamp: new Date().toISOString() }, 401);
|
|
270
|
-
}
|
|
271
|
-
if (error instanceof AuthorizationError) {
|
|
272
|
-
return ctx.json({ errorType: "AUTH_ERROR", code: "ACCESS_DENIED", message: error.message, summary: "권한 없음 - 접근 권한이 부족합니다", requiredRoles: error.requiredRoles, timestamp: new Date().toISOString() }, 403);
|
|
273
|
-
}
|
|
274
|
-
if (error instanceof ValidationError) {
|
|
275
|
-
return ctx.json({ errorType: "LOGIC_ERROR", code: ErrorCode.SLOT_VALIDATION_ERROR, message: "Validation failed", summary: "입력 검증 실패 - 요청 데이터 확인 필요", fix: { file: routeContext ? `spec/slots/${routeContext.routeId}.slot.ts` : "spec/slots/", suggestion: "요청 데이터가 스키마와 일치하는지 확인하세요" }, route: routeContext, errors: error.errors, timestamp: new Date().toISOString() }, 400);
|
|
276
|
-
}
|
|
277
|
-
const classifier = new ErrorClassifier(null, routeContext);
|
|
278
|
-
const manduError = classifier.classify(error);
|
|
279
|
-
console.error(`[Mandu] ${manduError.errorType}:`, manduError.message);
|
|
280
|
-
const response = formatErrorResponse(manduError, { isDev: process.env.NODE_ENV !== "production" });
|
|
281
|
-
return ctx.json(response, 500);
|
|
282
|
-
};
|
|
283
|
-
lifecycle.onError.push({ fn: defaultErrorHandler, scope: "local" });
|
|
284
|
-
return lifecycle;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
getMethods(): HttpMethod[] {
|
|
288
|
-
return Array.from(this.config.handlers.keys());
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
hasMethod(method: HttpMethod): boolean {
|
|
292
|
-
return this.config.handlers.has(method);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export const Mandu = {
|
|
297
|
-
filling<TLoaderData = unknown>(): ManduFilling<TLoaderData> {
|
|
298
|
-
return new ManduFilling<TLoaderData>();
|
|
299
|
-
},
|
|
300
|
-
contract<T extends ContractDefinition>(definition: T): T & ContractInstance {
|
|
301
|
-
return createContract(definition);
|
|
302
|
-
},
|
|
303
|
-
context(request: Request, params?: Record<string, string>): ManduContext {
|
|
304
|
-
return new ManduContext(request, params);
|
|
305
|
-
},
|
|
306
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Filling - 만두소 🥟
|
|
3
|
+
* 체이닝 API로 비즈니스 로직 정의
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ManduContext, ValidationError } from "./context";
|
|
7
|
+
import { AuthenticationError, AuthorizationError } from "./auth";
|
|
8
|
+
import { ErrorClassifier, formatErrorResponse, ErrorCode } from "../error";
|
|
9
|
+
import { createContract, type ContractDefinition, type ContractInstance } from "../contract";
|
|
10
|
+
import {
|
|
11
|
+
type Middleware as RuntimeMiddleware,
|
|
12
|
+
type MiddlewareEntry,
|
|
13
|
+
compose,
|
|
14
|
+
} from "../runtime/compose";
|
|
15
|
+
import {
|
|
16
|
+
type LifecycleStore,
|
|
17
|
+
type OnRequestHandler,
|
|
18
|
+
type OnParseHandler,
|
|
19
|
+
type BeforeHandleHandler,
|
|
20
|
+
type AfterHandleHandler,
|
|
21
|
+
type MapResponseHandler,
|
|
22
|
+
type OnErrorHandler,
|
|
23
|
+
type AfterResponseHandler,
|
|
24
|
+
createLifecycleStore,
|
|
25
|
+
executeLifecycle,
|
|
26
|
+
type ExecuteOptions,
|
|
27
|
+
} from "../runtime/lifecycle";
|
|
28
|
+
|
|
29
|
+
/** Handler function type */
|
|
30
|
+
export type Handler = (ctx: ManduContext) => Response | Promise<Response>;
|
|
31
|
+
|
|
32
|
+
/** Guard function type (alias of BeforeHandle) */
|
|
33
|
+
export type Guard = BeforeHandleHandler;
|
|
34
|
+
|
|
35
|
+
/** HTTP methods */
|
|
36
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
37
|
+
|
|
38
|
+
/** Loader function type - SSR 데이터 로딩 */
|
|
39
|
+
export type Loader<T = unknown> = (ctx: ManduContext) => T | Promise<T>;
|
|
40
|
+
|
|
41
|
+
/** Loader 실행 옵션 */
|
|
42
|
+
export interface LoaderOptions<T = unknown> {
|
|
43
|
+
/** 타임아웃 (ms), 기본값 5000 */
|
|
44
|
+
timeout?: number;
|
|
45
|
+
/** 타임아웃 또는 에러 시 반환할 fallback 데이터 */
|
|
46
|
+
fallback?: T;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Loader 타임아웃 에러 */
|
|
50
|
+
export class LoaderTimeoutError extends Error {
|
|
51
|
+
constructor(timeout: number) {
|
|
52
|
+
super(`Loader timed out after ${timeout}ms`);
|
|
53
|
+
this.name = "LoaderTimeoutError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface FillingConfig<TLoaderData = unknown> {
|
|
58
|
+
handlers: Map<HttpMethod, Handler>;
|
|
59
|
+
loader?: Loader<TLoaderData>;
|
|
60
|
+
lifecycle: LifecycleStore;
|
|
61
|
+
middleware: MiddlewareEntry[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class ManduFilling<TLoaderData = unknown> {
|
|
65
|
+
private config: FillingConfig<TLoaderData> = {
|
|
66
|
+
handlers: new Map(),
|
|
67
|
+
lifecycle: createLifecycleStore(),
|
|
68
|
+
middleware: [],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
loader(loaderFn: Loader<TLoaderData>): this {
|
|
72
|
+
this.config.loader = loaderFn;
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async executeLoader(
|
|
77
|
+
ctx: ManduContext,
|
|
78
|
+
options: LoaderOptions<TLoaderData> = {}
|
|
79
|
+
): Promise<TLoaderData | undefined> {
|
|
80
|
+
if (!this.config.loader) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const { timeout = 5000, fallback } = options;
|
|
84
|
+
try {
|
|
85
|
+
const loaderPromise = Promise.resolve(this.config.loader(ctx));
|
|
86
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
87
|
+
setTimeout(() => reject(new LoaderTimeoutError(timeout)), timeout);
|
|
88
|
+
});
|
|
89
|
+
return await Promise.race([loaderPromise, timeoutPromise]);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (fallback !== undefined) {
|
|
92
|
+
console.warn(`[Mandu] Loader failed, using fallback:`, error instanceof Error ? error.message : String(error));
|
|
93
|
+
return fallback;
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
hasLoader(): boolean {
|
|
100
|
+
return !!this.config.loader;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get(handler: Handler): this {
|
|
104
|
+
this.config.handlers.set("GET", handler);
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
post(handler: Handler): this {
|
|
109
|
+
this.config.handlers.set("POST", handler);
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
put(handler: Handler): this {
|
|
114
|
+
this.config.handlers.set("PUT", handler);
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
patch(handler: Handler): this {
|
|
119
|
+
this.config.handlers.set("PATCH", handler);
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
delete(handler: Handler): this {
|
|
124
|
+
this.config.handlers.set("DELETE", handler);
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
head(handler: Handler): this {
|
|
129
|
+
this.config.handlers.set("HEAD", handler);
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
options(handler: Handler): this {
|
|
134
|
+
this.config.handlers.set("OPTIONS", handler);
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
all(handler: Handler): this {
|
|
139
|
+
const methods: HttpMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
140
|
+
methods.forEach((method) => this.config.handlers.set(method, handler));
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 요청 시작 훅
|
|
146
|
+
*/
|
|
147
|
+
onRequest(fn: OnRequestHandler): this {
|
|
148
|
+
this.config.lifecycle.onRequest.push({ fn, scope: "local" });
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Compose-style middleware (Hono/Koa 스타일)
|
|
154
|
+
* lifecycle의 handler 단계에서 실행됨
|
|
155
|
+
*/
|
|
156
|
+
middleware(fn: RuntimeMiddleware, name?: string): this {
|
|
157
|
+
this.config.middleware.push({
|
|
158
|
+
fn,
|
|
159
|
+
name: name || fn.name || `middleware_${this.config.middleware.length}`,
|
|
160
|
+
isAsync: fn.constructor.name === "AsyncFunction",
|
|
161
|
+
});
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 바디 파싱 훅
|
|
167
|
+
* body를 읽을 때는 req.clone() 사용 권장
|
|
168
|
+
*/
|
|
169
|
+
onParse(fn: OnParseHandler): this {
|
|
170
|
+
this.config.lifecycle.onParse.push({ fn, scope: "local" });
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
beforeHandle(fn: BeforeHandleHandler): this {
|
|
175
|
+
this.config.lifecycle.beforeHandle.push({ fn, scope: "local" });
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Guard alias (beforeHandle와 동일)
|
|
181
|
+
* 인증/인가, 요청 차단 등에 사용
|
|
182
|
+
*/
|
|
183
|
+
guard(fn: Guard): this {
|
|
184
|
+
return this.beforeHandle(fn);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Middleware alias (guard와 동일)
|
|
189
|
+
*/
|
|
190
|
+
use(fn: Guard): this {
|
|
191
|
+
return this.guard(fn);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 핸들러 후 훅
|
|
196
|
+
*/
|
|
197
|
+
afterHandle(fn: AfterHandleHandler): this {
|
|
198
|
+
this.config.lifecycle.afterHandle.push({ fn, scope: "local" });
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 최종 응답 매핑 훅
|
|
204
|
+
*/
|
|
205
|
+
mapResponse(fn: MapResponseHandler): this {
|
|
206
|
+
this.config.lifecycle.mapResponse.push({ fn, scope: "local" });
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 에러 핸들링 훅
|
|
212
|
+
*/
|
|
213
|
+
onError(fn: OnErrorHandler): this {
|
|
214
|
+
this.config.lifecycle.onError.push({ fn, scope: "local" });
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 응답 후 훅 (비동기)
|
|
220
|
+
*/
|
|
221
|
+
afterResponse(fn: AfterResponseHandler): this {
|
|
222
|
+
this.config.lifecycle.afterResponse.push({ fn, scope: "local" });
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async handle(
|
|
227
|
+
request: Request,
|
|
228
|
+
params: Record<string, string> = {},
|
|
229
|
+
routeContext?: { routeId: string; pattern: string },
|
|
230
|
+
options?: ExecuteOptions
|
|
231
|
+
): Promise<Response> {
|
|
232
|
+
const ctx = new ManduContext(request, params);
|
|
233
|
+
const method = request.method.toUpperCase() as HttpMethod;
|
|
234
|
+
const handler = this.config.handlers.get(method);
|
|
235
|
+
if (!handler) {
|
|
236
|
+
return ctx.json({ status: "error", message: `Method ${method} not allowed`, allowed: Array.from(this.config.handlers.keys()) }, 405);
|
|
237
|
+
}
|
|
238
|
+
const lifecycleWithDefaults = this.createLifecycleWithDefaults(routeContext);
|
|
239
|
+
const runHandler = async () => {
|
|
240
|
+
if (this.config.middleware.length === 0) {
|
|
241
|
+
return handler(ctx);
|
|
242
|
+
}
|
|
243
|
+
const chain: MiddlewareEntry[] = [
|
|
244
|
+
...this.config.middleware,
|
|
245
|
+
{
|
|
246
|
+
fn: async (innerCtx) => handler(innerCtx),
|
|
247
|
+
name: "handler",
|
|
248
|
+
isAsync: true,
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
const composed = compose(chain);
|
|
252
|
+
return composed(ctx);
|
|
253
|
+
};
|
|
254
|
+
return executeLifecycle(lifecycleWithDefaults, ctx, runHandler, options);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private createLifecycleWithDefaults(routeContext?: { routeId: string; pattern: string }): LifecycleStore {
|
|
258
|
+
const lifecycle: LifecycleStore = {
|
|
259
|
+
onRequest: [...this.config.lifecycle.onRequest],
|
|
260
|
+
onParse: [...this.config.lifecycle.onParse],
|
|
261
|
+
beforeHandle: [...this.config.lifecycle.beforeHandle],
|
|
262
|
+
afterHandle: [...this.config.lifecycle.afterHandle],
|
|
263
|
+
mapResponse: [...this.config.lifecycle.mapResponse],
|
|
264
|
+
afterResponse: [...this.config.lifecycle.afterResponse],
|
|
265
|
+
onError: [...this.config.lifecycle.onError],
|
|
266
|
+
};
|
|
267
|
+
const defaultErrorHandler: OnErrorHandler = (ctx, error) => {
|
|
268
|
+
if (error instanceof AuthenticationError) {
|
|
269
|
+
return ctx.json({ errorType: "AUTH_ERROR", code: "AUTHENTICATION_REQUIRED", message: error.message, summary: "인증 필요 - 로그인 후 다시 시도하세요", timestamp: new Date().toISOString() }, 401);
|
|
270
|
+
}
|
|
271
|
+
if (error instanceof AuthorizationError) {
|
|
272
|
+
return ctx.json({ errorType: "AUTH_ERROR", code: "ACCESS_DENIED", message: error.message, summary: "권한 없음 - 접근 권한이 부족합니다", requiredRoles: error.requiredRoles, timestamp: new Date().toISOString() }, 403);
|
|
273
|
+
}
|
|
274
|
+
if (error instanceof ValidationError) {
|
|
275
|
+
return ctx.json({ errorType: "LOGIC_ERROR", code: ErrorCode.SLOT_VALIDATION_ERROR, message: "Validation failed", summary: "입력 검증 실패 - 요청 데이터 확인 필요", fix: { file: routeContext ? `spec/slots/${routeContext.routeId}.slot.ts` : "spec/slots/", suggestion: "요청 데이터가 스키마와 일치하는지 확인하세요" }, route: routeContext, errors: error.errors, timestamp: new Date().toISOString() }, 400);
|
|
276
|
+
}
|
|
277
|
+
const classifier = new ErrorClassifier(null, routeContext);
|
|
278
|
+
const manduError = classifier.classify(error);
|
|
279
|
+
console.error(`[Mandu] ${manduError.errorType}:`, manduError.message);
|
|
280
|
+
const response = formatErrorResponse(manduError, { isDev: process.env.NODE_ENV !== "production" });
|
|
281
|
+
return ctx.json(response, 500);
|
|
282
|
+
};
|
|
283
|
+
lifecycle.onError.push({ fn: defaultErrorHandler, scope: "local" });
|
|
284
|
+
return lifecycle;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
getMethods(): HttpMethod[] {
|
|
288
|
+
return Array.from(this.config.handlers.keys());
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
hasMethod(method: HttpMethod): boolean {
|
|
292
|
+
return this.config.handlers.has(method);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const Mandu = {
|
|
297
|
+
filling<TLoaderData = unknown>(): ManduFilling<TLoaderData> {
|
|
298
|
+
return new ManduFilling<TLoaderData>();
|
|
299
|
+
},
|
|
300
|
+
contract<T extends ContractDefinition>(definition: T): T & ContractInstance {
|
|
301
|
+
return createContract(definition);
|
|
302
|
+
},
|
|
303
|
+
context(request: Request, params?: Record<string, string>): ManduContext {
|
|
304
|
+
return new ManduContext(request, params);
|
|
305
|
+
},
|
|
306
|
+
};
|
package/src/filling/index.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Filling Module - 만두소 🥟
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export { ManduContext, ValidationError, CookieManager } from "./context";
|
|
6
|
-
export type { CookieOptions } from "./context";
|
|
7
|
-
export { ManduFilling, Mandu, LoaderTimeoutError } from "./filling";
|
|
8
|
-
export type { Handler, Guard, HttpMethod, Loader, LoaderOptions } from "./filling";
|
|
9
|
-
|
|
10
|
-
// Auth Guards
|
|
11
|
-
export {
|
|
12
|
-
AuthenticationError,
|
|
13
|
-
AuthorizationError,
|
|
14
|
-
requireUser,
|
|
15
|
-
requireRole,
|
|
16
|
-
requireAnyRole,
|
|
17
|
-
requireAllRoles,
|
|
18
|
-
createAuthGuard,
|
|
19
|
-
createRoleGuard,
|
|
20
|
-
} from "./auth";
|
|
21
|
-
export type { BaseUser, UserWithRole, UserWithRoles } from "./auth";
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Filling Module - 만두소 🥟
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { ManduContext, ValidationError, CookieManager } from "./context";
|
|
6
|
+
export type { CookieOptions } from "./context";
|
|
7
|
+
export { ManduFilling, Mandu, LoaderTimeoutError } from "./filling";
|
|
8
|
+
export type { Handler, Guard, HttpMethod, Loader, LoaderOptions } from "./filling";
|
|
9
|
+
|
|
10
|
+
// Auth Guards
|
|
11
|
+
export {
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
AuthorizationError,
|
|
14
|
+
requireUser,
|
|
15
|
+
requireRole,
|
|
16
|
+
requireAnyRole,
|
|
17
|
+
requireAllRoles,
|
|
18
|
+
createAuthGuard,
|
|
19
|
+
createRoleGuard,
|
|
20
|
+
} from "./auth";
|
|
21
|
+
export type { BaseUser, UserWithRole, UserWithRoles } from "./auth";
|
package/src/generator/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./generate";
|
|
2
|
-
export * from "./templates";
|
|
3
|
-
export * from "./contract-glue";
|
|
1
|
+
export * from "./generate";
|
|
2
|
+
export * from "./templates";
|
|
3
|
+
export * from "./contract-glue";
|
package/src/report/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "./build";
|
|
1
|
+
export * from "./build";
|