@asapjs/error 1.0.0-alpha.0
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.md +229 -0
- package/dist/factory.d.ts +15 -0
- package/dist/factory.js +88 -0
- package/dist/helpers.d.ts +7 -0
- package/dist/helpers.js +67 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +22 -0
- package/dist/middleware.d.ts +7 -0
- package/dist/middleware.js +81 -0
- package/dist/schema.d.ts +3 -0
- package/dist/schema.js +38 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.js +26 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# @asapjs/error
|
|
2
|
+
|
|
3
|
+
TypeScript 기반 ASAP.js 프레임워크를 위한 Effect 통합 에러 처리 패키지
|
|
4
|
+
|
|
5
|
+
## 주요 기능
|
|
6
|
+
|
|
7
|
+
- **TypeIs 기반 에러 정의**: 간단하고 타입 안전한 에러 생성
|
|
8
|
+
- **자동 Swagger 문서화**: TypeIs 스키마로부터 OpenAPI 스펙 자동 생성
|
|
9
|
+
- **Effect 라이브러리 통합**: stack-safe 로깅 및 구조화된 에러 처리
|
|
10
|
+
- **레거시 호환성**: 기존 `HttpException` 지원
|
|
11
|
+
|
|
12
|
+
## 설치
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
yarn add @asapjs/error effect
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 기본 사용법
|
|
19
|
+
|
|
20
|
+
### 1. 에러 정의 (새로운 방식)
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { error, TypeIs } from '@asapjs/error';
|
|
24
|
+
|
|
25
|
+
export class UserErrors {
|
|
26
|
+
static NOT_FOUND = error(404, "USER_NOT_FOUND", "사용자를 찾을 수 없습니다. ID: {userId}", {
|
|
27
|
+
userId: TypeIs.INT({ comment: "사용자 ID" }),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
static EMAIL_DUPLICATE = error(409, "USER_EMAIL_DUPLICATE", "이미 사용 중인 이메일입니다: {email}", {
|
|
31
|
+
email: TypeIs.STRING({ comment: "중복된 이메일" }),
|
|
32
|
+
existingUserId: TypeIs.INT({ comment: "기존 사용자 ID", optional: true }),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
static INVALID_DATA = error(400, "USER_INVALID_DATA", "잘못된 사용자 데이터입니다", {
|
|
36
|
+
invalidFields: TypeIs.ARRAY({
|
|
37
|
+
type: () => TypeIs.STRING(),
|
|
38
|
+
comment: "유효하지 않은 필드 목록"
|
|
39
|
+
}),
|
|
40
|
+
details: TypeIs.JSON({
|
|
41
|
+
comment: "상세 오류 정보",
|
|
42
|
+
optional: true
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. 컨트롤러에서 사용
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { Get, Post, RouterController } from '@asapjs/router';
|
|
52
|
+
import { wrapWithEffect } from '@asapjs/error';
|
|
53
|
+
import { UserErrors } from './errors/UserErrors';
|
|
54
|
+
|
|
55
|
+
export default class UserController extends RouterController {
|
|
56
|
+
@Get('/:id', {
|
|
57
|
+
title: '사용자 조회',
|
|
58
|
+
errors: [UserErrors.NOT_FOUND], // Swagger에 자동으로 문서화
|
|
59
|
+
})
|
|
60
|
+
public getUser = wrapWithEffect(async ({ path }) => {
|
|
61
|
+
const user = await findUser(path.id);
|
|
62
|
+
if (!user) {
|
|
63
|
+
throw UserErrors.NOT_FOUND({ userId: path.id });
|
|
64
|
+
}
|
|
65
|
+
return { result: user };
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
@Post('/', {
|
|
69
|
+
title: '사용자 생성',
|
|
70
|
+
body: CreateUserDto,
|
|
71
|
+
errors: [UserErrors.EMAIL_DUPLICATE, UserErrors.INVALID_DATA],
|
|
72
|
+
})
|
|
73
|
+
public createUser = wrapWithEffect(async ({ body }) => {
|
|
74
|
+
// 이메일 검증
|
|
75
|
+
if (!isValidEmail(body.email)) {
|
|
76
|
+
throw UserErrors.INVALID_DATA({
|
|
77
|
+
invalidFields: ['email'],
|
|
78
|
+
details: { email: 'Invalid email format' }
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 중복 체크
|
|
83
|
+
const existing = await findUserByEmail(body.email);
|
|
84
|
+
if (existing) {
|
|
85
|
+
throw UserErrors.EMAIL_DUPLICATE({
|
|
86
|
+
email: body.email,
|
|
87
|
+
existingUserId: existing.id
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const user = await createUser(body);
|
|
92
|
+
return { result: user };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 3. Express 앱에 미들웨어 적용
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { effectErrorHandler } from '@asapjs/error';
|
|
101
|
+
|
|
102
|
+
app.use(effectErrorHandler);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## HTTP 에러 응답 포맷
|
|
106
|
+
|
|
107
|
+
모든 에러는 다음과 같은 통일된 포맷으로 응답됩니다:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"status": 404,
|
|
112
|
+
"errorCode": "USER_NOT_FOUND",
|
|
113
|
+
"message": "사용자를 찾을 수 없습니다. ID: 123",
|
|
114
|
+
"data": {
|
|
115
|
+
"userId": 123
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## 주요 특징
|
|
121
|
+
|
|
122
|
+
### 1. TypeIs를 활용한 타입 안전성
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// 컴파일 타임에 타입 체크
|
|
126
|
+
throw UserErrors.NOT_FOUND({ userId: "123" }); // ❌ Error: userId must be number
|
|
127
|
+
throw UserErrors.NOT_FOUND({ userId: 123 }); // ✅ OK
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 2. 메시지 템플릿
|
|
131
|
+
|
|
132
|
+
에러 메시지에 `{fieldName}` 형식으로 데이터를 삽입할 수 있습니다:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
static NOT_FOUND = error(404, "USER_NOT_FOUND", "사용자 {userId}를 찾을 수 없습니다", {
|
|
136
|
+
userId: TypeIs.INT(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// 사용 시: "사용자 123를 찾을 수 없습니다"
|
|
140
|
+
throw UserErrors.NOT_FOUND({ userId: 123 });
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 3. 자동 Swagger 문서화
|
|
144
|
+
|
|
145
|
+
TypeIs 스키마가 자동으로 OpenAPI 스펙으로 변환됩니다:
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
components:
|
|
149
|
+
schemas:
|
|
150
|
+
USER_NOT_FOUND:
|
|
151
|
+
type: object
|
|
152
|
+
properties:
|
|
153
|
+
status:
|
|
154
|
+
type: number
|
|
155
|
+
example: 404
|
|
156
|
+
errorCode:
|
|
157
|
+
type: string
|
|
158
|
+
example: USER_NOT_FOUND
|
|
159
|
+
message:
|
|
160
|
+
type: string
|
|
161
|
+
data:
|
|
162
|
+
type: object
|
|
163
|
+
properties:
|
|
164
|
+
userId:
|
|
165
|
+
type: integer
|
|
166
|
+
description: 사용자 ID
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## TypeIs 지원 타입
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
TypeIs.INT() // 정수
|
|
173
|
+
TypeIs.STRING() // 문자열
|
|
174
|
+
TypeIs.BOOLEAN() // 불린
|
|
175
|
+
TypeIs.FLOAT() // 실수
|
|
176
|
+
TypeIs.JSON() // JSON 객체
|
|
177
|
+
TypeIs.ARRAY() // 배열
|
|
178
|
+
TypeIs.DATETIME() // 날짜/시간
|
|
179
|
+
TypeIs.ENUM() // 열거형
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
모든 타입은 `optional: true` 옵션을 지원합니다.
|
|
183
|
+
|
|
184
|
+
## 고급 기능
|
|
185
|
+
|
|
186
|
+
### Effect 래핑
|
|
187
|
+
|
|
188
|
+
`wrapWithEffect`는 다음 기능을 제공합니다:
|
|
189
|
+
|
|
190
|
+
- 자동 스팬 트레이싱
|
|
191
|
+
- 구조화된 로깅 (요청 ID, 파일, 라인, 함수명 포함)
|
|
192
|
+
- Effect Exit 처리
|
|
193
|
+
|
|
194
|
+
### 레거시 호환성
|
|
195
|
+
|
|
196
|
+
기존 `HttpException`을 사용하는 코드도 자동으로 처리됩니다:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
throw new HttpException(400, '잘못된 요청입니다');
|
|
200
|
+
// 자동으로 { status: 400, errorCode: 'LEGACY_HTTP_EXCEPTION', message: '...' } 로 변환
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 마이그레이션 가이드
|
|
204
|
+
|
|
205
|
+
### 기존 방식
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
export class UserNotFoundError extends NotFoundError {
|
|
209
|
+
constructor(userId: number) {
|
|
210
|
+
super('USER_NOT_FOUND', `사용자를 찾을 수 없습니다. ID: ${userId}`, { userId });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw new UserNotFoundError(123);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 새로운 방식
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
export class UserErrors {
|
|
221
|
+
static NOT_FOUND = error(404, "USER_NOT_FOUND", "사용자를 찾을 수 없습니다. ID: {userId}", {
|
|
222
|
+
userId: TypeIs.INT(),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
throw UserErrors.NOT_FOUND({ userId: 123 });
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
새로운 방식은 더 간결하고, 타입 안전하며, 자동 문서화를 지원합니다.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HttpError } from './types';
|
|
2
|
+
export interface ErrorCreator<T = any> {
|
|
3
|
+
(data: T): HttpError;
|
|
4
|
+
_status: number;
|
|
5
|
+
_code: string;
|
|
6
|
+
_message: string;
|
|
7
|
+
_schema: Record<string, any>;
|
|
8
|
+
}
|
|
9
|
+
export declare function error<T extends Record<string, any>>(status: number, code: string, message: string, schema: T): ErrorCreator<InferTypeFromSchema<T>>;
|
|
10
|
+
export declare function setAddSchemeFunction(func: any): void;
|
|
11
|
+
export declare function markSwaggerAsReady(): void;
|
|
12
|
+
type InferTypeFromSchema<T extends Record<string, any>> = {
|
|
13
|
+
[K in keyof T]: any;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
package/dist/factory.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.error = error;
|
|
4
|
+
exports.setAddSchemeFunction = setAddSchemeFunction;
|
|
5
|
+
exports.markSwaggerAsReady = markSwaggerAsReady;
|
|
6
|
+
// addScheme will be injected at runtime
|
|
7
|
+
let addSchemeFunc = () => { };
|
|
8
|
+
const types_1 = require("./types");
|
|
9
|
+
const schema_1 = require("./schema");
|
|
10
|
+
// 지연 등록을 위한 큐
|
|
11
|
+
const pendingSchemas = [];
|
|
12
|
+
let isSwaggerReady = false;
|
|
13
|
+
function error(status, code, message, schema) {
|
|
14
|
+
// Swagger 스키마 등록 (지연 실행)
|
|
15
|
+
const registerSchema = () => registerErrorSwaggerSchema(code, status, message, schema);
|
|
16
|
+
if (isSwaggerReady) {
|
|
17
|
+
registerSchema();
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
pendingSchemas.push(registerSchema);
|
|
21
|
+
}
|
|
22
|
+
// 에러 생성 함수
|
|
23
|
+
const creator = ((data) => {
|
|
24
|
+
// 스키마 등록 확인 (첫 사용 시)
|
|
25
|
+
ensureSchemaRegistered(code, status, message, schema);
|
|
26
|
+
// 데이터 유효성 검사
|
|
27
|
+
(0, schema_1.validateDataWithSchema)(data, schema);
|
|
28
|
+
// 메시지 템플릿 처리 (예: "사용자 {userId}를 찾을 수 없습니다")
|
|
29
|
+
const finalMessage = interpolateMessage(message, data);
|
|
30
|
+
return new types_1.HttpError(status, code, finalMessage, data);
|
|
31
|
+
});
|
|
32
|
+
// 메타데이터 저장 (Swagger 문서화용)
|
|
33
|
+
creator._status = status;
|
|
34
|
+
creator._code = code;
|
|
35
|
+
creator._message = message;
|
|
36
|
+
// 원본 schema를 그대로 저장 (함수 호출 결과가 아닌 원본)
|
|
37
|
+
creator._schema = schema;
|
|
38
|
+
return creator;
|
|
39
|
+
}
|
|
40
|
+
// addScheme 함수 주입
|
|
41
|
+
function setAddSchemeFunction(func) {
|
|
42
|
+
addSchemeFunc = func;
|
|
43
|
+
}
|
|
44
|
+
// Swagger가 준비되면 호출
|
|
45
|
+
function markSwaggerAsReady() {
|
|
46
|
+
isSwaggerReady = true;
|
|
47
|
+
// 대기 중인 스키마 모두 등록
|
|
48
|
+
while (pendingSchemas.length > 0) {
|
|
49
|
+
const register = pendingSchemas.shift();
|
|
50
|
+
if (register)
|
|
51
|
+
register();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 스키마가 등록되었는지 확인하고 필요시 등록
|
|
55
|
+
const registeredSchemas = new Set();
|
|
56
|
+
function ensureSchemaRegistered(code, status, message, schema) {
|
|
57
|
+
if (!registeredSchemas.has(code)) {
|
|
58
|
+
registerErrorSwaggerSchema(code, status, message, schema);
|
|
59
|
+
registeredSchemas.add(code);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function registerErrorSwaggerSchema(code, status, message, schema) {
|
|
63
|
+
const swaggerSchema = {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
status: { type: 'number', example: status },
|
|
67
|
+
errorCode: { type: 'string', example: code },
|
|
68
|
+
message: { type: 'string', example: message },
|
|
69
|
+
data: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: Object.entries(schema).reduce((acc, [key, typeIs]) => {
|
|
72
|
+
acc[key] = (0, schema_1.typeIsToSwaggerSchema)(typeIs);
|
|
73
|
+
return acc;
|
|
74
|
+
}, {}),
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ['status', 'errorCode', 'message'],
|
|
78
|
+
};
|
|
79
|
+
addSchemeFunc({
|
|
80
|
+
name: code,
|
|
81
|
+
data: swaggerSchema,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function interpolateMessage(template, data) {
|
|
85
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
86
|
+
return data[key] !== undefined ? String(data[key]) : match;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Effect, Cause } from 'effect';
|
|
2
|
+
import { Response } from 'express';
|
|
3
|
+
import { HttpErrorBody } from './types';
|
|
4
|
+
export declare function makeHttpError(status: number, errorCode: string, message: string, data?: Record<string, any>): HttpErrorBody;
|
|
5
|
+
export declare function errorToResponse(error: unknown, res: Response): void;
|
|
6
|
+
export declare function causeToError(cause: Cause.Cause<unknown>): unknown;
|
|
7
|
+
export declare function runEffectAsPromise<A, E>(effect: Effect.Effect<A, E, never>): Promise<A>;
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeHttpError = makeHttpError;
|
|
4
|
+
exports.errorToResponse = errorToResponse;
|
|
5
|
+
exports.causeToError = causeToError;
|
|
6
|
+
exports.runEffectAsPromise = runEffectAsPromise;
|
|
7
|
+
const effect_1 = require("effect");
|
|
8
|
+
const types_1 = require("./types");
|
|
9
|
+
function makeHttpError(status, errorCode, message, data) {
|
|
10
|
+
return {
|
|
11
|
+
status,
|
|
12
|
+
errorCode,
|
|
13
|
+
message,
|
|
14
|
+
data,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function errorToResponse(error, res) {
|
|
18
|
+
let errorBody;
|
|
19
|
+
if (error instanceof types_1.HttpError) {
|
|
20
|
+
errorBody = error.toJSON();
|
|
21
|
+
}
|
|
22
|
+
else if (error && typeof error === 'object' && 'status' in error && 'message' in error && !('errorCode' in error)) {
|
|
23
|
+
// Legacy HttpException 지원
|
|
24
|
+
errorBody = {
|
|
25
|
+
status: error.status,
|
|
26
|
+
errorCode: 'LEGACY_HTTP_EXCEPTION',
|
|
27
|
+
message: error.message,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
else if (error && typeof error === 'object' && 'status' in error && 'errorCode' in error && 'message' in error) {
|
|
31
|
+
errorBody = error;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
errorBody = new types_1.HttpError(500, 'INTERNAL_SERVER_ERROR', error instanceof Error ? error.message : '알 수 없는 서버 오류가 발생했습니다.').toJSON();
|
|
35
|
+
}
|
|
36
|
+
res.status(errorBody.status).json(errorBody);
|
|
37
|
+
}
|
|
38
|
+
function causeToError(cause) {
|
|
39
|
+
const failureOption = effect_1.Cause.failureOption(cause);
|
|
40
|
+
const dieOption = effect_1.Cause.dieOption(cause);
|
|
41
|
+
if (failureOption._tag === 'Some') {
|
|
42
|
+
return failureOption.value;
|
|
43
|
+
}
|
|
44
|
+
else if (dieOption._tag === 'Some') {
|
|
45
|
+
return dieOption.value;
|
|
46
|
+
}
|
|
47
|
+
else if (effect_1.Cause.isInterrupted(cause)) {
|
|
48
|
+
return new Error('Operation interrupted');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const failures = effect_1.Cause.failures(cause);
|
|
52
|
+
if (effect_1.Chunk.size(failures) > 0) {
|
|
53
|
+
return effect_1.Chunk.unsafeGet(failures, 0);
|
|
54
|
+
}
|
|
55
|
+
return new Error('Unknown error');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function runEffectAsPromise(effect) {
|
|
59
|
+
const exit = await effect_1.Effect.runPromiseExit(effect);
|
|
60
|
+
if (effect_1.Exit.isSuccess(exit)) {
|
|
61
|
+
return exit.value;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const error = causeToError(exit.cause);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./helpers"), exports);
|
|
19
|
+
__exportStar(require("./middleware"), exports);
|
|
20
|
+
__exportStar(require("./factory"), exports);
|
|
21
|
+
__exportStar(require("./schema"), exports);
|
|
22
|
+
// HttpException은 router 패키지에서 직접 import 해야 함 (순환 의존성 방지)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from 'express';
|
|
2
|
+
interface RequestWithId extends Request {
|
|
3
|
+
id?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const effectErrorHandler: (error: unknown, req: RequestWithId, res: Response, next: NextFunction) => void;
|
|
6
|
+
export declare function wrapWithEffect<T extends (...args: any[]) => any>(handler: T): (...args: any[]) => Promise<any>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.effectErrorHandler = void 0;
|
|
4
|
+
exports.wrapWithEffect = wrapWithEffect;
|
|
5
|
+
const effect_1 = require("effect");
|
|
6
|
+
const common_1 = require("@asapjs/common");
|
|
7
|
+
const helpers_1 = require("./helpers");
|
|
8
|
+
function getStackInfo(error) {
|
|
9
|
+
const stack = error.stack || '';
|
|
10
|
+
const stackLines = stack.split('\n');
|
|
11
|
+
for (const line of stackLines) {
|
|
12
|
+
const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
|
|
13
|
+
if (match) {
|
|
14
|
+
const [, functionName, file, line, column] = match;
|
|
15
|
+
return { functionName, file, line: parseInt(line), column: parseInt(column) };
|
|
16
|
+
}
|
|
17
|
+
const simpleMatch = line.match(/at\s+(.+?):(\d+):(\d+)/);
|
|
18
|
+
if (simpleMatch) {
|
|
19
|
+
const [, file, line, column] = simpleMatch;
|
|
20
|
+
return { functionName: 'anonymous', file, line: parseInt(line), column: parseInt(column) };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const effectErrorHandler = (error, req, res, next) => {
|
|
26
|
+
const config = (0, common_1.getConfig)();
|
|
27
|
+
const requestId = req.id || 'unknown';
|
|
28
|
+
const stackInfo = error instanceof Error ? getStackInfo(error) : null;
|
|
29
|
+
const logContext = {
|
|
30
|
+
requestId,
|
|
31
|
+
method: req.method,
|
|
32
|
+
url: req.url,
|
|
33
|
+
ip: req.ip,
|
|
34
|
+
userAgent: req.get('user-agent'),
|
|
35
|
+
...(stackInfo && {
|
|
36
|
+
file: stackInfo.file,
|
|
37
|
+
line: stackInfo.line,
|
|
38
|
+
function: stackInfo.functionName,
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
if (error instanceof Error) {
|
|
42
|
+
common_1.logger.error(`Error: ${error.message}`, {
|
|
43
|
+
...logContext,
|
|
44
|
+
stack: error.stack,
|
|
45
|
+
errorName: error.name,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
common_1.logger.error('Non-Error thrown', {
|
|
50
|
+
...logContext,
|
|
51
|
+
error: String(error),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
(0, helpers_1.errorToResponse)(error, res);
|
|
55
|
+
};
|
|
56
|
+
exports.effectErrorHandler = effectErrorHandler;
|
|
57
|
+
function wrapWithEffect(handler) {
|
|
58
|
+
return async (...args) => {
|
|
59
|
+
const [req, res, next] = args;
|
|
60
|
+
try {
|
|
61
|
+
const effect = effect_1.Effect.promise(() => handler(...args));
|
|
62
|
+
const traced = effect_1.Effect.withSpan(effect, `${req.method} ${req.path}`, {
|
|
63
|
+
attributes: {
|
|
64
|
+
'http.method': req.method,
|
|
65
|
+
'http.path': req.path,
|
|
66
|
+
'http.request_id': req.id || 'unknown',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const result = await effect_1.Effect.runPromise(traced);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (next) {
|
|
74
|
+
next(error);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
package/dist/schema.d.ts
ADDED
package/dist/schema.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.typeIsToSwaggerSchema = typeIsToSwaggerSchema;
|
|
4
|
+
exports.validateDataWithSchema = validateDataWithSchema;
|
|
5
|
+
function typeIsToSwaggerSchema(typeIs) {
|
|
6
|
+
// @asapjs/schema의 toSwagger 메서드 사용
|
|
7
|
+
if (typeIs.toSwagger) {
|
|
8
|
+
return typeIs.toSwagger();
|
|
9
|
+
}
|
|
10
|
+
// fallback
|
|
11
|
+
return { type: 'string' };
|
|
12
|
+
}
|
|
13
|
+
function validateDataWithSchema(data, schema) {
|
|
14
|
+
if (!data || typeof data !== 'object') {
|
|
15
|
+
throw new Error('Invalid data: must be an object');
|
|
16
|
+
}
|
|
17
|
+
for (const [key, typeIs] of Object.entries(schema)) {
|
|
18
|
+
const value = data[key];
|
|
19
|
+
// TypeIs가 함수로 전달된 경우 실행
|
|
20
|
+
const schemaType = typeof typeIs === 'function' ? typeIs() : typeIs;
|
|
21
|
+
// optional 여부 확인
|
|
22
|
+
const isOptional = schemaType.__options.optional || false;
|
|
23
|
+
// 필수 필드 체크
|
|
24
|
+
if (value === undefined && !isOptional) {
|
|
25
|
+
throw new Error(`Missing required field: ${key}`);
|
|
26
|
+
}
|
|
27
|
+
// 값이 있을 때만 타입 체크
|
|
28
|
+
if (value !== undefined) {
|
|
29
|
+
validateFieldType(key, value, schemaType);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function validateFieldType(fieldName, value, schemaType) {
|
|
34
|
+
// @asapjs/schema의 validate 메서드 사용
|
|
35
|
+
if (!schemaType.validate(value)) {
|
|
36
|
+
throw new Error(`Field '${fieldName}' has invalid type for ${schemaType.__name}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface HttpErrorBody {
|
|
2
|
+
status: number;
|
|
3
|
+
errorCode: string;
|
|
4
|
+
message: string;
|
|
5
|
+
data?: Record<string, any>;
|
|
6
|
+
}
|
|
7
|
+
export declare class HttpError extends Error implements HttpErrorBody {
|
|
8
|
+
readonly status: number;
|
|
9
|
+
readonly errorCode: string;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly data?: Record<string, any>;
|
|
12
|
+
constructor(status: number, errorCode: string, message: string, data?: Record<string, any>);
|
|
13
|
+
toJSON(): HttpErrorBody;
|
|
14
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HttpError = void 0;
|
|
4
|
+
class HttpError extends Error {
|
|
5
|
+
constructor(status, errorCode, message, data) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'HttpError';
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.errorCode = errorCode;
|
|
10
|
+
this.message = message;
|
|
11
|
+
this.data = data;
|
|
12
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
13
|
+
if (Error.captureStackTrace) {
|
|
14
|
+
Error.captureStackTrace(this, HttpError);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
toJSON() {
|
|
18
|
+
return {
|
|
19
|
+
status: this.status,
|
|
20
|
+
errorCode: this.errorCode,
|
|
21
|
+
message: this.message,
|
|
22
|
+
data: this.data,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.HttpError = HttpError;
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asapjs/error",
|
|
3
|
+
"version": "1.0.0-alpha.0",
|
|
4
|
+
"description": "Error handling utilities for ASAP.js with Effect integration",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@asapjs/common": "1.0.0-alpha.0",
|
|
16
|
+
"@asapjs/schema": "1.0.0-alpha.0",
|
|
17
|
+
"@asapjs/types": "1.0.0-alpha.0",
|
|
18
|
+
"effect": "^3.11.11",
|
|
19
|
+
"express": "^4.17.3"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/express": "^4.17.13",
|
|
23
|
+
"@types/node": "^17.0.21",
|
|
24
|
+
"typescript": "^5.8.3"
|
|
25
|
+
},
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"gitHead": "79561e3604dc505813035558d1bfcc6cda0b088a"
|
|
32
|
+
}
|