@pori15/logixlysia 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ export const parseError = (error: unknown): string => {
2
+ let message = 'An error occurred'
3
+
4
+ if (error instanceof Error) {
5
+ message = error.message
6
+ } else if (error && typeof error === 'object' && 'message' in error) {
7
+ message = error.message as string
8
+ } else {
9
+ message = String(error)
10
+ }
11
+
12
+ console.error(`Parsing error: ${message}`)
13
+
14
+ return message
15
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * RFC 9457 Problem JSON 格式化工具
3
+ * @see https://www.rfc-editor.org/rfc/rfc9457.html
4
+ */
5
+
6
+ import { ProblemError } from "../Error/errors";
7
+ import { Code } from "../Error/type";
8
+
9
+
10
+ /**
11
+ * 默认的状态码与标题映射表 (RFC 标准推荐)
12
+ */
13
+ const DEFAULT_TITLES: Record<number, string> = {
14
+ 400: 'Bad Request',
15
+ 401: 'Unauthorized',
16
+ 403: 'Forbidden',
17
+ 404: 'Not Found',
18
+ 409: 'Conflict',
19
+ 422: 'Unprocessable Entity',
20
+ 500: 'Internal Server Error',
21
+ 503: 'Service Unavailable',
22
+ };
23
+
24
+ export const normalizeToProblem = (
25
+ error: any,
26
+ code: Code,
27
+ path: string,
28
+ typeBaseUrl: string = 'about:blank'
29
+ ): ProblemError => {
30
+ // 1. 如果已经是 ProblemError,直接补充 instance 并返回
31
+ if (error instanceof ProblemError) {
32
+ // 如果没有 instance,自动补全为当前请求路径
33
+ return error.instance ? error : new ProblemError(
34
+ error.type,
35
+ error.title,
36
+ error.status,
37
+ error.detail,
38
+ path,
39
+ error.extensions
40
+ );
41
+ }
42
+
43
+ // 2. 初始化默认值
44
+ let status = 500;
45
+ let title = 'Internal Server Error';
46
+ let detail = error instanceof Error ? error.message : String(error);
47
+ let extensions: Record<string, unknown> = {};
48
+
49
+ // 3. 识别 Elysia 内置错误码并“对齐”标准
50
+ switch (code) {
51
+ case 'VALIDATION':
52
+ status = 400;
53
+ title = 'Validation Failed';
54
+ // 提取 Elysia 的校验细节
55
+ extensions = { errors: error.all || [] };
56
+ break;
57
+ case 'NOT_FOUND':
58
+ status = 404;
59
+ title = 'Resource Not Found';
60
+ break;
61
+ case 'PARSE':
62
+ status = 400;
63
+ title = 'Invalid Payload';
64
+ detail = 'The request body could not be parsed as valid JSON.';
65
+ break;
66
+ case 'INVALID_COOKIE_SIGNATURE':
67
+ status = 401;
68
+ title = 'Invalid Credentials';
69
+ break;
70
+ default:
71
+ // 如果错误对象本身带有状态码(比如某些库抛出的)
72
+ if (typeof error?.status === 'number') {
73
+ status = error.status;
74
+ title = DEFAULT_TITLES[status] || 'Unknown Error';
75
+ }
76
+ break;
77
+ }
78
+
79
+ // 4. 构造并返回标准的 ProblemError
80
+ return new ProblemError(
81
+ typeBaseUrl === 'about:blank' ? typeBaseUrl : `${typeBaseUrl}/${code}`,
82
+ title,
83
+ status,
84
+ detail,
85
+ path,
86
+ extensions
87
+ );
88
+ };
89
+
90
+
91
+ export interface ProblemJson {
92
+ type?: string
93
+ title: string
94
+ status: number
95
+ detail?: string
96
+ instance?: string
97
+ [key: string]: unknown
98
+ }
99
+
100
+ export interface ProblemJsonOptions {
101
+ /**
102
+ * 基础 URL,用于生成错误类型的链接
103
+ * @example 'https://api.example.com/errors'
104
+ */
105
+ typeBaseUrl?: string
106
+
107
+ /**
108
+ * 是否在日志中显示完整的 Problem JSON
109
+ * @default true
110
+ */
111
+ enabled?: boolean
112
+
113
+ /**
114
+ * 自定义格式化函数
115
+ */
116
+ format?: (error: unknown, request: Request) => ProblemJson | null
117
+ }
118
+
119
+ const isErrorWithStatus = (
120
+ value: unknown
121
+ ): value is { status: number } =>
122
+ typeof value === 'object' &&
123
+ value !== null &&
124
+ 'status' in value &&
125
+ typeof (value as { status?: unknown }).status === 'number'
126
+
127
+ const isErrorLike = (value: unknown): value is Error =>
128
+ value instanceof Error ||
129
+ (typeof value === 'object' &&
130
+ value !== null &&
131
+ 'message' in value &&
132
+ typeof (value as { message?: unknown }).message === 'string')
133
+
134
+ /**
135
+ * 将错误转换为 RFC 9457 Problem JSON 格式
136
+ */
137
+ export function toProblemJson(
138
+ error: unknown,
139
+ request: Request,
140
+ options: ProblemJsonOptions = {}
141
+ ): ProblemJson {
142
+ // 如果提供了自定义格式化函数,使用它
143
+ if (options.format) {
144
+ const custom = options.format(error, request)
145
+ if (custom) {
146
+ return custom
147
+ }
148
+ }
149
+
150
+ const url = new URL(request.url)
151
+ const status = isErrorWithStatus(error) ? error.status : 500
152
+ const message = isErrorLike(error)
153
+ ? error.message
154
+ : String(error ?? 'Unknown Error')
155
+
156
+ // 默认的 Problem JSON 结构
157
+ const problem: ProblemJson = {
158
+ type: options.typeBaseUrl
159
+ ? `${options.typeBaseUrl}/${status}`
160
+ : 'about:blank',
161
+ title: getDefaultTitle(status),
162
+ status,
163
+ detail: message,
164
+ instance: url.pathname + url.search
165
+ }
166
+
167
+ // 尝试从错误对象中提取额外的信息
168
+ if (typeof error === 'object' && error !== null) {
169
+ const err = error as Record<string, unknown>
170
+
171
+ // 如果错误已经有 Problem JSON 结构,直接使用
172
+ if ('type' in err || 'title' in err) {
173
+ if (err.type) problem.type = err.type as string
174
+ if (err.title) problem.title = err.title as string
175
+ }
176
+
177
+ // 添加其他扩展字段(排除标准字段)
178
+ for (const [key, value] of Object.entries(err)) {
179
+ if (
180
+ !['status', 'message', 'type', 'title', 'detail', 'instance'].includes(
181
+ key
182
+ )
183
+ ) {
184
+ problem[key] = value
185
+ }
186
+ }
187
+ }
188
+
189
+ return problem
190
+ }
191
+
192
+ /**
193
+ * 根据状态码获取默认标题
194
+ */
195
+ function getDefaultTitle(status: number): string {
196
+ const titles: Record<number, string> = {
197
+ 400: 'Bad Request',
198
+ 401: 'Unauthorized',
199
+ 402: 'Payment Required',
200
+ 403: 'Forbidden',
201
+ 404: 'Not Found',
202
+ 405: 'Method Not Allowed',
203
+ 406: 'Not Acceptable',
204
+ 407: 'Proxy Authentication Required',
205
+ 408: 'Request Timeout',
206
+ 409: 'Conflict',
207
+ 410: 'Gone',
208
+ 411: 'Length Required',
209
+ 412: 'Precondition Failed',
210
+ 413: 'Payload Too Large',
211
+ 414: 'URI Too Long',
212
+ 415: 'Unsupported Media Type',
213
+ 416: 'Range Not Satisfiable',
214
+ 417: 'Expectation Failed',
215
+ 418: "I'm a teapot",
216
+ 422: 'Unprocessable Entity',
217
+ 423: 'Locked',
218
+ 424: 'Failed Dependency',
219
+ 425: 'Too Early',
220
+ 426: 'Upgrade Required',
221
+ 428: 'Precondition Required',
222
+ 429: 'Too Many Requests',
223
+ 431: 'Request Header Fields Too Large',
224
+ 451: 'Unavailable For Legal Reasons',
225
+ 500: 'Internal Server Error',
226
+ 501: 'Not Implemented',
227
+ 502: 'Bad Gateway',
228
+ 503: 'Service Unavailable',
229
+ 504: 'Gateway Timeout',
230
+ 505: 'HTTP Version Not Supported',
231
+ 506: 'Variant Also Negotiates',
232
+ 507: 'Insufficient Storage',
233
+ 508: 'Loop Detected',
234
+ 510: 'Not Extended',
235
+ 511: 'Network Authentication Required'
236
+ }
237
+
238
+ return titles[status] || 'Error'
239
+ }
240
+
241
+ /**
242
+ * 将 Problem JSON 格式化为日志字符串
243
+ */
244
+ export function formatProblemJsonLog(
245
+ problem: ProblemJson,
246
+ request: Request
247
+ ): string {
248
+ const url = new URL(request.url)
249
+ const parts: string[] = []
250
+
251
+ // 标题行
252
+ const statusStr = problem.status.toString()
253
+ const emoji = getStatusEmoji(problem.status)
254
+ parts.push(
255
+ `\n${emoji} [HTTP ${statusStr}] ${request.method} ${url.pathname}`
256
+ )
257
+
258
+ // Problem JSON 内容
259
+ parts.push(` Type: ${problem.type}`)
260
+ parts.push(` Title: ${problem.title}`)
261
+ if (problem.detail) {
262
+ parts.push(` Detail: ${problem.detail}`)
263
+ }
264
+ if (problem.instance) {
265
+ parts.push(` Instance: ${problem.instance}`)
266
+ }
267
+
268
+ // 扩展字段
269
+ const extensions = Object.entries(problem).filter(
270
+ ([key]) =>
271
+ !['type', 'title', 'status', 'detail', 'instance'].includes(key)
272
+ )
273
+ if (extensions.length > 0) {
274
+ parts.push(' Extensions:')
275
+ for (const [key, value] of extensions) {
276
+ parts.push(` ${key}: ${JSON.stringify(value)}`)
277
+ }
278
+ }
279
+
280
+ return parts.join('\n')
281
+ }
282
+
283
+ function getStatusEmoji(status: number): string {
284
+ if (status >= 500) return '🔴'
285
+ if (status >= 400) return '🟡'
286
+ if (status >= 300) return '🔵'
287
+ if (status >= 200) return '🟢'
288
+ return '⚪'
289
+ }
@@ -0,0 +1,91 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import { basename, dirname } from 'node:path'
3
+
4
+ const SIZE_REGEX = /^(\d+(?:\.\d+)?)(k|kb|m|mb|g|gb)$/i
5
+ const INTERVAL_REGEX = /^(\d+)(h|d|w)$/i
6
+ const ROTATED_REGEX = /\.(\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2})(?:\.gz)?$/
7
+
8
+ export const parseSize = (value: number | string): number => {
9
+ if (typeof value === 'number') {
10
+ return value
11
+ }
12
+
13
+ const trimmed = value.trim()
14
+ const asNumber = Number(trimmed)
15
+ if (Number.isFinite(asNumber)) {
16
+ return asNumber
17
+ }
18
+
19
+ const match = trimmed.match(SIZE_REGEX)
20
+ if (!match) {
21
+ throw new Error(`Invalid size format: ${value}`)
22
+ }
23
+
24
+ const amount = Number(match[1])
25
+ const unit = match[2].toLowerCase()
26
+
27
+ let base = 1024
28
+ if (unit.startsWith('m')) {
29
+ base = 1024 * 1024
30
+ } else if (unit.startsWith('g')) {
31
+ base = 1024 * 1024 * 1024
32
+ }
33
+
34
+ return Math.floor(amount * base)
35
+ }
36
+
37
+ export const parseInterval = (value: string): number => {
38
+ const match = value.trim().match(INTERVAL_REGEX)
39
+ if (!match) {
40
+ throw new Error(`Invalid interval format: ${value}`)
41
+ }
42
+
43
+ const amount = Number(match[1])
44
+ const unit = match[2].toLowerCase()
45
+
46
+ let ms = 60 * 60 * 1000
47
+ if (unit === 'd') {
48
+ ms = 24 * 60 * 60 * 1000
49
+ } else if (unit === 'w') {
50
+ ms = 7 * 24 * 60 * 60 * 1000
51
+ }
52
+
53
+ return amount * ms
54
+ }
55
+
56
+ export const parseRetention = (
57
+ value: number | string
58
+ ): { type: 'count' | 'time'; value: number } => {
59
+ if (typeof value === 'number') {
60
+ return { type: 'count', value }
61
+ }
62
+ return { type: 'time', value: parseInterval(value) }
63
+ }
64
+
65
+ export const shouldRotateBySize = async (
66
+ filePath: string,
67
+ maxSizeBytes: number
68
+ ): Promise<boolean> => {
69
+ try {
70
+ const stat = await fs.stat(filePath)
71
+ return stat.size > maxSizeBytes
72
+ } catch {
73
+ return false
74
+ }
75
+ }
76
+
77
+ export const getRotatedFiles = async (filePath: string): Promise<string[]> => {
78
+ const dir = dirname(filePath)
79
+ const base = basename(filePath)
80
+
81
+ let entries: string[]
82
+ try {
83
+ entries = await fs.readdir(dir)
84
+ } catch {
85
+ return []
86
+ }
87
+
88
+ return entries
89
+ .filter(name => name.startsWith(`${base}.`) && ROTATED_REGEX.test(name))
90
+ .map(name => `${dir}/${name}`)
91
+ }