@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,258 @@
1
+ // src/libs/elysia-http-problem-json/errors.ts
2
+
3
+ export interface ProblemDocument {
4
+ type: string;
5
+ title: string;
6
+ status?: number;
7
+ detail?: string;
8
+ instance?: string;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ /**
13
+ * RFC 9457 Problem Details Error Base Class
14
+ *
15
+ * Core members as per RFC 9457:
16
+ * - type: A URI reference [RFC3986] that identifies the problem type.
17
+ * Defaults to "about:blank" when omitted.
18
+ * - title: A short, human-readable summary of the problem type.
19
+ * - status: The HTTP status code ([RFC7231], Section 6).
20
+ * - detail: A human-readable explanation specific to this occurrence of the problem.
21
+ * - instance: A URI reference that identifies the specific occurrence of the problem.
22
+ *
23
+ * Extension members: Additional properties can be added to provide more context.
24
+ * These are serialized as-is in the JSON response.
25
+ */
26
+ /**
27
+ * RFC 9457 Error Base Class
28
+ * * 修改思路:直接使用 public readonly 属性,拒绝嵌套,拒绝 Getter。
29
+ */
30
+ export class ProblemError extends Error {
31
+ // 1. 直接声明公开属性
32
+ public readonly status: number;
33
+ public readonly title: string;
34
+ public readonly type: string;
35
+ public readonly detail?: string;
36
+ public readonly instance?: string;
37
+ public readonly extensions?: Record<string, unknown>;
38
+
39
+ constructor(
40
+ type = "about:blank",
41
+ title: string,
42
+ status: number,
43
+ detail?: string,
44
+ instance?: string,
45
+ extensions: Record<string, unknown> = {}
46
+ ) {
47
+ super(detail || title);
48
+ Object.setPrototypeOf(this, ProblemError.prototype);
49
+
50
+ // 2. 直接赋值给 this
51
+ this.status = status;
52
+ this.title = title;
53
+ this.type = type;
54
+ this.detail = detail;
55
+ this.instance = instance;
56
+ this.extensions = extensions;
57
+ }
58
+
59
+ // 3. toJSON 的时候动态组装一下即可
60
+ toJSON(): ProblemDocument {
61
+ return {
62
+ type: this.type,
63
+ title: this.title,
64
+ status: this.status,
65
+ ...(this.detail ? { detail: this.detail } : {}),
66
+ ...(this.instance ? { instance: this.instance } : {}),
67
+ // 把扩展字段展开 (extensions)
68
+ ...this.extensions,
69
+ };
70
+ }
71
+ }
72
+
73
+ // --- 40X Errors ---
74
+ class BadRequest extends ProblemError {
75
+ constructor(detail?: string, extensions?: Record<string, any>) {
76
+ super(
77
+ "https://httpstatuses.com/400",
78
+ "Bad Request",
79
+ 400,
80
+ detail,
81
+ undefined,
82
+ extensions
83
+ );
84
+ }
85
+ }
86
+
87
+ class Unauthorized extends ProblemError {
88
+ constructor(detail?: string, extensions?: Record<string, any>) {
89
+ super(
90
+ "https://httpstatuses.com/401",
91
+ "Unauthorized",
92
+ 401,
93
+ detail,
94
+ undefined,
95
+ extensions
96
+ );
97
+ }
98
+ }
99
+
100
+ class Forbidden extends ProblemError {
101
+ constructor(detail?: string, extensions?: Record<string, any>) {
102
+ super(
103
+ "https://httpstatuses.com/403",
104
+ "Forbidden",
105
+ 403,
106
+ detail,
107
+ undefined,
108
+ extensions
109
+ );
110
+ }
111
+ }
112
+
113
+ class NotFound extends ProblemError {
114
+ constructor(detail?: string, extensions?: Record<string, any>) {
115
+ super(
116
+ "https://httpstatuses.com/404",
117
+ "Not Found",
118
+ 404,
119
+ detail,
120
+ undefined,
121
+ extensions
122
+ );
123
+ }
124
+ }
125
+
126
+ class Conflict extends ProblemError {
127
+ constructor(detail?: string, extensions?: Record<string, any>) {
128
+ super(
129
+ "https://httpstatuses.com/409",
130
+ "Conflict",
131
+ 409,
132
+ detail,
133
+ undefined,
134
+ extensions
135
+ );
136
+ }
137
+ }
138
+
139
+ class PaymentRequired extends ProblemError {
140
+ constructor(detail?: string, extensions?: Record<string, any>) {
141
+ super(
142
+ "https://httpstatuses.com/402",
143
+ "Payment Required",
144
+ 402,
145
+ detail,
146
+ undefined,
147
+ extensions
148
+ );
149
+ }
150
+ }
151
+
152
+ class MethodNotAllowed extends ProblemError {
153
+ constructor(detail?: string, extensions?: Record<string, any>) {
154
+ super(
155
+ "https://httpstatuses.com/405",
156
+ "Method Not Allowed",
157
+ 405,
158
+ detail,
159
+ undefined,
160
+ extensions
161
+ );
162
+ }
163
+ }
164
+
165
+ class NotAcceptable extends ProblemError {
166
+ constructor(detail?: string, extensions?: Record<string, any>) {
167
+ super(
168
+ "https://httpstatuses.com/406",
169
+ "Not Acceptable",
170
+ 406,
171
+ detail,
172
+ undefined,
173
+ extensions
174
+ );
175
+ }
176
+ }
177
+
178
+ // 50X Errors
179
+ class InternalServerError extends ProblemError {
180
+ constructor(detail?: string, extensions?: Record<string, any>) {
181
+ super(
182
+ "https://httpstatuses.com/500",
183
+ "Internal Server Error",
184
+ 500,
185
+ detail,
186
+ undefined,
187
+ extensions
188
+ );
189
+ }
190
+ }
191
+
192
+ class NotImplemented extends ProblemError {
193
+ constructor(detail?: string, extensions?: Record<string, any>) {
194
+ super(
195
+ "https://httpstatuses.com/501",
196
+ "Not Implemented",
197
+ 501,
198
+ detail,
199
+ undefined,
200
+ extensions
201
+ );
202
+ }
203
+ }
204
+
205
+ class BadGateway extends ProblemError {
206
+ constructor(detail?: string, extensions?: Record<string, any>) {
207
+ super(
208
+ "https://httpstatuses.com/502",
209
+ "Bad Gateway",
210
+ 502,
211
+ detail,
212
+ undefined,
213
+ extensions
214
+ );
215
+ }
216
+ }
217
+
218
+ class ServiceUnavailable extends ProblemError {
219
+ constructor(detail?: string, extensions?: Record<string, any>) {
220
+ super(
221
+ "https://httpstatuses.com/503",
222
+ "Service Unavailable",
223
+ 503,
224
+ detail,
225
+ undefined,
226
+ extensions
227
+ );
228
+ }
229
+ }
230
+
231
+ class GatewayTimeout extends ProblemError {
232
+ constructor(detail?: string, extensions?: Record<string, any>) {
233
+ super(
234
+ "https://httpstatuses.com/504",
235
+ "Gateway Timeout",
236
+ 504,
237
+ detail,
238
+ undefined,
239
+ extensions
240
+ );
241
+ }
242
+ }
243
+
244
+ export const HttpError = {
245
+ BadRequest,
246
+ Unauthorized,
247
+ PaymentRequired,
248
+ Forbidden,
249
+ NotFound,
250
+ MethodNotAllowed,
251
+ NotAcceptable,
252
+ Conflict,
253
+ InternalServerError,
254
+ NotImplemented,
255
+ BadGateway,
256
+ ServiceUnavailable,
257
+ GatewayTimeout,
258
+ } as const;
@@ -0,0 +1,51 @@
1
+ // src/libs/elysia-http-problem-json/types.ts
2
+
3
+ import { HttpError, ProblemError } from "./errors";
4
+
5
+ export type Code =
6
+ | number
7
+ | "PROBLEM_ERROR"
8
+ | "UNKNOWN"
9
+ | "VALIDATION"
10
+ | "NOT_FOUND"
11
+ | "PARSE"
12
+ | "INTERNAL_SERVER_ERROR"
13
+ | "INVALID_COOKIE_SIGNATURE"
14
+ | "INVALID_FILE_TYPE";
15
+
16
+ // 获取 HttpError 对象的所有 Key (例如 "BadRequest" | "NotFound")
17
+ export type HttpErrorType = keyof typeof HttpError;
18
+
19
+ export interface ErrorContext {
20
+ request: Request;
21
+ path: string;
22
+ code: string | number;
23
+ error: unknown;
24
+ }
25
+
26
+ export interface HttpProblemJsonOptions {
27
+ /**
28
+ * 自定义错误类型的 Base URL
29
+ * @example "https://api.mysite.com/errors"
30
+ */
31
+ typeBaseUrl?: string;
32
+
33
+ /**
34
+ * 🪝 Transform Hook
35
+ * 将未知错误转换为 HttpError。
36
+ * 返回 undefined/null 表示不处理(走默认逻辑)。
37
+ */
38
+ transform?: (
39
+ error: unknown,
40
+ context: ErrorContext
41
+ ) => ProblemError | undefined | null; // 这里直接返回 ProblemError 实例更好,或者用 HttpErrorType 也可以,看你喜好
42
+
43
+ /**
44
+ * 📢 Listen Hook
45
+ * 在响应发送前触发(用于日志)。
46
+ */
47
+ onBeforeRespond?: (
48
+ problem: ProblemError,
49
+ context: ErrorContext
50
+ ) => void | Promise<void>;
51
+ }
@@ -0,0 +1,26 @@
1
+ import elysiaPkg from 'elysia/package.json'
2
+
3
+ const centerText = (text: string, width: number): string => {
4
+ if (text.length >= width) {
5
+ return text.slice(0, width)
6
+ }
7
+
8
+ const left = Math.floor((width - text.length) / 2)
9
+ const right = width - text.length - left
10
+ return `${' '.repeat(left)}${text}${' '.repeat(right)}`
11
+ }
12
+
13
+ export const renderBanner = (message: string): string => {
14
+ const versionLine = `Elysia v${elysiaPkg.version}`
15
+ const contentWidth = Math.max(message.length, versionLine.length)
16
+ const innerWidth = contentWidth + 4 // 2 spaces padding on both sides
17
+
18
+ const top = `┌${'─'.repeat(innerWidth)}┐`
19
+ const bot = `└${'─'.repeat(innerWidth)}┘`
20
+ const empty = `│${' '.repeat(innerWidth)}│`
21
+
22
+ const versionRow = `│${centerText(versionLine, innerWidth)}│`
23
+ const messageRow = `│ ${message}${' '.repeat(Math.max(0, innerWidth - message.length - 4))} │`
24
+
25
+ return [top, empty, versionRow, empty, messageRow, empty, bot].join('\n')
26
+ }
@@ -0,0 +1,28 @@
1
+ import type { Options } from '../interfaces'
2
+ import { renderBanner } from './banner'
3
+
4
+ export const startServer = (
5
+ server: { port?: number; hostname?: string; protocol?: string | null },
6
+ options: Options
7
+ ): void => {
8
+ const showStartupMessage = options.config?.showStartupMessage ?? true
9
+ if (!showStartupMessage) {
10
+ return
11
+ }
12
+
13
+ const { port, hostname, protocol } = server
14
+ if (port === undefined || !hostname || !protocol) {
15
+ return
16
+ }
17
+
18
+ const url = `${protocol}://${hostname}:${port}`
19
+ const message = `🦊 Elysia is running at ${url}`
20
+
21
+ const format = options.config?.startupMessageFormat ?? 'banner'
22
+ if (format === 'simple') {
23
+ console.log(message)
24
+ return
25
+ }
26
+
27
+ console.log(renderBanner(message))
28
+ }
@@ -0,0 +1,58 @@
1
+ import { StatusMap } from 'elysia'
2
+
3
+ const DIGITS_ONLY = /^\d+$/
4
+ const DELIMITERS = /[_-]+/g
5
+ const CAMEL_BOUNDARY_1 = /([a-z0-9])([A-Z])/g
6
+ const CAMEL_BOUNDARY_2 = /([A-Z])([A-Z][a-z])/g
7
+ const APOSTROPHES = /['’]/g
8
+ const NON_ALPHANUMERIC = /[^a-z0-9\s]+/g
9
+ const WHITESPACE = /\s+/g
10
+
11
+ const normalizeStatusName = (value: string): string => {
12
+ // Handles common variants:
13
+ // - case differences: "not found" vs "Not Found"
14
+ // - spacing/punctuation: "Not-Found", "not_found"
15
+ // - camelCase/PascalCase: "InternalServerError"
16
+ const trimmed = value.trim()
17
+ if (!trimmed) {
18
+ return ''
19
+ }
20
+
21
+ return trimmed
22
+ .replace(DELIMITERS, ' ')
23
+ .replace(CAMEL_BOUNDARY_1, '$1 $2')
24
+ .replace(CAMEL_BOUNDARY_2, '$1 $2')
25
+ .replace(APOSTROPHES, '')
26
+ .toLowerCase()
27
+ .replace(NON_ALPHANUMERIC, ' ')
28
+ .replace(WHITESPACE, ' ')
29
+ .trim()
30
+ }
31
+
32
+ const STATUS_BY_NORMALIZED_NAME = (() => {
33
+ const map = new Map<string, number>()
34
+
35
+ for (const [name, code] of Object.entries(StatusMap)) {
36
+ map.set(normalizeStatusName(name), code)
37
+ }
38
+
39
+ return map
40
+ })()
41
+
42
+ export const getStatusCode = (value: unknown): number => {
43
+ if (typeof value === 'number' && Number.isFinite(value)) {
44
+ return value
45
+ }
46
+
47
+ if (typeof value === 'string') {
48
+ const trimmed = value.trim()
49
+ if (DIGITS_ONLY.test(trimmed)) {
50
+ return Number(trimmed)
51
+ }
52
+
53
+ const known = STATUS_BY_NORMALIZED_NAME.get(normalizeStatusName(trimmed))
54
+ return known ?? 500
55
+ }
56
+
57
+ return 500
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,141 @@
1
+ import { Elysia, type SingletonBase } from 'elysia'
2
+ import { startServer } from './extensions'
3
+ import type { LogixlysiaStore, Options } from './interfaces'
4
+ import { createLogger } from './logger'
5
+ import { normalizeToProblem } from './utils/handle-error'
6
+
7
+ export type Logixlysia = Elysia<
8
+ 'Logixlysia',
9
+ SingletonBase & { store: LogixlysiaStore }
10
+ >
11
+
12
+ export const logixlysia = (options: Options = {}): Logixlysia => {
13
+ const didCustomLog = new WeakSet<Request>()
14
+ const baseLogger = createLogger(options)
15
+ const logger = {
16
+ ...baseLogger,
17
+ debug: (
18
+ request: Request,
19
+ message: string,
20
+ context?: Record<string, unknown>
21
+ ) => {
22
+ didCustomLog.add(request)
23
+ baseLogger.debug(request, message, context)
24
+ },
25
+ info: (
26
+ request: Request,
27
+ message: string,
28
+ context?: Record<string, unknown>
29
+ ) => {
30
+ didCustomLog.add(request)
31
+ baseLogger.info(request, message, context)
32
+ },
33
+ warn: (
34
+ request: Request,
35
+ message: string,
36
+ context?: Record<string, unknown>
37
+ ) => {
38
+ didCustomLog.add(request)
39
+ baseLogger.warn(request, message, context)
40
+ },
41
+ error: (
42
+ request: Request,
43
+ message: string,
44
+ context?: Record<string, unknown>
45
+ ) => {
46
+ didCustomLog.add(request)
47
+ baseLogger.error(request, message, context)
48
+ }
49
+ }
50
+
51
+ const app = new Elysia({
52
+ name: 'Logixlysia',
53
+ detail: {
54
+ description:
55
+ 'Logixlysia is a plugin for Elysia that provides a logger and pino logger.',
56
+ tags: ['logging', 'pino']
57
+ }
58
+ })
59
+
60
+ return (
61
+ app
62
+ .state('logger', logger)
63
+ .state('pino', logger.pino)
64
+ .state('beforeTime', BigInt(0))
65
+ .onStart(({ server }) => {
66
+ if (server) {
67
+ startServer(server, options)
68
+ }
69
+ })
70
+ .onRequest(({ store }) => {
71
+ store.beforeTime = process.hrtime.bigint()
72
+ })
73
+ .onAfterHandle(({ request, set, store }) => {
74
+ if (didCustomLog.has(request)) {
75
+ return
76
+ }
77
+
78
+ const status = typeof set.status === 'number' ? set.status : 200
79
+ let level: 'INFO' | 'WARNING' | 'ERROR' = 'INFO'
80
+ if (status >= 500) {
81
+ level = 'ERROR'
82
+ } else if (status >= 400) {
83
+ level = 'WARNING'
84
+ }
85
+
86
+ logger.log(level, request, { status }, store)
87
+ })
88
+ .onError(({ request, error,code, path,store,set }) => {
89
+ // logger.handleHttpError(request, error, store)
90
+
91
+ // ==========================================
92
+ // Phase 1: Transform (转换)
93
+ // ==========================================
94
+ let result = options.transform ? options.transform(error, { request, code }) : error
95
+
96
+ // ==========================================
97
+ // Phase 2: Normalization (规范化)
98
+ // ==========================================
99
+ // 统一转为 ProblemError 实例
100
+ const problem = normalizeToProblem(result, code, path, options.config?.error?.problemJson?.typeBaseUrl)
101
+
102
+ // ==========================================
103
+ // Phase 3: Logging (日志)
104
+ // ==========================================
105
+ // 调用上面改造后的函数,它现在只负责记录,不负责逻辑判断
106
+ logger.handleHttpError(request, problem, store, options)
107
+
108
+ // ==========================================
109
+ // Phase 4: Response (响应)
110
+ // ==========================================
111
+ // 统一设置 Header 和 Status
112
+ set.status = problem.status
113
+ set.headers['content-type'] = 'application/problem+json'
114
+
115
+ // 返回符合 RFC 标准的 JSON
116
+ return problem.toJSON()
117
+
118
+
119
+ })
120
+ // Ensure plugin lifecycle hooks (onRequest/onAfterHandle/onError) apply to the parent app.
121
+ .as('scoped') as unknown as Logixlysia
122
+ )
123
+ }
124
+
125
+ export type {
126
+ Logger,
127
+ LogixlysiaContext,
128
+ LogixlysiaStore,
129
+ LogLevel,
130
+ Options,
131
+ Pino,
132
+ StoreData,
133
+ Transport,
134
+ } from './interfaces'
135
+
136
+ export { HttpError } from './interfaces'
137
+ export { toProblemJson, formatProblemJsonLog } from './utils/handle-error'
138
+ export type { ProblemJson } from './utils/handle-error'
139
+
140
+ export default logixlysia
141
+
@@ -0,0 +1,136 @@
1
+ import type {
2
+ Logger as PinoLogger,
3
+ LoggerOptions as PinoLoggerOptions
4
+ } from 'pino'
5
+ import { ProblemError } from './Error/errors'
6
+ import { Code } from './Error/type'
7
+
8
+ export type Pino = PinoLogger<never, boolean>
9
+ export type Request = Request
10
+
11
+ export type LogLevel = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR'
12
+
13
+ export interface StoreData {
14
+ beforeTime: bigint
15
+ }
16
+
17
+ export interface LogixlysiaStore {
18
+ logger: Logger
19
+ pino: Pino
20
+ beforeTime?: bigint
21
+ [key: string]: unknown
22
+ }
23
+
24
+ export interface Transport {
25
+ log: (
26
+ level: LogLevel,
27
+ message: string,
28
+ meta?: Record<string, unknown>
29
+ ) => void | Promise<void>
30
+ }
31
+
32
+ export interface LogRotationConfig {
33
+ /**
34
+ * Max log file size before rotation, e.g. '10m', '5k', or a byte count.
35
+ */
36
+ maxSize?: string | number
37
+ /**
38
+ * Keep at most N files or keep files for a duration like '7d'.
39
+ */
40
+ maxFiles?: number | string
41
+ /**
42
+ * Rotate at a fixed interval, e.g. '1d', '12h'.
43
+ */
44
+ interval?: string
45
+ compress?: boolean
46
+ compression?: 'gzip'
47
+ }
48
+
49
+
50
+
51
+
52
+
53
+ export interface Options {
54
+ config?: {
55
+ showStartupMessage?: boolean
56
+ startupMessageFormat?: 'simple' | 'banner'
57
+ useColors?: boolean
58
+ ip?: boolean
59
+ timestamp?: {
60
+ translateTime?: string
61
+ }
62
+ customLogFormat?: string
63
+
64
+ // Outputs
65
+ transports?: Transport[]
66
+ useTransportsOnly?: boolean
67
+ disableInternalLogger?: boolean
68
+ disableFileLogging?: boolean
69
+ logFilePath?: string
70
+ logRotation?: LogRotationConfig
71
+
72
+ // Pino
73
+ pino?: (PinoLoggerOptions & { prettyPrint?: boolean }) | undefined
74
+
75
+ error?:{
76
+ problemJson?:{
77
+ typeBaseUrl?: string
78
+ }
79
+ }
80
+
81
+ }
82
+
83
+ transform?: (error: unknown, context: { request: Request; code: Code }) => ProblemError | unknown
84
+
85
+
86
+ }
87
+
88
+ export class HttpError extends Error {
89
+ readonly status: number
90
+
91
+ constructor(status: number, message: string) {
92
+ super(message)
93
+ this.status = status
94
+ }
95
+ }
96
+
97
+ export interface Logger {
98
+ pino: Pino
99
+ log: (
100
+ level: LogLevel,
101
+ request: Request,
102
+ data: Record<string, unknown>,
103
+ store: StoreData
104
+ ) => void
105
+ handleHttpError: (
106
+ request: Request,
107
+ error: ProblemError,
108
+ store: StoreData,
109
+ options: Options
110
+ ) => void
111
+ debug: (
112
+ request: Request,
113
+ message: string,
114
+ context?: Record<string, unknown>
115
+ ) => void
116
+ info: (
117
+ request: Request,
118
+ message: string,
119
+ context?: Record<string, unknown>
120
+ ) => void
121
+ warn: (
122
+ request: Request,
123
+ message: string,
124
+ context?: Record<string, unknown>
125
+ ) => void
126
+ error: (
127
+ request: Request,
128
+ message: string,
129
+ context?: Record<string, unknown>
130
+ ) => void
131
+ }
132
+
133
+ export interface LogixlysiaContext {
134
+ request: Request
135
+ store: LogixlysiaStore
136
+ }