@pori15/logixlysia 0.0.1 → 6.0.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.
@@ -1,122 +1,122 @@
1
- import { promises as fs } from 'node:fs'
2
- import { promisify } from 'node:util'
3
- import { gzip } from 'node:zlib'
4
- import type { LogRotationConfig } from '../interfaces'
5
- import {
6
- getRotatedFiles,
7
- parseRetention,
8
- parseSize,
9
- shouldRotateBySize
10
- } from '../utils/rotation'
11
-
12
- const gzipAsync = promisify(gzip)
13
-
14
- const pad2 = (value: number): string => String(value).padStart(2, '0')
15
-
16
- export const getRotatedFileName = (filePath: string, date: Date): string => {
17
- const yyyy = date.getFullYear()
18
- const mm = pad2(date.getMonth() + 1)
19
- const dd = pad2(date.getDate())
20
- const HH = pad2(date.getHours())
21
- const MM = pad2(date.getMinutes())
22
- const ss = pad2(date.getSeconds())
23
- return `${filePath}.${yyyy}-${mm}-${dd}-${HH}-${MM}-${ss}`
24
- }
25
-
26
- export const rotateFile = async (filePath: string): Promise<string> => {
27
- try {
28
- const stat = await fs.stat(filePath)
29
- if (stat.size === 0) {
30
- return ''
31
- }
32
- } catch {
33
- return ''
34
- }
35
-
36
- const rotated = getRotatedFileName(filePath, new Date())
37
- await fs.rename(filePath, rotated)
38
- return rotated
39
- }
40
-
41
- export const compressFile = async (filePath: string): Promise<void> => {
42
- const content = await fs.readFile(filePath)
43
- const compressed = await gzipAsync(content)
44
- await fs.writeFile(`${filePath}.gz`, compressed)
45
- await fs.rm(filePath, { force: true })
46
- }
47
-
48
- export const shouldRotate = async (
49
- filePath: string,
50
- config: LogRotationConfig
51
- ): Promise<boolean> => {
52
- if (config.maxSize === undefined) {
53
- return false
54
- }
55
- const maxSize = parseSize(config.maxSize)
56
- return await shouldRotateBySize(filePath, maxSize)
57
- }
58
-
59
- const cleanupByCount = async (
60
- filePath: string,
61
- maxFiles: number
62
- ): Promise<void> => {
63
- const rotated = await getRotatedFiles(filePath)
64
- if (rotated.length <= maxFiles) {
65
- return
66
- }
67
-
68
- const stats = await Promise.all(
69
- rotated.map(async p => ({ path: p, stat: await fs.stat(p) }))
70
- )
71
-
72
- stats.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs)
73
- const toDelete = stats.slice(maxFiles)
74
- await Promise.all(toDelete.map(({ path }) => fs.rm(path, { force: true })))
75
- }
76
-
77
- const cleanupByTime = async (
78
- filePath: string,
79
- maxAgeMs: number
80
- ): Promise<void> => {
81
- const rotated = await getRotatedFiles(filePath)
82
- if (rotated.length === 0) {
83
- return
84
- }
85
-
86
- const now = Date.now()
87
- const stats = await Promise.all(
88
- rotated.map(async p => ({ path: p, stat: await fs.stat(p) }))
89
- )
90
-
91
- const toDelete = stats.filter(({ stat }) => now - stat.mtimeMs > maxAgeMs)
92
- await Promise.all(toDelete.map(({ path }) => fs.rm(path, { force: true })))
93
- }
94
-
95
- export const performRotation = async (
96
- filePath: string,
97
- config: LogRotationConfig
98
- ): Promise<void> => {
99
- const rotated = await rotateFile(filePath)
100
- if (!rotated) {
101
- return
102
- }
103
-
104
- const shouldCompress = config.compress === true
105
- if (shouldCompress) {
106
- const algo = config.compression ?? 'gzip'
107
- if (algo === 'gzip') {
108
- await compressFile(rotated)
109
- }
110
- }
111
-
112
- if (config.maxFiles !== undefined) {
113
- const retention = parseRetention(config.maxFiles)
114
- if (retention.type === 'count') {
115
- await cleanupByCount(filePath, retention.value)
116
- } else {
117
- await cleanupByTime(filePath, retention.value)
118
- }
119
- }
120
-
121
- // Optional interval-based rotation cleanup (create interval directories / naming) is not required by tests.
122
- }
1
+ import { promises as fs } from 'node:fs'
2
+ import { promisify } from 'node:util'
3
+ import { gzip } from 'node:zlib'
4
+ import type { LogRotationConfig } from '../interfaces'
5
+ import {
6
+ getRotatedFiles,
7
+ parseRetention,
8
+ parseSize,
9
+ shouldRotateBySize
10
+ } from '../utils/rotation'
11
+
12
+ const gzipAsync = promisify(gzip)
13
+
14
+ const pad2 = (value: number): string => String(value).padStart(2, '0')
15
+
16
+ export const getRotatedFileName = (filePath: string, date: Date): string => {
17
+ const yyyy = date.getFullYear()
18
+ const mm = pad2(date.getMonth() + 1)
19
+ const dd = pad2(date.getDate())
20
+ const HH = pad2(date.getHours())
21
+ const MM = pad2(date.getMinutes())
22
+ const ss = pad2(date.getSeconds())
23
+ return `${filePath}.${yyyy}-${mm}-${dd}-${HH}-${MM}-${ss}`
24
+ }
25
+
26
+ export const rotateFile = async (filePath: string): Promise<string> => {
27
+ try {
28
+ const stat = await fs.stat(filePath)
29
+ if (stat.size === 0) {
30
+ return ''
31
+ }
32
+ } catch {
33
+ return ''
34
+ }
35
+
36
+ const rotated = getRotatedFileName(filePath, new Date())
37
+ await fs.rename(filePath, rotated)
38
+ return rotated
39
+ }
40
+
41
+ export const compressFile = async (filePath: string): Promise<void> => {
42
+ const content = await fs.readFile(filePath)
43
+ const compressed = await gzipAsync(content)
44
+ await fs.writeFile(`${filePath}.gz`, compressed)
45
+ await fs.rm(filePath, { force: true })
46
+ }
47
+
48
+ export const shouldRotate = async (
49
+ filePath: string,
50
+ config: LogRotationConfig
51
+ ): Promise<boolean> => {
52
+ if (config.maxSize === undefined) {
53
+ return false
54
+ }
55
+ const maxSize = parseSize(config.maxSize)
56
+ return await shouldRotateBySize(filePath, maxSize)
57
+ }
58
+
59
+ const cleanupByCount = async (
60
+ filePath: string,
61
+ maxFiles: number
62
+ ): Promise<void> => {
63
+ const rotated = await getRotatedFiles(filePath)
64
+ if (rotated.length <= maxFiles) {
65
+ return
66
+ }
67
+
68
+ const stats = await Promise.all(
69
+ rotated.map(async p => ({ path: p, stat: await fs.stat(p) }))
70
+ )
71
+
72
+ stats.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs)
73
+ const toDelete = stats.slice(maxFiles)
74
+ await Promise.all(toDelete.map(({ path }) => fs.rm(path, { force: true })))
75
+ }
76
+
77
+ const cleanupByTime = async (
78
+ filePath: string,
79
+ maxAgeMs: number
80
+ ): Promise<void> => {
81
+ const rotated = await getRotatedFiles(filePath)
82
+ if (rotated.length === 0) {
83
+ return
84
+ }
85
+
86
+ const now = Date.now()
87
+ const stats = await Promise.all(
88
+ rotated.map(async p => ({ path: p, stat: await fs.stat(p) }))
89
+ )
90
+
91
+ const toDelete = stats.filter(({ stat }) => now - stat.mtimeMs > maxAgeMs)
92
+ await Promise.all(toDelete.map(({ path }) => fs.rm(path, { force: true })))
93
+ }
94
+
95
+ export const performRotation = async (
96
+ filePath: string,
97
+ config: LogRotationConfig
98
+ ): Promise<void> => {
99
+ const rotated = await rotateFile(filePath)
100
+ if (!rotated) {
101
+ return
102
+ }
103
+
104
+ const shouldCompress = config.compress === true
105
+ if (shouldCompress) {
106
+ const algo = config.compression ?? 'gzip'
107
+ if (algo === 'gzip') {
108
+ await compressFile(rotated)
109
+ }
110
+ }
111
+
112
+ if (config.maxFiles !== undefined) {
113
+ const retention = parseRetention(config.maxFiles)
114
+ if (retention.type === 'count') {
115
+ await cleanupByCount(filePath, retention.value)
116
+ } else {
117
+ await cleanupByTime(filePath, retention.value)
118
+ }
119
+ }
120
+
121
+ // Optional interval-based rotation cleanup (create interval directories / naming) is not required by tests.
122
+ }
@@ -1,15 +1,13 @@
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
- }
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
+ return message
13
+ }
@@ -3,9 +3,8 @@
3
3
  * @see https://www.rfc-editor.org/rfc/rfc9457.html
4
4
  */
5
5
 
6
- import { ProblemError } from "../Error/errors";
7
- import { Code } from "../Error/type";
8
-
6
+ import { ProblemError } from '../Error/errors'
7
+ import type { Code } from '../Error/type'
9
8
 
10
9
  /**
11
10
  * 默认的状态码与标题映射表 (RFC 标准推荐)
@@ -18,62 +17,64 @@ const DEFAULT_TITLES: Record<number, string> = {
18
17
  409: 'Conflict',
19
18
  422: 'Unprocessable Entity',
20
19
  500: 'Internal Server Error',
21
- 503: 'Service Unavailable',
22
- };
20
+ 503: 'Service Unavailable'
21
+ }
23
22
 
24
23
  export const normalizeToProblem = (
25
24
  error: any,
26
25
  code: Code,
27
26
  path: string,
28
- typeBaseUrl: string = 'about:blank'
27
+ typeBaseUrl = 'about:blank'
29
28
  ): ProblemError => {
30
29
  // 1. 如果已经是 ProblemError,直接补充 instance 并返回
31
30
  if (error instanceof ProblemError) {
32
31
  // 如果没有 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
- );
32
+ return error.instance
33
+ ? error
34
+ : new ProblemError(
35
+ error.type,
36
+ error.title,
37
+ error.status,
38
+ error.detail,
39
+ path,
40
+ error.extensions
41
+ )
41
42
  }
42
43
 
43
44
  // 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> = {};
45
+ let status = 500
46
+ let title = 'Internal Server Error'
47
+ let detail = error instanceof Error ? error.message : String(error)
48
+ let extensions: Record<string, unknown> = {}
48
49
 
49
50
  // 3. 识别 Elysia 内置错误码并“对齐”标准
50
51
  switch (code) {
51
52
  case 'VALIDATION':
52
- status = 400;
53
- title = 'Validation Failed';
53
+ status = 400
54
+ title = 'Validation Failed'
54
55
  // 提取 Elysia 的校验细节
55
- extensions = { errors: error.all || [] };
56
- break;
56
+ extensions = { errors: error.all || [] }
57
+ break
57
58
  case 'NOT_FOUND':
58
- status = 404;
59
- title = 'Resource Not Found';
60
- break;
59
+ status = 404
60
+ title = 'Resource Not Found'
61
+ break
61
62
  case 'PARSE':
62
- status = 400;
63
- title = 'Invalid Payload';
64
- detail = 'The request body could not be parsed as valid JSON.';
65
- break;
63
+ status = 400
64
+ title = 'Invalid Payload'
65
+ detail = 'The request body could not be parsed as valid JSON.'
66
+ break
66
67
  case 'INVALID_COOKIE_SIGNATURE':
67
- status = 401;
68
- title = 'Invalid Credentials';
69
- break;
68
+ status = 401
69
+ title = 'Invalid Credentials'
70
+ break
70
71
  default:
71
72
  // 如果错误对象本身带有状态码(比如某些库抛出的)
72
73
  if (typeof error?.status === 'number') {
73
- status = error.status;
74
- title = DEFAULT_TITLES[status] || 'Unknown Error';
74
+ status = error.status
75
+ title = DEFAULT_TITLES[status] || 'Unknown Error'
75
76
  }
76
- break;
77
+ break
77
78
  }
78
79
 
79
80
  // 4. 构造并返回标准的 ProblemError
@@ -84,9 +85,8 @@ export const normalizeToProblem = (
84
85
  detail,
85
86
  path,
86
87
  extensions
87
- );
88
- };
89
-
88
+ )
89
+ }
90
90
 
91
91
  export interface ProblemJson {
92
92
  type?: string
@@ -116,9 +116,7 @@ export interface ProblemJsonOptions {
116
116
  format?: (error: unknown, request: Request) => ProblemJson | null
117
117
  }
118
118
 
119
- const isErrorWithStatus = (
120
- value: unknown
121
- ): value is { status: number } =>
119
+ const isErrorWithStatus = (value: unknown): value is { status: number } =>
122
120
  typeof value === 'object' &&
123
121
  value !== null &&
124
122
  'status' in value &&
@@ -251,9 +249,7 @@ export function formatProblemJsonLog(
251
249
  // 标题行
252
250
  const statusStr = problem.status.toString()
253
251
  const emoji = getStatusEmoji(problem.status)
254
- parts.push(
255
- `\n${emoji} [HTTP ${statusStr}] ${request.method} ${url.pathname}`
256
- )
252
+ parts.push(`\n${emoji} [HTTP ${statusStr}] ${request.method} ${url.pathname}`)
257
253
 
258
254
  // Problem JSON 内容
259
255
  parts.push(` Type: ${problem.type}`)
@@ -267,8 +263,7 @@ export function formatProblemJsonLog(
267
263
 
268
264
  // 扩展字段
269
265
  const extensions = Object.entries(problem).filter(
270
- ([key]) =>
271
- !['type', 'title', 'status', 'detail', 'instance'].includes(key)
266
+ ([key]) => !['type', 'title', 'status', 'detail', 'instance'].includes(key)
272
267
  )
273
268
  if (extensions.length > 0) {
274
269
  parts.push(' Extensions:')
@@ -1,91 +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
- }
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
+ }