@spfn/core 0.1.0-alpha.21 → 0.1.0-alpha.23
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/dist/db/index.js +237 -50
- package/dist/db/index.js.map +1 -1
- package/dist/index.js +280 -68
- package/dist/index.js.map +1 -1
- package/dist/scripts/index.js +992 -820
- package/dist/scripts/index.js.map +1 -1
- package/dist/server/index.js +280 -68
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/scripts/index.js
CHANGED
|
@@ -1,971 +1,1143 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
2
2
|
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
|
3
3
|
import postgres from 'postgres';
|
|
4
|
-
import { config } from 'dotenv';
|
|
5
4
|
import { fileURLToPath } from 'url';
|
|
6
5
|
import { dirname, join, resolve } from 'path';
|
|
6
|
+
import { config } from 'dotenv';
|
|
7
|
+
import { existsSync, mkdirSync, createWriteStream, readFileSync } from 'fs';
|
|
8
|
+
import pino from 'pino';
|
|
7
9
|
import { mkdir, writeFile, readdir, stat } from 'fs/promises';
|
|
8
10
|
import * as ts from 'typescript';
|
|
9
|
-
import { existsSync, mkdirSync, createWriteStream, readFileSync } from 'fs';
|
|
10
11
|
import { watch } from 'chokidar';
|
|
11
|
-
import pino from 'pino';
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
|
|
14
14
|
// src/scripts/migrate.ts
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await migrate(db, {
|
|
32
|
-
migrationsFolder: join(projectRoot, "drizzle")
|
|
33
|
-
});
|
|
34
|
-
console.log("\u2705 Migration completed successfully");
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.error("\u274C Migration failed:", error);
|
|
37
|
-
throw error;
|
|
38
|
-
} finally {
|
|
39
|
-
await migrationConnection.end();
|
|
40
|
-
console.log("\u{1F50C} Database connection closed");
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
runMigrations().then(() => {
|
|
44
|
-
console.log("\u{1F389} All migrations applied");
|
|
45
|
-
process.exit(0);
|
|
46
|
-
}).catch((error) => {
|
|
47
|
-
console.error("\u{1F4A5} Migration process failed:", error);
|
|
48
|
-
process.exit(1);
|
|
49
|
-
});
|
|
50
|
-
async function scanContracts(routesDir) {
|
|
51
|
-
const contractFiles = await scanContractFiles(routesDir);
|
|
52
|
-
const mappings = [];
|
|
53
|
-
for (let i = 0; i < contractFiles.length; i++) {
|
|
54
|
-
const filePath = contractFiles[i];
|
|
55
|
-
const exports = extractContractExports(filePath);
|
|
56
|
-
const basePath = getBasePathFromFile(filePath, routesDir);
|
|
57
|
-
for (let j = 0; j < exports.length; j++) {
|
|
58
|
-
const contractExport = exports[j];
|
|
59
|
-
const fullPath = combinePaths(basePath, contractExport.path);
|
|
60
|
-
mappings.push({
|
|
61
|
-
method: contractExport.method,
|
|
62
|
-
path: fullPath,
|
|
63
|
-
contractName: contractExport.name,
|
|
64
|
-
contractImportPath: getImportPathFromRoutes(filePath, routesDir),
|
|
65
|
-
routeFile: "",
|
|
66
|
-
// Not needed anymore
|
|
67
|
-
contractFile: filePath
|
|
15
|
+
var PinoAdapter = class _PinoAdapter {
|
|
16
|
+
logger;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
19
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
20
|
+
const fileLoggingEnabled = process.env.LOGGER_FILE_ENABLED === "true";
|
|
21
|
+
const targets = [];
|
|
22
|
+
if (!isProduction && isDevelopment) {
|
|
23
|
+
targets.push({
|
|
24
|
+
target: "pino-pretty",
|
|
25
|
+
level: "debug",
|
|
26
|
+
options: {
|
|
27
|
+
colorize: true,
|
|
28
|
+
translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l",
|
|
29
|
+
ignore: "pid,hostname"
|
|
30
|
+
}
|
|
68
31
|
});
|
|
69
32
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
files.push(fullPath);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
} catch (error) {
|
|
87
|
-
}
|
|
88
|
-
return files;
|
|
89
|
-
}
|
|
90
|
-
function extractContractExports(filePath) {
|
|
91
|
-
const sourceCode = readFileSync(filePath, "utf-8");
|
|
92
|
-
const sourceFile = ts.createSourceFile(
|
|
93
|
-
filePath,
|
|
94
|
-
sourceCode,
|
|
95
|
-
ts.ScriptTarget.Latest,
|
|
96
|
-
true
|
|
97
|
-
);
|
|
98
|
-
const exports = [];
|
|
99
|
-
function visit(node) {
|
|
100
|
-
if (ts.isVariableStatement(node)) {
|
|
101
|
-
const hasExport = node.modifiers?.some(
|
|
102
|
-
(m) => m.kind === ts.SyntaxKind.ExportKeyword
|
|
103
|
-
);
|
|
104
|
-
if (hasExport && node.declarationList.declarations.length > 0) {
|
|
105
|
-
const declaration = node.declarationList.declarations[0];
|
|
106
|
-
if (ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) {
|
|
107
|
-
const name = declaration.name.text;
|
|
108
|
-
if (isContractName(name)) {
|
|
109
|
-
const contractData = extractContractData(declaration.initializer);
|
|
110
|
-
if (contractData.method && contractData.path) {
|
|
111
|
-
exports.push({
|
|
112
|
-
name,
|
|
113
|
-
method: contractData.method,
|
|
114
|
-
path: contractData.path
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
}
|
|
33
|
+
if (fileLoggingEnabled && isProduction) {
|
|
34
|
+
const logDir = process.env.LOG_DIR || "./logs";
|
|
35
|
+
const maxFileSize = process.env.LOG_MAX_FILE_SIZE || "10M";
|
|
36
|
+
const maxFiles = parseInt(process.env.LOG_MAX_FILES || "10", 10);
|
|
37
|
+
targets.push({
|
|
38
|
+
target: "pino-roll",
|
|
39
|
+
level: "info",
|
|
40
|
+
options: {
|
|
41
|
+
file: `${logDir}/app.log`,
|
|
42
|
+
frequency: "daily",
|
|
43
|
+
size: maxFileSize,
|
|
44
|
+
limit: { count: maxFiles },
|
|
45
|
+
mkdir: true
|
|
118
46
|
}
|
|
119
|
-
}
|
|
47
|
+
});
|
|
120
48
|
}
|
|
121
|
-
|
|
49
|
+
this.logger = pino({
|
|
50
|
+
level: config.level,
|
|
51
|
+
// Transport 설정 (targets가 있으면 사용, 없으면 기본 stdout)
|
|
52
|
+
transport: targets.length > 0 ? { targets } : void 0,
|
|
53
|
+
// 기본 필드
|
|
54
|
+
base: config.module ? { module: config.module } : void 0
|
|
55
|
+
});
|
|
122
56
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const result = {};
|
|
128
|
-
for (let i = 0; i < objectLiteral.properties.length; i++) {
|
|
129
|
-
const prop = objectLiteral.properties[i];
|
|
130
|
-
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
131
|
-
const propName = prop.name.text;
|
|
132
|
-
if (propName === "method") {
|
|
133
|
-
let value;
|
|
134
|
-
if (ts.isStringLiteral(prop.initializer)) {
|
|
135
|
-
value = prop.initializer.text;
|
|
136
|
-
} else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
|
|
137
|
-
value = prop.initializer.expression.text;
|
|
138
|
-
}
|
|
139
|
-
if (value) result.method = value;
|
|
140
|
-
} else if (propName === "path") {
|
|
141
|
-
let value;
|
|
142
|
-
if (ts.isStringLiteral(prop.initializer)) {
|
|
143
|
-
value = prop.initializer.text;
|
|
144
|
-
} else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
|
|
145
|
-
value = prop.initializer.expression.text;
|
|
146
|
-
}
|
|
147
|
-
if (value) result.path = value;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
57
|
+
child(module) {
|
|
58
|
+
const childLogger = new _PinoAdapter({ level: this.logger.level, module });
|
|
59
|
+
childLogger.logger = this.logger.child({ module });
|
|
60
|
+
return childLogger;
|
|
150
61
|
}
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
function isContractName(name) {
|
|
154
|
-
return name.indexOf("Contract") !== -1 || name.indexOf("contract") !== -1 || name.endsWith("Schema") || name.endsWith("schema");
|
|
155
|
-
}
|
|
156
|
-
function getBasePathFromFile(filePath, routesDir) {
|
|
157
|
-
let relativePath = filePath.replace(routesDir, "");
|
|
158
|
-
if (relativePath.startsWith("/")) {
|
|
159
|
-
relativePath = relativePath.slice(1);
|
|
62
|
+
debug(message, context) {
|
|
63
|
+
this.logger.debug(context || {}, message);
|
|
160
64
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return "/";
|
|
65
|
+
info(message, context) {
|
|
66
|
+
this.logger.info(context || {}, message);
|
|
164
67
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const seg = segments[i];
|
|
169
|
-
if (seg === "index") {
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
if (seg.startsWith("[") && seg.endsWith("]")) {
|
|
173
|
-
transformed.push(":" + seg.slice(1, -1));
|
|
68
|
+
warn(message, errorOrContext, context) {
|
|
69
|
+
if (errorOrContext instanceof Error) {
|
|
70
|
+
this.logger.warn({ err: errorOrContext, ...context }, message);
|
|
174
71
|
} else {
|
|
175
|
-
|
|
72
|
+
this.logger.warn(errorOrContext || {}, message);
|
|
176
73
|
}
|
|
177
74
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
basePath = basePath || "/";
|
|
185
|
-
contractPath = contractPath || "/";
|
|
186
|
-
if (basePath.endsWith("/") && basePath !== "/") {
|
|
187
|
-
basePath = basePath.slice(0, -1);
|
|
75
|
+
error(message, errorOrContext, context) {
|
|
76
|
+
if (errorOrContext instanceof Error) {
|
|
77
|
+
this.logger.error({ err: errorOrContext, ...context }, message);
|
|
78
|
+
} else {
|
|
79
|
+
this.logger.error(errorOrContext || {}, message);
|
|
80
|
+
}
|
|
188
81
|
}
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
82
|
+
fatal(message, errorOrContext, context) {
|
|
83
|
+
if (errorOrContext instanceof Error) {
|
|
84
|
+
this.logger.fatal({ err: errorOrContext, ...context }, message);
|
|
85
|
+
} else {
|
|
86
|
+
this.logger.fatal(errorOrContext || {}, message);
|
|
192
87
|
}
|
|
193
|
-
return basePath + contractPath;
|
|
194
88
|
}
|
|
195
|
-
|
|
196
|
-
return basePath;
|
|
89
|
+
async close() {
|
|
197
90
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/logger/logger.ts
|
|
94
|
+
var Logger = class _Logger {
|
|
95
|
+
config;
|
|
96
|
+
module;
|
|
97
|
+
constructor(config) {
|
|
98
|
+
this.config = config;
|
|
99
|
+
this.module = config.module;
|
|
204
100
|
}
|
|
205
|
-
|
|
206
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Get current log level
|
|
103
|
+
*/
|
|
104
|
+
get level() {
|
|
105
|
+
return this.config.level;
|
|
207
106
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Create child logger (per module)
|
|
109
|
+
*/
|
|
110
|
+
child(module) {
|
|
111
|
+
return new _Logger({
|
|
112
|
+
...this.config,
|
|
113
|
+
module
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Debug log
|
|
118
|
+
*/
|
|
119
|
+
debug(message, context) {
|
|
120
|
+
this.log("debug", message, void 0, context);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Info log
|
|
124
|
+
*/
|
|
125
|
+
info(message, context) {
|
|
126
|
+
this.log("info", message, void 0, context);
|
|
127
|
+
}
|
|
128
|
+
warn(message, errorOrContext, context) {
|
|
129
|
+
if (errorOrContext instanceof Error) {
|
|
130
|
+
this.log("warn", message, errorOrContext, context);
|
|
131
|
+
} else {
|
|
132
|
+
this.log("warn", message, void 0, errorOrContext);
|
|
219
133
|
}
|
|
220
|
-
grouped[resource].push(mapping);
|
|
221
134
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
for (let i = 0; i < segments.length; i++) {
|
|
228
|
-
const seg = segments[i];
|
|
229
|
-
if (!seg.startsWith(":")) {
|
|
230
|
-
staticSegments.push(seg);
|
|
135
|
+
error(message, errorOrContext, context) {
|
|
136
|
+
if (errorOrContext instanceof Error) {
|
|
137
|
+
this.log("error", message, errorOrContext, context);
|
|
138
|
+
} else {
|
|
139
|
+
this.log("error", message, void 0, errorOrContext);
|
|
231
140
|
}
|
|
232
141
|
}
|
|
233
|
-
|
|
234
|
-
|
|
142
|
+
fatal(message, errorOrContext, context) {
|
|
143
|
+
if (errorOrContext instanceof Error) {
|
|
144
|
+
this.log("fatal", message, errorOrContext, context);
|
|
145
|
+
} else {
|
|
146
|
+
this.log("fatal", message, void 0, errorOrContext);
|
|
147
|
+
}
|
|
235
148
|
}
|
|
236
|
-
|
|
237
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Log processing (internal)
|
|
151
|
+
*/
|
|
152
|
+
log(level, message, error, context) {
|
|
153
|
+
const metadata = {
|
|
154
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
155
|
+
level,
|
|
156
|
+
message,
|
|
157
|
+
module: this.module,
|
|
158
|
+
error,
|
|
159
|
+
context
|
|
160
|
+
};
|
|
161
|
+
this.processTransports(metadata);
|
|
238
162
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Process Transports
|
|
165
|
+
*/
|
|
166
|
+
processTransports(metadata) {
|
|
167
|
+
const promises = this.config.transports.filter((transport) => transport.enabled).map((transport) => this.safeTransportLog(transport, metadata));
|
|
168
|
+
Promise.all(promises).catch((error) => {
|
|
169
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
170
|
+
process.stderr.write(`[Logger] Transport error: ${errorMessage}
|
|
171
|
+
`);
|
|
172
|
+
});
|
|
243
173
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Transport log (error-safe)
|
|
176
|
+
*/
|
|
177
|
+
async safeTransportLog(transport, metadata) {
|
|
178
|
+
try {
|
|
179
|
+
await transport.log(metadata);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
182
|
+
process.stderr.write(`[Logger] Transport "${transport.name}" failed: ${errorMessage}
|
|
183
|
+
`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Close all Transports
|
|
188
|
+
*/
|
|
189
|
+
async close() {
|
|
190
|
+
const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
|
|
191
|
+
await Promise.all(closePromises);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/logger/types.ts
|
|
196
|
+
var LOG_LEVEL_PRIORITY = {
|
|
197
|
+
debug: 0,
|
|
198
|
+
info: 1,
|
|
199
|
+
warn: 2,
|
|
200
|
+
error: 3,
|
|
201
|
+
fatal: 4
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/logger/formatters.ts
|
|
205
|
+
var COLORS = {
|
|
206
|
+
reset: "\x1B[0m",
|
|
207
|
+
bright: "\x1B[1m",
|
|
208
|
+
dim: "\x1B[2m",
|
|
209
|
+
// 로그 레벨 컬러
|
|
210
|
+
debug: "\x1B[36m",
|
|
211
|
+
// cyan
|
|
212
|
+
info: "\x1B[32m",
|
|
213
|
+
// green
|
|
214
|
+
warn: "\x1B[33m",
|
|
215
|
+
// yellow
|
|
216
|
+
error: "\x1B[31m",
|
|
217
|
+
// red
|
|
218
|
+
fatal: "\x1B[35m",
|
|
219
|
+
// magenta
|
|
220
|
+
// 추가 컬러
|
|
221
|
+
gray: "\x1B[90m"
|
|
222
|
+
};
|
|
223
|
+
function colorizeLevel(level) {
|
|
224
|
+
const color = COLORS[level];
|
|
225
|
+
const levelStr = level.toUpperCase().padEnd(5);
|
|
226
|
+
return `${color}${levelStr}${COLORS.reset}`;
|
|
262
227
|
}
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
code += generateHeader();
|
|
266
|
-
code += generateImports(mappings, options);
|
|
267
|
-
code += generateApiObject(grouped, options);
|
|
268
|
-
code += generateFooter();
|
|
269
|
-
return code;
|
|
228
|
+
function formatTimestamp(date) {
|
|
229
|
+
return date.toISOString();
|
|
270
230
|
}
|
|
271
|
-
function
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
`;
|
|
231
|
+
function formatTimestampHuman(date) {
|
|
232
|
+
const year = date.getFullYear();
|
|
233
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
234
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
235
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
236
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
237
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
238
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
239
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
|
|
282
240
|
}
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
`;
|
|
241
|
+
function formatError(error) {
|
|
242
|
+
const lines = [];
|
|
243
|
+
lines.push(`${error.name}: ${error.message}`);
|
|
244
|
+
if (error.stack) {
|
|
245
|
+
const stackLines = error.stack.split("\n").slice(1);
|
|
246
|
+
lines.push(...stackLines);
|
|
290
247
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
code += `import { ${contracts.join(", ")} } from '${importPath}';
|
|
299
|
-
`;
|
|
248
|
+
return lines.join("\n");
|
|
249
|
+
}
|
|
250
|
+
function formatContext(context) {
|
|
251
|
+
try {
|
|
252
|
+
return JSON.stringify(context, null, 2);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return "[Context serialization failed]";
|
|
300
255
|
}
|
|
301
|
-
code += `
|
|
302
|
-
`;
|
|
303
|
-
return code;
|
|
304
256
|
}
|
|
305
|
-
function
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
257
|
+
function formatConsole(metadata, colorize = true) {
|
|
258
|
+
const parts = [];
|
|
259
|
+
const timestamp = formatTimestampHuman(metadata.timestamp);
|
|
260
|
+
if (colorize) {
|
|
261
|
+
parts.push(`${COLORS.gray}${timestamp}${COLORS.reset}`);
|
|
262
|
+
} else {
|
|
263
|
+
parts.push(timestamp);
|
|
264
|
+
}
|
|
265
|
+
if (colorize) {
|
|
266
|
+
parts.push(colorizeLevel(metadata.level));
|
|
267
|
+
} else {
|
|
268
|
+
parts.push(metadata.level.toUpperCase().padEnd(5));
|
|
269
|
+
}
|
|
270
|
+
if (metadata.module) {
|
|
271
|
+
if (colorize) {
|
|
272
|
+
parts.push(`${COLORS.dim}[${metadata.module}]${COLORS.reset}`);
|
|
273
|
+
} else {
|
|
274
|
+
parts.push(`[${metadata.module}]`);
|
|
312
275
|
}
|
|
313
|
-
groups[path].add(mapping.contractName);
|
|
314
276
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
result[key] = Array.from(groups[key]);
|
|
277
|
+
parts.push(metadata.message);
|
|
278
|
+
let output = parts.join(" ");
|
|
279
|
+
if (metadata.context && Object.keys(metadata.context).length > 0) {
|
|
280
|
+
output += "\n" + formatContext(metadata.context);
|
|
320
281
|
}
|
|
321
|
-
|
|
282
|
+
if (metadata.error) {
|
|
283
|
+
output += "\n" + formatError(metadata.error);
|
|
284
|
+
}
|
|
285
|
+
return output;
|
|
322
286
|
}
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
287
|
+
function formatJSON(metadata) {
|
|
288
|
+
const obj = {
|
|
289
|
+
timestamp: formatTimestamp(metadata.timestamp),
|
|
290
|
+
level: metadata.level,
|
|
291
|
+
message: metadata.message
|
|
292
|
+
};
|
|
293
|
+
if (metadata.module) {
|
|
294
|
+
obj.module = metadata.module;
|
|
295
|
+
}
|
|
296
|
+
if (metadata.context) {
|
|
297
|
+
obj.context = metadata.context;
|
|
298
|
+
}
|
|
299
|
+
if (metadata.error) {
|
|
300
|
+
obj.error = {
|
|
301
|
+
name: metadata.error.name,
|
|
302
|
+
message: metadata.error.message,
|
|
303
|
+
stack: metadata.error.stack
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return JSON.stringify(obj);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/logger/transports/console.ts
|
|
310
|
+
var ConsoleTransport = class {
|
|
311
|
+
name = "console";
|
|
312
|
+
level;
|
|
313
|
+
enabled;
|
|
314
|
+
colorize;
|
|
315
|
+
constructor(config) {
|
|
316
|
+
this.level = config.level;
|
|
317
|
+
this.enabled = config.enabled;
|
|
318
|
+
this.colorize = config.colorize ?? true;
|
|
319
|
+
}
|
|
320
|
+
async log(metadata) {
|
|
321
|
+
if (!this.enabled) {
|
|
322
|
+
return;
|
|
339
323
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
324
|
+
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const message = formatConsole(metadata, this.colorize);
|
|
328
|
+
if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
|
|
329
|
+
console.error(message);
|
|
330
|
+
} else {
|
|
331
|
+
console.log(message);
|
|
343
332
|
}
|
|
344
|
-
code += `
|
|
345
|
-
`;
|
|
346
333
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
334
|
+
};
|
|
335
|
+
var FileTransport = class {
|
|
336
|
+
name = "file";
|
|
337
|
+
level;
|
|
338
|
+
enabled;
|
|
339
|
+
logDir;
|
|
340
|
+
currentStream = null;
|
|
341
|
+
currentFilename = null;
|
|
342
|
+
constructor(config) {
|
|
343
|
+
this.level = config.level;
|
|
344
|
+
this.enabled = config.enabled;
|
|
345
|
+
this.logDir = config.logDir;
|
|
346
|
+
if (!existsSync(this.logDir)) {
|
|
347
|
+
mkdirSync(this.logDir, { recursive: true });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async log(metadata) {
|
|
351
|
+
if (!this.enabled) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const message = formatJSON(metadata);
|
|
358
|
+
const filename = this.getLogFilename(metadata.timestamp);
|
|
359
|
+
if (this.currentFilename !== filename) {
|
|
360
|
+
await this.rotateStream(filename);
|
|
361
|
+
}
|
|
362
|
+
if (this.currentStream) {
|
|
363
|
+
return new Promise((resolve2, reject) => {
|
|
364
|
+
this.currentStream.write(message + "\n", "utf-8", (error) => {
|
|
365
|
+
if (error) {
|
|
366
|
+
process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
|
|
367
|
+
`);
|
|
368
|
+
reject(error);
|
|
369
|
+
} else {
|
|
370
|
+
resolve2();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
}
|
|
365
375
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
376
|
+
/**
|
|
377
|
+
* 스트림 교체 (날짜 변경 시)
|
|
378
|
+
*/
|
|
379
|
+
async rotateStream(filename) {
|
|
380
|
+
if (this.currentStream) {
|
|
381
|
+
await this.closeStream();
|
|
382
|
+
}
|
|
383
|
+
const filepath = join(this.logDir, filename);
|
|
384
|
+
this.currentStream = createWriteStream(filepath, {
|
|
385
|
+
flags: "a",
|
|
386
|
+
// append mode
|
|
387
|
+
encoding: "utf-8"
|
|
388
|
+
});
|
|
389
|
+
this.currentFilename = filename;
|
|
390
|
+
this.currentStream.on("error", (error) => {
|
|
391
|
+
process.stderr.write(`[FileTransport] Stream error: ${error.message}
|
|
392
|
+
`);
|
|
393
|
+
this.currentStream = null;
|
|
394
|
+
this.currentFilename = null;
|
|
395
|
+
});
|
|
370
396
|
}
|
|
371
|
-
|
|
372
|
-
|
|
397
|
+
/**
|
|
398
|
+
* 현재 스트림 닫기
|
|
399
|
+
*/
|
|
400
|
+
async closeStream() {
|
|
401
|
+
if (!this.currentStream) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
return new Promise((resolve2, reject) => {
|
|
405
|
+
this.currentStream.end((error) => {
|
|
406
|
+
if (error) {
|
|
407
|
+
reject(error);
|
|
408
|
+
} else {
|
|
409
|
+
this.currentStream = null;
|
|
410
|
+
this.currentFilename = null;
|
|
411
|
+
resolve2();
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
});
|
|
373
415
|
}
|
|
374
|
-
|
|
375
|
-
|
|
416
|
+
/**
|
|
417
|
+
* 날짜별 로그 파일명 생성
|
|
418
|
+
*/
|
|
419
|
+
getLogFilename(date) {
|
|
420
|
+
const year = date.getFullYear();
|
|
421
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
422
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
423
|
+
return `${year}-${month}-${day}.log`;
|
|
376
424
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (params.length > 0) {
|
|
380
|
-
code += `options`;
|
|
381
|
-
} else {
|
|
382
|
-
code += `{}`;
|
|
425
|
+
async close() {
|
|
426
|
+
await this.closeStream();
|
|
383
427
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
return "list";
|
|
393
|
-
}
|
|
394
|
-
if (method === "post") {
|
|
395
|
-
return "create";
|
|
396
|
-
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/logger/config.ts
|
|
431
|
+
function getDefaultLogLevel() {
|
|
432
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
433
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
434
|
+
if (isDevelopment) {
|
|
435
|
+
return "debug";
|
|
397
436
|
}
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
return "getById";
|
|
401
|
-
}
|
|
402
|
-
if (method === "put" || method === "patch") {
|
|
403
|
-
return "update";
|
|
404
|
-
}
|
|
405
|
-
if (method === "delete") {
|
|
406
|
-
return "delete";
|
|
407
|
-
}
|
|
437
|
+
if (isProduction) {
|
|
438
|
+
return "info";
|
|
408
439
|
}
|
|
409
|
-
return
|
|
440
|
+
return "warn";
|
|
410
441
|
}
|
|
411
|
-
function
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
* import { client } from './api';
|
|
420
|
-
* import { createAuthInterceptor } from '@spfn/auth/nextjs';
|
|
421
|
-
* import { NextJSCookieProvider } from '@spfn/auth/nextjs';
|
|
422
|
-
*
|
|
423
|
-
* client.use(createAuthInterceptor({
|
|
424
|
-
* cookieProvider: new NextJSCookieProvider(),
|
|
425
|
-
* encryptionKey: process.env.ENCRYPTION_KEY!
|
|
426
|
-
* }));
|
|
427
|
-
* \`\`\`
|
|
428
|
-
*/
|
|
429
|
-
export { client };
|
|
430
|
-
`;
|
|
442
|
+
function getConsoleConfig() {
|
|
443
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
444
|
+
return {
|
|
445
|
+
level: "debug",
|
|
446
|
+
enabled: true,
|
|
447
|
+
colorize: !isProduction
|
|
448
|
+
// Dev: colored output, Production: plain text
|
|
449
|
+
};
|
|
431
450
|
}
|
|
432
|
-
function
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
451
|
+
function getFileConfig() {
|
|
452
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
453
|
+
return {
|
|
454
|
+
level: "info",
|
|
455
|
+
enabled: isProduction,
|
|
456
|
+
// File logging in production only
|
|
457
|
+
logDir: process.env.LOG_DIR || "./logs",
|
|
458
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
459
|
+
// 10MB
|
|
460
|
+
maxFiles: 10
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/logger/adapters/custom.ts
|
|
465
|
+
function initializeTransports() {
|
|
466
|
+
const transports = [];
|
|
467
|
+
const consoleConfig = getConsoleConfig();
|
|
468
|
+
transports.push(new ConsoleTransport(consoleConfig));
|
|
469
|
+
const fileConfig = getFileConfig();
|
|
470
|
+
if (fileConfig.enabled) {
|
|
471
|
+
transports.push(new FileTransport(fileConfig));
|
|
438
472
|
}
|
|
439
|
-
return
|
|
473
|
+
return transports;
|
|
440
474
|
}
|
|
441
|
-
var
|
|
475
|
+
var CustomAdapter = class _CustomAdapter {
|
|
442
476
|
logger;
|
|
443
|
-
constructor(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (!isProduction && isDevelopment) {
|
|
449
|
-
targets.push({
|
|
450
|
-
target: "pino-pretty",
|
|
451
|
-
level: "debug",
|
|
452
|
-
options: {
|
|
453
|
-
colorize: true,
|
|
454
|
-
translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l",
|
|
455
|
-
ignore: "pid,hostname"
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
if (fileLoggingEnabled && isProduction) {
|
|
460
|
-
const logDir = process.env.LOG_DIR || "./logs";
|
|
461
|
-
const maxFileSize = process.env.LOG_MAX_FILE_SIZE || "10M";
|
|
462
|
-
const maxFiles = parseInt(process.env.LOG_MAX_FILES || "10", 10);
|
|
463
|
-
targets.push({
|
|
464
|
-
target: "pino-roll",
|
|
465
|
-
level: "info",
|
|
466
|
-
options: {
|
|
467
|
-
file: `${logDir}/app.log`,
|
|
468
|
-
frequency: "daily",
|
|
469
|
-
size: maxFileSize,
|
|
470
|
-
limit: { count: maxFiles },
|
|
471
|
-
mkdir: true
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
this.logger = pino({
|
|
476
|
-
level: config2.level,
|
|
477
|
-
// Transport 설정 (targets가 있으면 사용, 없으면 기본 stdout)
|
|
478
|
-
transport: targets.length > 0 ? { targets } : void 0,
|
|
479
|
-
// 기본 필드
|
|
480
|
-
base: config2.module ? { module: config2.module } : void 0
|
|
477
|
+
constructor(config) {
|
|
478
|
+
this.logger = new Logger({
|
|
479
|
+
level: config.level,
|
|
480
|
+
module: config.module,
|
|
481
|
+
transports: initializeTransports()
|
|
481
482
|
});
|
|
482
483
|
}
|
|
483
484
|
child(module) {
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
return
|
|
485
|
+
const adapter = new _CustomAdapter({ level: this.logger.level, module });
|
|
486
|
+
adapter.logger = this.logger.child(module);
|
|
487
|
+
return adapter;
|
|
487
488
|
}
|
|
488
489
|
debug(message, context) {
|
|
489
|
-
this.logger.debug(
|
|
490
|
+
this.logger.debug(message, context);
|
|
490
491
|
}
|
|
491
492
|
info(message, context) {
|
|
492
|
-
this.logger.info(
|
|
493
|
+
this.logger.info(message, context);
|
|
493
494
|
}
|
|
494
495
|
warn(message, errorOrContext, context) {
|
|
495
496
|
if (errorOrContext instanceof Error) {
|
|
496
|
-
this.logger.warn(
|
|
497
|
+
this.logger.warn(message, errorOrContext, context);
|
|
497
498
|
} else {
|
|
498
|
-
this.logger.warn(
|
|
499
|
+
this.logger.warn(message, errorOrContext);
|
|
499
500
|
}
|
|
500
501
|
}
|
|
501
502
|
error(message, errorOrContext, context) {
|
|
502
503
|
if (errorOrContext instanceof Error) {
|
|
503
|
-
this.logger.error(
|
|
504
|
+
this.logger.error(message, errorOrContext, context);
|
|
504
505
|
} else {
|
|
505
|
-
this.logger.error(
|
|
506
|
+
this.logger.error(message, errorOrContext);
|
|
506
507
|
}
|
|
507
508
|
}
|
|
508
509
|
fatal(message, errorOrContext, context) {
|
|
509
510
|
if (errorOrContext instanceof Error) {
|
|
510
|
-
this.logger.fatal(
|
|
511
|
+
this.logger.fatal(message, errorOrContext, context);
|
|
511
512
|
} else {
|
|
512
|
-
this.logger.fatal(
|
|
513
|
+
this.logger.fatal(message, errorOrContext);
|
|
513
514
|
}
|
|
514
515
|
}
|
|
515
516
|
async close() {
|
|
517
|
+
await this.logger.close();
|
|
516
518
|
}
|
|
517
519
|
};
|
|
518
520
|
|
|
519
|
-
// src/logger/
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
521
|
+
// src/logger/adapter-factory.ts
|
|
522
|
+
function createAdapter(type) {
|
|
523
|
+
const level = getDefaultLogLevel();
|
|
524
|
+
switch (type) {
|
|
525
|
+
case "pino":
|
|
526
|
+
return new PinoAdapter({ level });
|
|
527
|
+
case "custom":
|
|
528
|
+
return new CustomAdapter({ level });
|
|
529
|
+
default:
|
|
530
|
+
return new PinoAdapter({ level });
|
|
526
531
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
return
|
|
532
|
+
}
|
|
533
|
+
function getAdapterType() {
|
|
534
|
+
const adapterEnv = process.env.LOGGER_ADAPTER;
|
|
535
|
+
if (adapterEnv === "custom" || adapterEnv === "pino") {
|
|
536
|
+
return adapterEnv;
|
|
537
|
+
}
|
|
538
|
+
return "pino";
|
|
539
|
+
}
|
|
540
|
+
var logger = createAdapter(getAdapterType());
|
|
541
|
+
|
|
542
|
+
// src/env/config.ts
|
|
543
|
+
var ENV_FILE_PRIORITY = [
|
|
544
|
+
".env",
|
|
545
|
+
// Base configuration (lowest priority)
|
|
546
|
+
".env.{NODE_ENV}",
|
|
547
|
+
// Environment-specific
|
|
548
|
+
".env.local",
|
|
549
|
+
// Local overrides
|
|
550
|
+
".env.{NODE_ENV}.local"
|
|
551
|
+
// Local environment-specific (highest priority)
|
|
552
|
+
];
|
|
553
|
+
var TEST_ONLY_FILES = [
|
|
554
|
+
".env.test",
|
|
555
|
+
".env.test.local"
|
|
556
|
+
];
|
|
557
|
+
|
|
558
|
+
// src/env/loader.ts
|
|
559
|
+
var envLogger = logger.child("environment");
|
|
560
|
+
var environmentLoaded = false;
|
|
561
|
+
var cachedLoadResult;
|
|
562
|
+
function buildFileList(basePath, nodeEnv) {
|
|
563
|
+
const files = [];
|
|
564
|
+
for (const pattern of ENV_FILE_PRIORITY) {
|
|
565
|
+
const fileName = pattern.replace("{NODE_ENV}", nodeEnv);
|
|
566
|
+
if (nodeEnv !== "test" && TEST_ONLY_FILES.includes(fileName)) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
files.push(join(basePath, fileName));
|
|
570
|
+
}
|
|
571
|
+
return files;
|
|
572
|
+
}
|
|
573
|
+
function loadSingleFile(filePath, debug) {
|
|
574
|
+
if (!existsSync(filePath)) {
|
|
575
|
+
if (debug) {
|
|
576
|
+
envLogger.debug("Environment file not found (optional)", {
|
|
577
|
+
path: filePath
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return { success: false, parsed: {}, error: "File not found" };
|
|
532
581
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
582
|
+
try {
|
|
583
|
+
const result = config({ path: filePath });
|
|
584
|
+
if (result.error) {
|
|
585
|
+
envLogger.warn("Failed to parse environment file", {
|
|
586
|
+
path: filePath,
|
|
587
|
+
error: result.error.message
|
|
588
|
+
});
|
|
589
|
+
return {
|
|
590
|
+
success: false,
|
|
591
|
+
parsed: {},
|
|
592
|
+
error: result.error.message
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
const parsed = result.parsed || {};
|
|
596
|
+
if (debug) {
|
|
597
|
+
envLogger.debug("Environment file loaded successfully", {
|
|
598
|
+
path: filePath,
|
|
599
|
+
variables: Object.keys(parsed),
|
|
600
|
+
count: Object.keys(parsed).length
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return { success: true, parsed };
|
|
604
|
+
} catch (error) {
|
|
605
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
606
|
+
envLogger.error("Error loading environment file", {
|
|
607
|
+
path: filePath,
|
|
608
|
+
error: message
|
|
540
609
|
});
|
|
610
|
+
return { success: false, parsed: {}, error: message };
|
|
541
611
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
612
|
+
}
|
|
613
|
+
function validateRequiredVars(required, debug) {
|
|
614
|
+
const missing = [];
|
|
615
|
+
for (const varName of required) {
|
|
616
|
+
if (!process.env[varName]) {
|
|
617
|
+
missing.push(varName);
|
|
618
|
+
}
|
|
547
619
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
620
|
+
if (missing.length > 0) {
|
|
621
|
+
const error = `Required environment variables missing: ${missing.join(", ")}`;
|
|
622
|
+
envLogger.error("Environment validation failed", {
|
|
623
|
+
missing,
|
|
624
|
+
required
|
|
625
|
+
});
|
|
626
|
+
throw new Error(error);
|
|
553
627
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
628
|
+
if (debug) {
|
|
629
|
+
envLogger.debug("Required environment variables validated", {
|
|
630
|
+
required,
|
|
631
|
+
allPresent: true
|
|
632
|
+
});
|
|
560
633
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
634
|
+
}
|
|
635
|
+
function loadEnvironment(options = {}) {
|
|
636
|
+
const {
|
|
637
|
+
basePath = process.cwd(),
|
|
638
|
+
customPaths = [],
|
|
639
|
+
debug = false,
|
|
640
|
+
nodeEnv = process.env.NODE_ENV || "development",
|
|
641
|
+
required = [],
|
|
642
|
+
useCache = true
|
|
643
|
+
} = options;
|
|
644
|
+
if (useCache && environmentLoaded && cachedLoadResult) {
|
|
645
|
+
if (debug) {
|
|
646
|
+
envLogger.debug("Returning cached environment", {
|
|
647
|
+
loaded: cachedLoadResult.loaded.length,
|
|
648
|
+
variables: Object.keys(cachedLoadResult.parsed).length
|
|
649
|
+
});
|
|
566
650
|
}
|
|
651
|
+
return cachedLoadResult;
|
|
652
|
+
}
|
|
653
|
+
if (debug) {
|
|
654
|
+
envLogger.debug("Loading environment variables", {
|
|
655
|
+
basePath,
|
|
656
|
+
nodeEnv,
|
|
657
|
+
customPaths,
|
|
658
|
+
required
|
|
659
|
+
});
|
|
567
660
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
661
|
+
const result = {
|
|
662
|
+
success: true,
|
|
663
|
+
loaded: [],
|
|
664
|
+
failed: [],
|
|
665
|
+
parsed: {},
|
|
666
|
+
warnings: []
|
|
667
|
+
};
|
|
668
|
+
const standardFiles = buildFileList(basePath, nodeEnv);
|
|
669
|
+
const allFiles = [...standardFiles, ...customPaths];
|
|
670
|
+
if (debug) {
|
|
671
|
+
envLogger.debug("Environment files to load", {
|
|
672
|
+
standardFiles,
|
|
673
|
+
customPaths,
|
|
674
|
+
total: allFiles.length
|
|
675
|
+
});
|
|
574
676
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
this.processTransports(metadata);
|
|
677
|
+
const reversedFiles = [...allFiles].reverse();
|
|
678
|
+
for (const filePath of reversedFiles) {
|
|
679
|
+
const fileResult = loadSingleFile(filePath, debug);
|
|
680
|
+
if (fileResult.success) {
|
|
681
|
+
result.loaded.push(filePath);
|
|
682
|
+
Object.assign(result.parsed, fileResult.parsed);
|
|
683
|
+
} else if (fileResult.error) {
|
|
684
|
+
result.failed.push({
|
|
685
|
+
path: filePath,
|
|
686
|
+
reason: fileResult.error
|
|
687
|
+
});
|
|
688
|
+
}
|
|
588
689
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
596
|
-
process.stderr.write(`[Logger] Transport error: ${errorMessage}
|
|
597
|
-
`);
|
|
690
|
+
if (debug || result.loaded.length > 0) {
|
|
691
|
+
envLogger.info("Environment loading complete", {
|
|
692
|
+
loaded: result.loaded.length,
|
|
693
|
+
failed: result.failed.length,
|
|
694
|
+
variables: Object.keys(result.parsed).length,
|
|
695
|
+
files: result.loaded
|
|
598
696
|
});
|
|
599
697
|
}
|
|
600
|
-
|
|
601
|
-
* Transport log (error-safe)
|
|
602
|
-
*/
|
|
603
|
-
async safeTransportLog(transport, metadata) {
|
|
698
|
+
if (required.length > 0) {
|
|
604
699
|
try {
|
|
605
|
-
|
|
700
|
+
validateRequiredVars(required, debug);
|
|
606
701
|
} catch (error) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
702
|
+
result.success = false;
|
|
703
|
+
result.errors = [
|
|
704
|
+
error instanceof Error ? error.message : "Validation failed"
|
|
705
|
+
];
|
|
706
|
+
throw error;
|
|
610
707
|
}
|
|
611
708
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const closePromises = this.config.transports.filter((transport) => transport.close).map((transport) => transport.close());
|
|
617
|
-
await Promise.all(closePromises);
|
|
618
|
-
}
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
// src/logger/types.ts
|
|
622
|
-
var LOG_LEVEL_PRIORITY = {
|
|
623
|
-
debug: 0,
|
|
624
|
-
info: 1,
|
|
625
|
-
warn: 2,
|
|
626
|
-
error: 3,
|
|
627
|
-
fatal: 4
|
|
628
|
-
};
|
|
709
|
+
environmentLoaded = true;
|
|
710
|
+
cachedLoadResult = result;
|
|
711
|
+
return result;
|
|
712
|
+
}
|
|
629
713
|
|
|
630
|
-
// src/
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
// green
|
|
640
|
-
warn: "\x1B[33m",
|
|
641
|
-
// yellow
|
|
642
|
-
error: "\x1B[31m",
|
|
643
|
-
// red
|
|
644
|
-
fatal: "\x1B[35m",
|
|
645
|
-
// magenta
|
|
646
|
-
// 추가 컬러
|
|
647
|
-
gray: "\x1B[90m"
|
|
648
|
-
};
|
|
649
|
-
function colorizeLevel(level) {
|
|
650
|
-
const color = COLORS[level];
|
|
651
|
-
const levelStr = level.toUpperCase().padEnd(5);
|
|
652
|
-
return `${color}${levelStr}${COLORS.reset}`;
|
|
714
|
+
// src/scripts/migrate.ts
|
|
715
|
+
loadEnvironment({ debug: true });
|
|
716
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
717
|
+
var __dirname = dirname(__filename);
|
|
718
|
+
var projectRoot = join(__dirname, "../../..");
|
|
719
|
+
var DATABASE_URL = process.env.DATABASE_URL;
|
|
720
|
+
if (!DATABASE_URL) {
|
|
721
|
+
console.error("\u274C DATABASE_URL environment variable is required");
|
|
722
|
+
process.exit(1);
|
|
653
723
|
}
|
|
654
|
-
function
|
|
655
|
-
|
|
724
|
+
async function runMigrations() {
|
|
725
|
+
console.log("\u{1F504} Starting database migration...");
|
|
726
|
+
console.log(`\u{1F4C2} Migrations folder: ${join(projectRoot, "drizzle")}`);
|
|
727
|
+
const migrationConnection = postgres(DATABASE_URL, { max: 1 });
|
|
728
|
+
const db = drizzle(migrationConnection);
|
|
729
|
+
try {
|
|
730
|
+
console.log("\u23F3 Applying migrations...");
|
|
731
|
+
await migrate(db, {
|
|
732
|
+
migrationsFolder: join(projectRoot, "drizzle")
|
|
733
|
+
});
|
|
734
|
+
console.log("\u2705 Migration completed successfully");
|
|
735
|
+
} catch (error) {
|
|
736
|
+
console.error("\u274C Migration failed:", error);
|
|
737
|
+
throw error;
|
|
738
|
+
} finally {
|
|
739
|
+
await migrationConnection.end();
|
|
740
|
+
console.log("\u{1F50C} Database connection closed");
|
|
741
|
+
}
|
|
656
742
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
743
|
+
runMigrations().then(() => {
|
|
744
|
+
console.log("\u{1F389} All migrations applied");
|
|
745
|
+
process.exit(0);
|
|
746
|
+
}).catch((error) => {
|
|
747
|
+
console.error("\u{1F4A5} Migration process failed:", error);
|
|
748
|
+
process.exit(1);
|
|
749
|
+
});
|
|
750
|
+
async function scanContracts(routesDir) {
|
|
751
|
+
const contractFiles = await scanContractFiles(routesDir);
|
|
752
|
+
const mappings = [];
|
|
753
|
+
for (let i = 0; i < contractFiles.length; i++) {
|
|
754
|
+
const filePath = contractFiles[i];
|
|
755
|
+
const exports = extractContractExports(filePath);
|
|
756
|
+
const basePath = getBasePathFromFile(filePath, routesDir);
|
|
757
|
+
for (let j = 0; j < exports.length; j++) {
|
|
758
|
+
const contractExport = exports[j];
|
|
759
|
+
const fullPath = combinePaths(basePath, contractExport.path);
|
|
760
|
+
mappings.push({
|
|
761
|
+
method: contractExport.method,
|
|
762
|
+
path: fullPath,
|
|
763
|
+
contractName: contractExport.name,
|
|
764
|
+
contractImportPath: getImportPathFromRoutes(filePath, routesDir),
|
|
765
|
+
routeFile: "",
|
|
766
|
+
// Not needed anymore
|
|
767
|
+
contractFile: filePath
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return mappings;
|
|
666
772
|
}
|
|
667
|
-
function
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
773
|
+
async function scanContractFiles(dir, files = []) {
|
|
774
|
+
try {
|
|
775
|
+
const entries = await readdir(dir);
|
|
776
|
+
for (let i = 0; i < entries.length; i++) {
|
|
777
|
+
const entry = entries[i];
|
|
778
|
+
const fullPath = join(dir, entry);
|
|
779
|
+
const fileStat = await stat(fullPath);
|
|
780
|
+
if (fileStat.isDirectory()) {
|
|
781
|
+
await scanContractFiles(fullPath, files);
|
|
782
|
+
} else if (entry === "contract.ts") {
|
|
783
|
+
files.push(fullPath);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
} catch (error) {
|
|
787
|
+
}
|
|
788
|
+
return files;
|
|
789
|
+
}
|
|
790
|
+
function extractContractExports(filePath) {
|
|
791
|
+
const sourceCode = readFileSync(filePath, "utf-8");
|
|
792
|
+
const sourceFile = ts.createSourceFile(
|
|
793
|
+
filePath,
|
|
794
|
+
sourceCode,
|
|
795
|
+
ts.ScriptTarget.Latest,
|
|
796
|
+
true
|
|
797
|
+
);
|
|
798
|
+
const exports = [];
|
|
799
|
+
function visit(node) {
|
|
800
|
+
if (ts.isVariableStatement(node)) {
|
|
801
|
+
const hasExport = node.modifiers?.some(
|
|
802
|
+
(m) => m.kind === ts.SyntaxKind.ExportKeyword
|
|
803
|
+
);
|
|
804
|
+
if (hasExport && node.declarationList.declarations.length > 0) {
|
|
805
|
+
const declaration = node.declarationList.declarations[0];
|
|
806
|
+
if (ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) {
|
|
807
|
+
const name = declaration.name.text;
|
|
808
|
+
if (isContractName(name)) {
|
|
809
|
+
const contractData = extractContractData(declaration.initializer);
|
|
810
|
+
if (contractData.method && contractData.path) {
|
|
811
|
+
exports.push({
|
|
812
|
+
name,
|
|
813
|
+
method: contractData.method,
|
|
814
|
+
path: contractData.path
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
ts.forEachChild(node, visit);
|
|
822
|
+
}
|
|
823
|
+
visit(sourceFile);
|
|
824
|
+
return exports;
|
|
825
|
+
}
|
|
826
|
+
function extractContractData(objectLiteral) {
|
|
827
|
+
const result = {};
|
|
828
|
+
for (let i = 0; i < objectLiteral.properties.length; i++) {
|
|
829
|
+
const prop = objectLiteral.properties[i];
|
|
830
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
831
|
+
const propName = prop.name.text;
|
|
832
|
+
if (propName === "method") {
|
|
833
|
+
let value;
|
|
834
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
835
|
+
value = prop.initializer.text;
|
|
836
|
+
} else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
|
|
837
|
+
value = prop.initializer.expression.text;
|
|
838
|
+
}
|
|
839
|
+
if (value) result.method = value;
|
|
840
|
+
} else if (propName === "path") {
|
|
841
|
+
let value;
|
|
842
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
843
|
+
value = prop.initializer.text;
|
|
844
|
+
} else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
|
|
845
|
+
value = prop.initializer.expression.text;
|
|
846
|
+
}
|
|
847
|
+
if (value) result.path = value;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
673
850
|
}
|
|
674
|
-
return
|
|
851
|
+
return result;
|
|
675
852
|
}
|
|
676
|
-
function
|
|
677
|
-
|
|
678
|
-
return JSON.stringify(context, null, 2);
|
|
679
|
-
} catch (error) {
|
|
680
|
-
return "[Context serialization failed]";
|
|
681
|
-
}
|
|
853
|
+
function isContractName(name) {
|
|
854
|
+
return name.indexOf("Contract") !== -1 || name.indexOf("contract") !== -1 || name.endsWith("Schema") || name.endsWith("schema");
|
|
682
855
|
}
|
|
683
|
-
function
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
parts.push(`${COLORS.gray}${timestamp}${COLORS.reset}`);
|
|
688
|
-
} else {
|
|
689
|
-
parts.push(timestamp);
|
|
856
|
+
function getBasePathFromFile(filePath, routesDir) {
|
|
857
|
+
let relativePath = filePath.replace(routesDir, "");
|
|
858
|
+
if (relativePath.startsWith("/")) {
|
|
859
|
+
relativePath = relativePath.slice(1);
|
|
690
860
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
parts.push(metadata.level.toUpperCase().padEnd(5));
|
|
861
|
+
relativePath = relativePath.replace("/contract.ts", "");
|
|
862
|
+
if (relativePath === "index" || relativePath === "") {
|
|
863
|
+
return "/";
|
|
695
864
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
865
|
+
const segments = relativePath.split("/");
|
|
866
|
+
const transformed = [];
|
|
867
|
+
for (let i = 0; i < segments.length; i++) {
|
|
868
|
+
const seg = segments[i];
|
|
869
|
+
if (seg === "index") {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (seg.startsWith("[") && seg.endsWith("]")) {
|
|
873
|
+
transformed.push(":" + seg.slice(1, -1));
|
|
699
874
|
} else {
|
|
700
|
-
|
|
875
|
+
transformed.push(seg);
|
|
701
876
|
}
|
|
702
877
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (metadata.context && Object.keys(metadata.context).length > 0) {
|
|
706
|
-
output += "\n" + formatContext(metadata.context);
|
|
707
|
-
}
|
|
708
|
-
if (metadata.error) {
|
|
709
|
-
output += "\n" + formatError(metadata.error);
|
|
878
|
+
if (transformed.length === 0) {
|
|
879
|
+
return "/";
|
|
710
880
|
}
|
|
711
|
-
return
|
|
881
|
+
return "/" + transformed.join("/");
|
|
712
882
|
}
|
|
713
|
-
function
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
};
|
|
719
|
-
if (metadata.module) {
|
|
720
|
-
obj.module = metadata.module;
|
|
883
|
+
function combinePaths(basePath, contractPath) {
|
|
884
|
+
basePath = basePath || "/";
|
|
885
|
+
contractPath = contractPath || "/";
|
|
886
|
+
if (basePath.endsWith("/") && basePath !== "/") {
|
|
887
|
+
basePath = basePath.slice(0, -1);
|
|
721
888
|
}
|
|
722
|
-
if (
|
|
723
|
-
|
|
889
|
+
if (contractPath.startsWith("/") && contractPath !== "/") {
|
|
890
|
+
if (basePath === "/") {
|
|
891
|
+
return contractPath;
|
|
892
|
+
}
|
|
893
|
+
return basePath + contractPath;
|
|
724
894
|
}
|
|
725
|
-
if (
|
|
726
|
-
|
|
727
|
-
name: metadata.error.name,
|
|
728
|
-
message: metadata.error.message,
|
|
729
|
-
stack: metadata.error.stack
|
|
730
|
-
};
|
|
895
|
+
if (contractPath === "/") {
|
|
896
|
+
return basePath;
|
|
731
897
|
}
|
|
732
|
-
return
|
|
898
|
+
return basePath + "/" + contractPath;
|
|
733
899
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
level;
|
|
739
|
-
enabled;
|
|
740
|
-
colorize;
|
|
741
|
-
constructor(config2) {
|
|
742
|
-
this.level = config2.level;
|
|
743
|
-
this.enabled = config2.enabled;
|
|
744
|
-
this.colorize = config2.colorize ?? true;
|
|
900
|
+
function getImportPathFromRoutes(filePath, routesDir) {
|
|
901
|
+
let relativePath = filePath.replace(routesDir, "");
|
|
902
|
+
if (relativePath.startsWith("/")) {
|
|
903
|
+
relativePath = relativePath.slice(1);
|
|
745
904
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
if (LOG_LEVEL_PRIORITY[metadata.level] < LOG_LEVEL_PRIORITY[this.level]) {
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
const message = formatConsole(metadata, this.colorize);
|
|
754
|
-
if (metadata.level === "warn" || metadata.level === "error" || metadata.level === "fatal") {
|
|
755
|
-
console.error(message);
|
|
756
|
-
} else {
|
|
757
|
-
console.log(message);
|
|
758
|
-
}
|
|
905
|
+
if (relativePath.endsWith(".ts")) {
|
|
906
|
+
relativePath = relativePath.slice(0, -3);
|
|
759
907
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
this.logDir = config2.logDir;
|
|
772
|
-
if (!existsSync(this.logDir)) {
|
|
773
|
-
mkdirSync(this.logDir, { recursive: true });
|
|
908
|
+
return "@/server/routes/" + relativePath;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/codegen/route-scanner.ts
|
|
912
|
+
function groupByResource(mappings) {
|
|
913
|
+
const grouped = {};
|
|
914
|
+
for (let i = 0; i < mappings.length; i++) {
|
|
915
|
+
const mapping = mappings[i];
|
|
916
|
+
const resource = extractResourceName(mapping.path);
|
|
917
|
+
if (!grouped[resource]) {
|
|
918
|
+
grouped[resource] = [];
|
|
774
919
|
}
|
|
920
|
+
grouped[resource].push(mapping);
|
|
775
921
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (this.currentFilename !== filename) {
|
|
786
|
-
await this.rotateStream(filename);
|
|
787
|
-
}
|
|
788
|
-
if (this.currentStream) {
|
|
789
|
-
return new Promise((resolve2, reject) => {
|
|
790
|
-
this.currentStream.write(message + "\n", "utf-8", (error) => {
|
|
791
|
-
if (error) {
|
|
792
|
-
process.stderr.write(`[FileTransport] Failed to write log: ${error.message}
|
|
793
|
-
`);
|
|
794
|
-
reject(error);
|
|
795
|
-
} else {
|
|
796
|
-
resolve2();
|
|
797
|
-
}
|
|
798
|
-
});
|
|
799
|
-
});
|
|
922
|
+
return grouped;
|
|
923
|
+
}
|
|
924
|
+
function extractResourceName(path) {
|
|
925
|
+
const segments = path.slice(1).split("/").filter((s) => s && s !== "*");
|
|
926
|
+
const staticSegments = [];
|
|
927
|
+
for (let i = 0; i < segments.length; i++) {
|
|
928
|
+
const seg = segments[i];
|
|
929
|
+
if (!seg.startsWith(":")) {
|
|
930
|
+
staticSegments.push(seg);
|
|
800
931
|
}
|
|
801
932
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
933
|
+
if (staticSegments.length === 0) {
|
|
934
|
+
return "root";
|
|
935
|
+
}
|
|
936
|
+
if (staticSegments.length === 1) {
|
|
937
|
+
return staticSegments[0];
|
|
938
|
+
}
|
|
939
|
+
const result = [staticSegments[0]];
|
|
940
|
+
for (let i = 1; i < staticSegments.length; i++) {
|
|
941
|
+
const seg = staticSegments[i];
|
|
942
|
+
result.push(seg.charAt(0).toUpperCase() + seg.slice(1));
|
|
943
|
+
}
|
|
944
|
+
return result.join("");
|
|
945
|
+
}
|
|
946
|
+
async function generateClient(mappings, options) {
|
|
947
|
+
const startTime = Date.now();
|
|
948
|
+
const grouped = groupByResource(mappings);
|
|
949
|
+
const resourceNames = Object.keys(grouped);
|
|
950
|
+
const code = generateClientCode(mappings, grouped, options);
|
|
951
|
+
await mkdir(dirname(options.outputPath), { recursive: true });
|
|
952
|
+
await writeFile(options.outputPath, code, "utf-8");
|
|
953
|
+
const stats = {
|
|
954
|
+
routesScanned: mappings.length,
|
|
955
|
+
contractsFound: mappings.length,
|
|
956
|
+
contractFiles: countUniqueContractFiles(mappings),
|
|
957
|
+
resourcesGenerated: resourceNames.length,
|
|
958
|
+
methodsGenerated: mappings.length,
|
|
959
|
+
duration: Date.now() - startTime
|
|
960
|
+
};
|
|
961
|
+
return stats;
|
|
962
|
+
}
|
|
963
|
+
function generateClientCode(mappings, grouped, options) {
|
|
964
|
+
let code = "";
|
|
965
|
+
code += generateHeader();
|
|
966
|
+
code += generateImports(mappings, options);
|
|
967
|
+
code += generateApiObject(grouped, options);
|
|
968
|
+
code += generateFooter();
|
|
969
|
+
return code;
|
|
970
|
+
}
|
|
971
|
+
function generateHeader() {
|
|
972
|
+
return `/**
|
|
973
|
+
* Auto-generated API Client
|
|
974
|
+
*
|
|
975
|
+
* Generated by @spfn/core codegen
|
|
976
|
+
* DO NOT EDIT MANUALLY
|
|
977
|
+
*
|
|
978
|
+
* @generated ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
979
|
+
*/
|
|
980
|
+
|
|
981
|
+
`;
|
|
982
|
+
}
|
|
983
|
+
function generateImports(mappings, options) {
|
|
984
|
+
let code = "";
|
|
985
|
+
code += `import { client } from '@spfn/core/client';
|
|
986
|
+
`;
|
|
987
|
+
if (options.includeTypes !== false) {
|
|
988
|
+
code += `import type { InferContract } from '@spfn/core';
|
|
989
|
+
`;
|
|
822
990
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
991
|
+
code += `
|
|
992
|
+
`;
|
|
993
|
+
const importGroups = groupContractsByImportPath(mappings);
|
|
994
|
+
const importPaths = Object.keys(importGroups);
|
|
995
|
+
for (let i = 0; i < importPaths.length; i++) {
|
|
996
|
+
const importPath = importPaths[i];
|
|
997
|
+
const contracts = importGroups[importPath];
|
|
998
|
+
code += `import { ${contracts.join(", ")} } from '${importPath}';
|
|
999
|
+
`;
|
|
1000
|
+
}
|
|
1001
|
+
code += `
|
|
1002
|
+
`;
|
|
1003
|
+
return code;
|
|
1004
|
+
}
|
|
1005
|
+
function groupContractsByImportPath(mappings) {
|
|
1006
|
+
const groups = {};
|
|
1007
|
+
for (let i = 0; i < mappings.length; i++) {
|
|
1008
|
+
const mapping = mappings[i];
|
|
1009
|
+
const path = mapping.contractImportPath;
|
|
1010
|
+
if (!groups[path]) {
|
|
1011
|
+
groups[path] = /* @__PURE__ */ new Set();
|
|
829
1012
|
}
|
|
830
|
-
|
|
831
|
-
this.currentStream.end((error) => {
|
|
832
|
-
if (error) {
|
|
833
|
-
reject(error);
|
|
834
|
-
} else {
|
|
835
|
-
this.currentStream = null;
|
|
836
|
-
this.currentFilename = null;
|
|
837
|
-
resolve2();
|
|
838
|
-
}
|
|
839
|
-
});
|
|
840
|
-
});
|
|
1013
|
+
groups[path].add(mapping.contractName);
|
|
841
1014
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
848
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
849
|
-
return `${year}-${month}-${day}.log`;
|
|
1015
|
+
const result = {};
|
|
1016
|
+
const keys = Object.keys(groups);
|
|
1017
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1018
|
+
const key = keys[i];
|
|
1019
|
+
result[key] = Array.from(groups[key]);
|
|
850
1020
|
}
|
|
851
|
-
|
|
852
|
-
|
|
1021
|
+
return result;
|
|
1022
|
+
}
|
|
1023
|
+
function generateApiObject(grouped, options) {
|
|
1024
|
+
let code = "";
|
|
1025
|
+
code += `/**
|
|
1026
|
+
* Type-safe API client
|
|
1027
|
+
*/
|
|
1028
|
+
export const api = {
|
|
1029
|
+
`;
|
|
1030
|
+
const resourceNames = Object.keys(grouped);
|
|
1031
|
+
for (let i = 0; i < resourceNames.length; i++) {
|
|
1032
|
+
const resourceName = resourceNames[i];
|
|
1033
|
+
const routes = grouped[resourceName];
|
|
1034
|
+
code += ` ${resourceName}: {
|
|
1035
|
+
`;
|
|
1036
|
+
for (let j = 0; j < routes.length; j++) {
|
|
1037
|
+
const route = routes[j];
|
|
1038
|
+
code += generateMethodCode(route, options);
|
|
1039
|
+
}
|
|
1040
|
+
code += ` }`;
|
|
1041
|
+
if (i < resourceNames.length - 1) {
|
|
1042
|
+
code += `,`;
|
|
1043
|
+
}
|
|
1044
|
+
code += `
|
|
1045
|
+
`;
|
|
853
1046
|
}
|
|
854
|
-
};
|
|
1047
|
+
code += `} as const;
|
|
855
1048
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
const isProduction = process.env.NODE_ENV === "production";
|
|
859
|
-
const isDevelopment = process.env.NODE_ENV === "development";
|
|
860
|
-
if (isDevelopment) {
|
|
861
|
-
return "debug";
|
|
862
|
-
}
|
|
863
|
-
if (isProduction) {
|
|
864
|
-
return "info";
|
|
865
|
-
}
|
|
866
|
-
return "warn";
|
|
867
|
-
}
|
|
868
|
-
function getConsoleConfig() {
|
|
869
|
-
const isProduction = process.env.NODE_ENV === "production";
|
|
870
|
-
return {
|
|
871
|
-
level: "debug",
|
|
872
|
-
enabled: true,
|
|
873
|
-
colorize: !isProduction
|
|
874
|
-
// Dev: colored output, Production: plain text
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
function getFileConfig() {
|
|
878
|
-
const isProduction = process.env.NODE_ENV === "production";
|
|
879
|
-
return {
|
|
880
|
-
level: "info",
|
|
881
|
-
enabled: isProduction,
|
|
882
|
-
// File logging in production only
|
|
883
|
-
logDir: process.env.LOG_DIR || "./logs",
|
|
884
|
-
maxFileSize: 10 * 1024 * 1024,
|
|
885
|
-
// 10MB
|
|
886
|
-
maxFiles: 10
|
|
887
|
-
};
|
|
1049
|
+
`;
|
|
1050
|
+
return code;
|
|
888
1051
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1052
|
+
function generateMethodCode(mapping, options) {
|
|
1053
|
+
const methodName = generateMethodName(mapping);
|
|
1054
|
+
const contractType = `typeof ${mapping.contractName}`;
|
|
1055
|
+
const hasParams = mapping.path.includes(":");
|
|
1056
|
+
const hasBody = ["POST", "PUT", "PATCH"].indexOf(mapping.method) !== -1;
|
|
1057
|
+
let code = "";
|
|
1058
|
+
if (options.includeJsDoc !== false) {
|
|
1059
|
+
code += ` /**
|
|
1060
|
+
`;
|
|
1061
|
+
code += ` * ${mapping.method} ${mapping.path}
|
|
1062
|
+
`;
|
|
1063
|
+
code += ` */
|
|
1064
|
+
`;
|
|
898
1065
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
constructor(config2) {
|
|
904
|
-
this.logger = new Logger({
|
|
905
|
-
level: config2.level,
|
|
906
|
-
module: config2.module,
|
|
907
|
-
transports: initializeTransports()
|
|
908
|
-
});
|
|
1066
|
+
code += ` ${methodName}: (`;
|
|
1067
|
+
const params = [];
|
|
1068
|
+
if (hasParams) {
|
|
1069
|
+
params.push(`params: InferContract<${contractType}>['params']`);
|
|
909
1070
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
adapter.logger = this.logger.child(module);
|
|
913
|
-
return adapter;
|
|
1071
|
+
if (hasBody) {
|
|
1072
|
+
params.push(`body: InferContract<${contractType}>['body']`);
|
|
914
1073
|
}
|
|
915
|
-
|
|
916
|
-
|
|
1074
|
+
if (params.length > 0) {
|
|
1075
|
+
code += `options: { ${params.join(", ")} }`;
|
|
917
1076
|
}
|
|
918
|
-
|
|
919
|
-
|
|
1077
|
+
code += `) => `;
|
|
1078
|
+
code += `client.call('${mapping.path}', ${mapping.contractName}, `;
|
|
1079
|
+
if (params.length > 0) {
|
|
1080
|
+
code += `options`;
|
|
1081
|
+
} else {
|
|
1082
|
+
code += `{}`;
|
|
920
1083
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1084
|
+
code += `),
|
|
1085
|
+
`;
|
|
1086
|
+
return code;
|
|
1087
|
+
}
|
|
1088
|
+
function generateMethodName(mapping) {
|
|
1089
|
+
const method = mapping.method.toLowerCase();
|
|
1090
|
+
if (mapping.path === "/" || mapping.path.match(/^\/[\w-]+$/)) {
|
|
1091
|
+
if (method === "get") {
|
|
1092
|
+
return "list";
|
|
926
1093
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
if (errorOrContext instanceof Error) {
|
|
930
|
-
this.logger.error(message, errorOrContext, context);
|
|
931
|
-
} else {
|
|
932
|
-
this.logger.error(message, errorOrContext);
|
|
1094
|
+
if (method === "post") {
|
|
1095
|
+
return "create";
|
|
933
1096
|
}
|
|
934
1097
|
}
|
|
935
|
-
|
|
936
|
-
if (
|
|
937
|
-
|
|
938
|
-
}
|
|
939
|
-
|
|
1098
|
+
if (mapping.path.includes(":")) {
|
|
1099
|
+
if (method === "get") {
|
|
1100
|
+
return "getById";
|
|
1101
|
+
}
|
|
1102
|
+
if (method === "put" || method === "patch") {
|
|
1103
|
+
return "update";
|
|
1104
|
+
}
|
|
1105
|
+
if (method === "delete") {
|
|
1106
|
+
return "delete";
|
|
940
1107
|
}
|
|
941
1108
|
}
|
|
942
|
-
|
|
943
|
-
await this.logger.close();
|
|
944
|
-
}
|
|
945
|
-
};
|
|
946
|
-
|
|
947
|
-
// src/logger/adapter-factory.ts
|
|
948
|
-
function createAdapter(type) {
|
|
949
|
-
const level = getDefaultLogLevel();
|
|
950
|
-
switch (type) {
|
|
951
|
-
case "pino":
|
|
952
|
-
return new PinoAdapter({ level });
|
|
953
|
-
case "custom":
|
|
954
|
-
return new CustomAdapter({ level });
|
|
955
|
-
default:
|
|
956
|
-
return new PinoAdapter({ level });
|
|
957
|
-
}
|
|
1109
|
+
return method;
|
|
958
1110
|
}
|
|
959
|
-
function
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1111
|
+
function generateFooter() {
|
|
1112
|
+
return `/**
|
|
1113
|
+
* Export client instance for advanced usage
|
|
1114
|
+
*
|
|
1115
|
+
* Use this to add interceptors or customize the client:
|
|
1116
|
+
*
|
|
1117
|
+
* @example
|
|
1118
|
+
* \`\`\`ts
|
|
1119
|
+
* import { client } from './api';
|
|
1120
|
+
* import { createAuthInterceptor } from '@spfn/auth/nextjs';
|
|
1121
|
+
* import { NextJSCookieProvider } from '@spfn/auth/nextjs';
|
|
1122
|
+
*
|
|
1123
|
+
* client.use(createAuthInterceptor({
|
|
1124
|
+
* cookieProvider: new NextJSCookieProvider(),
|
|
1125
|
+
* encryptionKey: process.env.ENCRYPTION_KEY!
|
|
1126
|
+
* }));
|
|
1127
|
+
* \`\`\`
|
|
1128
|
+
*/
|
|
1129
|
+
export { client };
|
|
1130
|
+
`;
|
|
1131
|
+
}
|
|
1132
|
+
function countUniqueContractFiles(mappings) {
|
|
1133
|
+
const files = /* @__PURE__ */ new Set();
|
|
1134
|
+
for (let i = 0; i < mappings.length; i++) {
|
|
1135
|
+
if (mappings[i].contractFile) {
|
|
1136
|
+
files.add(mappings[i].contractFile);
|
|
1137
|
+
}
|
|
963
1138
|
}
|
|
964
|
-
return
|
|
1139
|
+
return files.size;
|
|
965
1140
|
}
|
|
966
|
-
var logger = createAdapter(getAdapterType());
|
|
967
|
-
|
|
968
|
-
// src/codegen/watch-generate.ts
|
|
969
1141
|
var codegenLogger = logger.child("codegen");
|
|
970
1142
|
async function generateOnce(options) {
|
|
971
1143
|
const cwd = process.cwd();
|