返回 AiToEarn
config-editor.service.ts
根目录 / project / aitoearn-backend / libs / config-editor / src / config-editor.service.ts
1 import { readFile, writeFile } from 'node:fs/promises'
2 import { resolve } from 'node:path'
3 import { Injectable } from '@nestjs/common'
4 import { AppException, isZodDto, ResponseCode } from '@yikart/common'
5 import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'
6 import { z } from 'zod'
7 import { ConfigEditorConfig } from './config-editor.config'
8 import { ConfigFileFormat } from './config-editor.vo'
9
10 @Injectable()
11 export class ConfigEditorService {
12 constructor(
13 private readonly config: ConfigEditorConfig<unknown>,
14 ) {}
15
16 async getConfig() {
17 const configPath = resolve(process.cwd(), this.config.configPath)
18 const format = this.getConfigFileFormat(configPath)
19 const content = await this.readConfigFile(configPath)
20 const config = this.validateConfigValue(this.parseConfig(content, format))
21 return {
22 config,
23 format,
24 }
25 }
26
27 validateConfig(config: Record<string, unknown>) {
28 this.validateConfigValue(config)
29 }
30
31 async saveConfig(config: Record<string, unknown>) {
32 const configPath = resolve(process.cwd(), this.config.configPath)
33 const format = this.getConfigFileFormat(configPath)
34 const content = this.serializeConfig(this.validateConfigValue(config), format)
35 await this.writeConfigFile(configPath, content)
36 }
37
38 private getConfigFileFormat(filePath: string): ConfigFileFormat {
39 const lowerPath = filePath.toLowerCase()
40 if (lowerPath.endsWith('.json')) {
41 return ConfigFileFormat.Json
42 }
43 if (lowerPath.endsWith('.yaml') || lowerPath.endsWith('.yml')) {
44 return ConfigFileFormat.Yaml
45 }
46 throw new AppException(ResponseCode.ConfigEditorUnsupportedFormat)
47 }
48
49 private parseConfig(content: string, format: ConfigFileFormat) {
50 try {
51 return format === ConfigFileFormat.Json
52 ? JSON.parse(content)
53 : parseYaml(content)
54 }
55 catch (error) {
56 throw new AppException(
57 ResponseCode.ConfigEditorParseFailed,
58 error instanceof Error ? error.message : String(error),
59 )
60 }
61 }
62
63 private validateConfigValue(config: unknown): Record<string, unknown> {
64 const schema = isZodDto(this.config.schema) ? this.config.schema.schema : this.config.schema
65 if (!(schema instanceof z.ZodType)) {
66 throw new AppException(ResponseCode.ConfigEditorValidationFailed)
67 }
68
69 const result = schema.safeParse(config)
70 if (!result.success) {
71 throw new AppException(ResponseCode.ConfigEditorValidationFailed, z.prettifyError(result.error))
72 }
73 return result.data as Record<string, unknown>
74 }
75
76 private serializeConfig(config: Record<string, unknown>, format: ConfigFileFormat) {
77 if (format === ConfigFileFormat.Json) {
78 return `${JSON.stringify(config, null, 2)}\n`
79 }
80 return stringifyYaml(config)
81 }
82
83 private async readConfigFile(configPath: string) {
84 try {
85 return await readFile(configPath, 'utf-8')
86 }
87 catch (error) {
88 throw new AppException(
89 ResponseCode.ConfigEditorReadFailed,
90 error instanceof Error ? error.message : String(error),
91 )
92 }
93 }
94
95 private async writeConfigFile(configPath: string, content: string) {
96 try {
97 await writeFile(configPath, content, 'utf-8')
98 }
99 catch (error) {
100 throw new AppException(
101 ResponseCode.ConfigEditorWriteFailed,
102 error instanceof Error ? error.message : String(error),
103 )
104 }
105 }
106 }
107
107 lines TYPESCRIPT