@spfn/core 0.1.0-alpha.72 → 0.1.0-alpha.74
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.
|
@@ -5,806 +5,294 @@ import * as ts from 'typescript';
|
|
|
5
5
|
import pino from 'pino';
|
|
6
6
|
|
|
7
7
|
// src/codegen/built-in/contract/index.ts
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!contractExport.path.startsWith("/")) {
|
|
17
|
-
throw new Error(
|
|
18
|
-
`Contract '${contractExport.name}' in ${filePath} must use absolute path. Found: '${contractExport.path}'. Use '/your-path' instead.`
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
if (packagePrefix && !contractExport.path.startsWith(packagePrefix)) {
|
|
22
|
-
throw new Error(
|
|
23
|
-
`Contract '${contractExport.name}' in ${filePath} must include package prefix. Expected path to start with '${packagePrefix}', but found: '${contractExport.path}'. Example: path: '${packagePrefix}/${contractExport.path}'`
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
mappings.push({
|
|
27
|
-
method: contractExport.method,
|
|
28
|
-
path: contractExport.path,
|
|
29
|
-
contractName: contractExport.name,
|
|
30
|
-
contractImportPath: getImportPath(filePath),
|
|
31
|
-
routeFile: "",
|
|
32
|
-
contractFile: filePath,
|
|
33
|
-
hasQuery: contractExport.hasQuery,
|
|
34
|
-
hasBody: contractExport.hasBody,
|
|
35
|
-
hasParams: contractExport.hasParams
|
|
36
|
-
});
|
|
37
|
-
}
|
|
8
|
+
var PinoAdapter = class _PinoAdapter {
|
|
9
|
+
logger;
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.logger = pino({
|
|
12
|
+
level: config.level,
|
|
13
|
+
// 기본 필드
|
|
14
|
+
base: config.module ? { module: config.module } : void 0
|
|
15
|
+
});
|
|
38
16
|
}
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
17
|
+
child(module) {
|
|
18
|
+
const childLogger = new _PinoAdapter({ level: this.logger.level, module });
|
|
19
|
+
childLogger.logger = this.logger.child({ module });
|
|
20
|
+
return childLogger;
|
|
21
|
+
}
|
|
22
|
+
debug(message, context) {
|
|
23
|
+
this.logger.debug(context || {}, message);
|
|
24
|
+
}
|
|
25
|
+
info(message, context) {
|
|
26
|
+
this.logger.info(context || {}, message);
|
|
27
|
+
}
|
|
28
|
+
warn(message, errorOrContext, context) {
|
|
29
|
+
if (errorOrContext instanceof Error) {
|
|
30
|
+
this.logger.warn({ err: errorOrContext, ...context }, message);
|
|
31
|
+
} else {
|
|
32
|
+
this.logger.warn(errorOrContext || {}, message);
|
|
55
33
|
}
|
|
56
|
-
} catch (error) {
|
|
57
34
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
filePath,
|
|
64
|
-
sourceCode,
|
|
65
|
-
ts.ScriptTarget.Latest,
|
|
66
|
-
true
|
|
67
|
-
);
|
|
68
|
-
const exports = [];
|
|
69
|
-
function visit(node) {
|
|
70
|
-
if (ts.isVariableStatement(node)) {
|
|
71
|
-
const hasExport = node.modifiers?.some(
|
|
72
|
-
(m) => m.kind === ts.SyntaxKind.ExportKeyword
|
|
73
|
-
);
|
|
74
|
-
if (hasExport && node.declarationList.declarations.length > 0) {
|
|
75
|
-
const declaration = node.declarationList.declarations[0];
|
|
76
|
-
if (ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.initializer) {
|
|
77
|
-
const name = declaration.name.text;
|
|
78
|
-
const hasSatisfiesRouteContract = checkSatisfiesRouteContract(declaration.initializer);
|
|
79
|
-
if (hasSatisfiesRouteContract) {
|
|
80
|
-
const objectLiteral = extractObjectLiteral(declaration.initializer);
|
|
81
|
-
if (objectLiteral) {
|
|
82
|
-
const contractData = extractContractData(objectLiteral);
|
|
83
|
-
if (contractData.method && contractData.path) {
|
|
84
|
-
exports.push({
|
|
85
|
-
name,
|
|
86
|
-
method: contractData.method,
|
|
87
|
-
path: contractData.path,
|
|
88
|
-
hasQuery: contractData.hasQuery,
|
|
89
|
-
hasBody: contractData.hasBody,
|
|
90
|
-
hasParams: contractData.hasParams
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (isContractName(name)) {
|
|
97
|
-
const objectLiteral = extractObjectLiteral(declaration.initializer);
|
|
98
|
-
if (objectLiteral) {
|
|
99
|
-
const contractData = extractContractData(objectLiteral);
|
|
100
|
-
if (contractData.method && contractData.path) {
|
|
101
|
-
exports.push({
|
|
102
|
-
name,
|
|
103
|
-
method: contractData.method,
|
|
104
|
-
path: contractData.path,
|
|
105
|
-
hasQuery: contractData.hasQuery,
|
|
106
|
-
hasBody: contractData.hasBody,
|
|
107
|
-
hasParams: contractData.hasParams
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
35
|
+
error(message, errorOrContext, context) {
|
|
36
|
+
if (errorOrContext instanceof Error) {
|
|
37
|
+
this.logger.error({ err: errorOrContext, ...context }, message);
|
|
38
|
+
} else {
|
|
39
|
+
this.logger.error(errorOrContext || {}, message);
|
|
114
40
|
}
|
|
115
|
-
ts.forEachChild(node, visit);
|
|
116
41
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
42
|
+
fatal(message, errorOrContext, context) {
|
|
43
|
+
if (errorOrContext instanceof Error) {
|
|
44
|
+
this.logger.fatal({ err: errorOrContext, ...context }, message);
|
|
45
|
+
} else {
|
|
46
|
+
this.logger.fatal(errorOrContext || {}, message);
|
|
47
|
+
}
|
|
123
48
|
}
|
|
124
|
-
|
|
125
|
-
if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
126
|
-
return typeNode.typeName.text === "RouteContract";
|
|
49
|
+
async close() {
|
|
127
50
|
}
|
|
128
|
-
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/logger/types.ts
|
|
54
|
+
var LOG_LEVEL_PRIORITY = {
|
|
55
|
+
debug: 0,
|
|
56
|
+
info: 1,
|
|
57
|
+
warn: 2,
|
|
58
|
+
error: 3,
|
|
59
|
+
fatal: 4
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/logger/formatters.ts
|
|
63
|
+
var SENSITIVE_KEYS = [
|
|
64
|
+
"password",
|
|
65
|
+
"passwd",
|
|
66
|
+
"pwd",
|
|
67
|
+
"secret",
|
|
68
|
+
"token",
|
|
69
|
+
"apikey",
|
|
70
|
+
"api_key",
|
|
71
|
+
"accesstoken",
|
|
72
|
+
"access_token",
|
|
73
|
+
"refreshtoken",
|
|
74
|
+
"refresh_token",
|
|
75
|
+
"authorization",
|
|
76
|
+
"auth",
|
|
77
|
+
"cookie",
|
|
78
|
+
"session",
|
|
79
|
+
"sessionid",
|
|
80
|
+
"session_id",
|
|
81
|
+
"privatekey",
|
|
82
|
+
"private_key",
|
|
83
|
+
"creditcard",
|
|
84
|
+
"credit_card",
|
|
85
|
+
"cardnumber",
|
|
86
|
+
"card_number",
|
|
87
|
+
"cvv",
|
|
88
|
+
"ssn",
|
|
89
|
+
"pin"
|
|
90
|
+
];
|
|
91
|
+
var MASKED_VALUE = "***MASKED***";
|
|
92
|
+
function isSensitiveKey(key) {
|
|
93
|
+
const lowerKey = key.toLowerCase();
|
|
94
|
+
return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
|
|
129
95
|
}
|
|
130
|
-
function
|
|
131
|
-
if (
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
if (ts.isSatisfiesExpression(initializer)) {
|
|
135
|
-
return extractObjectLiteral(initializer.expression);
|
|
96
|
+
function maskSensitiveData(data) {
|
|
97
|
+
if (data === null || data === void 0) {
|
|
98
|
+
return data;
|
|
136
99
|
}
|
|
137
|
-
if (
|
|
138
|
-
return
|
|
100
|
+
if (Array.isArray(data)) {
|
|
101
|
+
return data.map((item) => maskSensitiveData(item));
|
|
139
102
|
}
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
let value;
|
|
150
|
-
if (ts.isStringLiteral(prop.initializer)) {
|
|
151
|
-
value = prop.initializer.text;
|
|
152
|
-
} else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
|
|
153
|
-
value = prop.initializer.expression.text;
|
|
154
|
-
}
|
|
155
|
-
if (value) result.method = value;
|
|
156
|
-
} else if (propName === "path") {
|
|
157
|
-
let value;
|
|
158
|
-
if (ts.isStringLiteral(prop.initializer)) {
|
|
159
|
-
value = prop.initializer.text;
|
|
160
|
-
} else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
|
|
161
|
-
value = prop.initializer.expression.text;
|
|
162
|
-
}
|
|
163
|
-
if (value) result.path = value;
|
|
164
|
-
} else if (propName === "query") {
|
|
165
|
-
result.hasQuery = true;
|
|
166
|
-
} else if (propName === "body") {
|
|
167
|
-
result.hasBody = true;
|
|
168
|
-
} else if (propName === "params") {
|
|
169
|
-
result.hasParams = true;
|
|
103
|
+
if (typeof data === "object") {
|
|
104
|
+
const masked = {};
|
|
105
|
+
for (const [key, value] of Object.entries(data)) {
|
|
106
|
+
if (isSensitiveKey(key)) {
|
|
107
|
+
masked[key] = MASKED_VALUE;
|
|
108
|
+
} else if (typeof value === "object" && value !== null) {
|
|
109
|
+
masked[key] = maskSensitiveData(value);
|
|
110
|
+
} else {
|
|
111
|
+
masked[key] = value;
|
|
170
112
|
}
|
|
171
113
|
}
|
|
114
|
+
return masked;
|
|
172
115
|
}
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
function isContractName(name) {
|
|
176
|
-
return name.indexOf("Contract") !== -1 || name.indexOf("contract") !== -1 || name.endsWith("Schema") || name.endsWith("schema");
|
|
116
|
+
return data;
|
|
177
117
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
118
|
+
var COLORS = {
|
|
119
|
+
reset: "\x1B[0m",
|
|
120
|
+
bright: "\x1B[1m",
|
|
121
|
+
dim: "\x1B[2m",
|
|
122
|
+
// 로그 레벨 컬러
|
|
123
|
+
debug: "\x1B[36m",
|
|
124
|
+
// cyan
|
|
125
|
+
info: "\x1B[32m",
|
|
126
|
+
// green
|
|
127
|
+
warn: "\x1B[33m",
|
|
128
|
+
// yellow
|
|
129
|
+
error: "\x1B[31m",
|
|
130
|
+
// red
|
|
131
|
+
fatal: "\x1B[35m",
|
|
132
|
+
// magenta
|
|
133
|
+
// 추가 컬러
|
|
134
|
+
gray: "\x1B[90m"
|
|
135
|
+
};
|
|
136
|
+
function formatTimestamp(date) {
|
|
137
|
+
return date.toISOString();
|
|
192
138
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
139
|
+
function formatTimestampHuman(date) {
|
|
140
|
+
const year = date.getFullYear();
|
|
141
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
142
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
143
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
144
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
145
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
146
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
147
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
|
|
148
|
+
}
|
|
149
|
+
function formatError(error) {
|
|
150
|
+
const lines = [];
|
|
151
|
+
lines.push(`${error.name}: ${error.message}`);
|
|
152
|
+
if (error.stack) {
|
|
153
|
+
const stackLines = error.stack.split("\n").slice(1);
|
|
154
|
+
lines.push(...stackLines);
|
|
204
155
|
}
|
|
205
|
-
return
|
|
156
|
+
return lines.join("\n");
|
|
206
157
|
}
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
158
|
+
function formatConsole(metadata, colorize = true) {
|
|
159
|
+
const parts = [];
|
|
160
|
+
const timestamp = formatTimestampHuman(metadata.timestamp);
|
|
161
|
+
if (colorize) {
|
|
162
|
+
parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
|
|
163
|
+
} else {
|
|
164
|
+
parts.push(`[${timestamp}]`);
|
|
211
165
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
staticSegments.push(seg);
|
|
166
|
+
if (metadata.module) {
|
|
167
|
+
if (colorize) {
|
|
168
|
+
parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
|
|
169
|
+
} else {
|
|
170
|
+
parts.push(`[module=${metadata.module}]`);
|
|
218
171
|
}
|
|
219
172
|
}
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
173
|
+
if (metadata.context && Object.keys(metadata.context).length > 0) {
|
|
174
|
+
Object.entries(metadata.context).forEach(([key, value]) => {
|
|
175
|
+
const valueStr = typeof value === "string" ? value : String(value);
|
|
176
|
+
if (colorize) {
|
|
177
|
+
parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
|
|
178
|
+
} else {
|
|
179
|
+
parts.push(`[${key}=${valueStr}]`);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
226
182
|
}
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
183
|
+
const levelStr = metadata.level.toUpperCase();
|
|
184
|
+
if (colorize) {
|
|
185
|
+
const color = COLORS[metadata.level];
|
|
186
|
+
parts.push(`${color}(${levelStr})${COLORS.reset}:`);
|
|
187
|
+
} else {
|
|
188
|
+
parts.push(`(${levelStr}):`);
|
|
231
189
|
}
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (parts.length === 1) {
|
|
237
|
-
return capitalize ? str.charAt(0).toUpperCase() + str.slice(1) : str;
|
|
190
|
+
if (colorize) {
|
|
191
|
+
parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
|
|
192
|
+
} else {
|
|
193
|
+
parts.push(metadata.message);
|
|
238
194
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (i === 0 && !capitalize) {
|
|
243
|
-
result.push(part);
|
|
244
|
-
} else {
|
|
245
|
-
result.push(part.charAt(0).toUpperCase() + part.slice(1));
|
|
246
|
-
}
|
|
195
|
+
let output = parts.join(" ");
|
|
196
|
+
if (metadata.error) {
|
|
197
|
+
output += "\n" + formatError(metadata.error);
|
|
247
198
|
}
|
|
248
|
-
return
|
|
199
|
+
return output;
|
|
249
200
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const resourceNames = Object.keys(grouped);
|
|
256
|
-
await generateSplitClient(mappings, grouped, options);
|
|
257
|
-
return {
|
|
258
|
-
routesScanned: mappings.length,
|
|
259
|
-
contractsFound: mappings.length,
|
|
260
|
-
contractFiles: countUniqueContractFiles(mappings),
|
|
261
|
-
resourcesGenerated: resourceNames.length,
|
|
262
|
-
methodsGenerated: mappings.length,
|
|
263
|
-
duration: Date.now() - startTime
|
|
201
|
+
function formatJSON(metadata) {
|
|
202
|
+
const obj = {
|
|
203
|
+
timestamp: formatTimestamp(metadata.timestamp),
|
|
204
|
+
level: metadata.level,
|
|
205
|
+
message: metadata.message
|
|
264
206
|
};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return `/**
|
|
268
|
-
* Auto-generated API Client
|
|
269
|
-
*
|
|
270
|
-
* Generated by @spfn/core codegen
|
|
271
|
-
* DO NOT EDIT MANUALLY
|
|
272
|
-
*
|
|
273
|
-
* @generated ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
274
|
-
*/
|
|
275
|
-
|
|
276
|
-
`;
|
|
277
|
-
}
|
|
278
|
-
function groupContractsByImportPath(mappings) {
|
|
279
|
-
const groups = {};
|
|
280
|
-
for (let i = 0; i < mappings.length; i++) {
|
|
281
|
-
const mapping = mappings[i];
|
|
282
|
-
const path = mapping.contractImportPath;
|
|
283
|
-
if (!groups[path]) {
|
|
284
|
-
groups[path] = /* @__PURE__ */ new Set();
|
|
285
|
-
}
|
|
286
|
-
groups[path].add(mapping.contractName);
|
|
287
|
-
}
|
|
288
|
-
const result = {};
|
|
289
|
-
const keys = Object.keys(groups);
|
|
290
|
-
for (let i = 0; i < keys.length; i++) {
|
|
291
|
-
const key = keys[i];
|
|
292
|
-
result[key] = Array.from(groups[key]);
|
|
207
|
+
if (metadata.module) {
|
|
208
|
+
obj.module = metadata.module;
|
|
293
209
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
function generateTypeName(mapping) {
|
|
297
|
-
let name = mapping.contractName;
|
|
298
|
-
if (name.endsWith("Contract")) {
|
|
299
|
-
name = name.slice(0, -8);
|
|
210
|
+
if (metadata.context) {
|
|
211
|
+
obj.context = metadata.context;
|
|
300
212
|
}
|
|
301
|
-
if (
|
|
302
|
-
|
|
213
|
+
if (metadata.error) {
|
|
214
|
+
obj.error = {
|
|
215
|
+
name: metadata.error.name,
|
|
216
|
+
message: metadata.error.message,
|
|
217
|
+
stack: metadata.error.stack
|
|
218
|
+
};
|
|
303
219
|
}
|
|
304
|
-
return
|
|
220
|
+
return JSON.stringify(obj);
|
|
305
221
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
222
|
+
|
|
223
|
+
// src/logger/logger.ts
|
|
224
|
+
var Logger = class _Logger {
|
|
225
|
+
config;
|
|
226
|
+
module;
|
|
227
|
+
constructor(config) {
|
|
228
|
+
this.config = config;
|
|
229
|
+
this.module = config.module;
|
|
310
230
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const hasQuery = mapping.hasQuery || false;
|
|
317
|
-
const hasBody = mapping.hasBody || false;
|
|
318
|
-
let code = "";
|
|
319
|
-
if (options.includeJsDoc !== false) {
|
|
320
|
-
code += `/**
|
|
321
|
-
`;
|
|
322
|
-
code += ` * ${mapping.method} ${mapping.path}
|
|
323
|
-
`;
|
|
324
|
-
code += ` */
|
|
325
|
-
`;
|
|
231
|
+
/**
|
|
232
|
+
* Get current log level
|
|
233
|
+
*/
|
|
234
|
+
get level() {
|
|
235
|
+
return this.config.level;
|
|
326
236
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Create child logger (per module)
|
|
239
|
+
*/
|
|
240
|
+
child(module) {
|
|
241
|
+
return new _Logger({
|
|
242
|
+
...this.config,
|
|
243
|
+
module
|
|
244
|
+
});
|
|
332
245
|
}
|
|
333
|
-
|
|
334
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Debug log
|
|
248
|
+
*/
|
|
249
|
+
debug(message, context) {
|
|
250
|
+
this.log("debug", message, void 0, context);
|
|
335
251
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
code += `) => `;
|
|
343
|
-
code += `client.call(${mapping.contractName}`;
|
|
344
|
-
if (params.length > 0) {
|
|
345
|
-
code += `, options`;
|
|
346
|
-
}
|
|
347
|
-
code += `);
|
|
348
|
-
|
|
349
|
-
`;
|
|
350
|
-
return code;
|
|
351
|
-
}
|
|
352
|
-
function countUniqueContractFiles(mappings) {
|
|
353
|
-
const files = /* @__PURE__ */ new Set();
|
|
354
|
-
for (let i = 0; i < mappings.length; i++) {
|
|
355
|
-
if (mappings[i].contractFile) {
|
|
356
|
-
files.add(mappings[i].contractFile);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
return files.size;
|
|
360
|
-
}
|
|
361
|
-
function toKebabCase(str) {
|
|
362
|
-
if (str.length === 0) {
|
|
363
|
-
return str;
|
|
364
|
-
}
|
|
365
|
-
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
366
|
-
}
|
|
367
|
-
async function generateSplitClient(_mappings, grouped, options) {
|
|
368
|
-
const outputPath = options.outputPath;
|
|
369
|
-
const outputDir = outputPath.endsWith(".ts") || outputPath.endsWith(".js") ? outputPath.replace(/\.[jt]s$/, "") : outputPath;
|
|
370
|
-
await mkdir(outputDir, { recursive: true });
|
|
371
|
-
const resourceNames = Object.keys(grouped);
|
|
372
|
-
for (let i = 0; i < resourceNames.length; i++) {
|
|
373
|
-
const resourceName = resourceNames[i];
|
|
374
|
-
const routes = grouped[resourceName];
|
|
375
|
-
const code = generateResourceFile(resourceName, routes, options);
|
|
376
|
-
const kebabName = toKebabCase(resourceName);
|
|
377
|
-
const filePath = `${outputDir}/${kebabName}.ts`;
|
|
378
|
-
await writeFile(filePath, code, "utf-8");
|
|
379
|
-
}
|
|
380
|
-
const indexCode = generateIndexFile(grouped, options);
|
|
381
|
-
const indexPath = `${outputDir}/index.ts`;
|
|
382
|
-
await writeFile(indexPath, indexCode, "utf-8");
|
|
383
|
-
}
|
|
384
|
-
function generateResourceFile(_resourceName, routes, options) {
|
|
385
|
-
let code = "";
|
|
386
|
-
code += generateHeader();
|
|
387
|
-
code += `import { client } from '@spfn/core/client';
|
|
388
|
-
`;
|
|
389
|
-
if (options.includeTypes !== false) {
|
|
390
|
-
code += `import type { InferContract } from '@spfn/core';
|
|
391
|
-
`;
|
|
392
|
-
}
|
|
393
|
-
code += `
|
|
394
|
-
`;
|
|
395
|
-
const importGroups = groupContractsByImportPath(routes);
|
|
396
|
-
const importPaths = Object.keys(importGroups);
|
|
397
|
-
for (let i = 0; i < importPaths.length; i++) {
|
|
398
|
-
const importPath = importPaths[i];
|
|
399
|
-
const contracts = importGroups[importPath];
|
|
400
|
-
code += `import { ${contracts.join(", ")} } from '${importPath}';
|
|
401
|
-
`;
|
|
402
|
-
}
|
|
403
|
-
code += `
|
|
404
|
-
`;
|
|
405
|
-
if (options.includeTypes !== false) {
|
|
406
|
-
code += `// ============================================
|
|
407
|
-
`;
|
|
408
|
-
code += `// Types
|
|
409
|
-
`;
|
|
410
|
-
code += `// ============================================
|
|
411
|
-
|
|
412
|
-
`;
|
|
413
|
-
for (let i = 0; i < routes.length; i++) {
|
|
414
|
-
const route = routes[i];
|
|
415
|
-
const typeName = generateTypeName(route);
|
|
416
|
-
const contractType = `typeof ${route.contractName}`;
|
|
417
|
-
code += `export type ${typeName}Response = InferContract<${contractType}>['response'];
|
|
418
|
-
`;
|
|
419
|
-
if (route.hasQuery) {
|
|
420
|
-
code += `export type ${typeName}Query = InferContract<${contractType}>['query'];
|
|
421
|
-
`;
|
|
422
|
-
}
|
|
423
|
-
if (route.hasParams || route.path.includes(":")) {
|
|
424
|
-
code += `export type ${typeName}Params = InferContract<${contractType}>['params'];
|
|
425
|
-
`;
|
|
426
|
-
}
|
|
427
|
-
if (route.hasBody) {
|
|
428
|
-
code += `export type ${typeName}Body = InferContract<${contractType}>['body'];
|
|
429
|
-
`;
|
|
430
|
-
}
|
|
431
|
-
code += `
|
|
432
|
-
`;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
code += `// ============================================
|
|
436
|
-
`;
|
|
437
|
-
code += `// API Functions
|
|
438
|
-
`;
|
|
439
|
-
code += `// ============================================
|
|
440
|
-
|
|
441
|
-
`;
|
|
442
|
-
for (let i = 0; i < routes.length; i++) {
|
|
443
|
-
const route = routes[i];
|
|
444
|
-
code += generateFunctionCode(route, options);
|
|
445
|
-
}
|
|
446
|
-
return code;
|
|
447
|
-
}
|
|
448
|
-
function generateIndexFile(grouped, options) {
|
|
449
|
-
let code = "";
|
|
450
|
-
const apiName = options.apiName || "api";
|
|
451
|
-
const resourceNames = Object.keys(grouped);
|
|
452
|
-
code += generateHeader();
|
|
453
|
-
code += `export { client } from '@spfn/core/client';
|
|
454
|
-
|
|
455
|
-
`;
|
|
456
|
-
for (let i = 0; i < resourceNames.length; i++) {
|
|
457
|
-
const resourceName = resourceNames[i];
|
|
458
|
-
const routes = grouped[resourceName];
|
|
459
|
-
const kebabName = toKebabCase(resourceName);
|
|
460
|
-
const typeNames = [];
|
|
461
|
-
for (let j = 0; j < routes.length; j++) {
|
|
462
|
-
const route = routes[j];
|
|
463
|
-
const typeName = generateTypeName(route);
|
|
464
|
-
typeNames.push(`${typeName}Response`);
|
|
465
|
-
if (route.hasQuery) {
|
|
466
|
-
typeNames.push(`${typeName}Query`);
|
|
467
|
-
}
|
|
468
|
-
if (route.hasParams || route.path.includes(":")) {
|
|
469
|
-
typeNames.push(`${typeName}Params`);
|
|
470
|
-
}
|
|
471
|
-
if (route.hasBody) {
|
|
472
|
-
typeNames.push(`${typeName}Body`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
if (typeNames.length > 0) {
|
|
476
|
-
code += `export type { ${typeNames.join(", ")} } from './${kebabName}.js';
|
|
477
|
-
`;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
code += `
|
|
481
|
-
`;
|
|
482
|
-
for (let i = 0; i < resourceNames.length; i++) {
|
|
483
|
-
const resourceName = resourceNames[i];
|
|
484
|
-
const routes = grouped[resourceName];
|
|
485
|
-
const kebabName = toKebabCase(resourceName);
|
|
486
|
-
const functionNames = routes.map((route) => generateFunctionName(route));
|
|
487
|
-
code += `import { ${functionNames.join(", ")} } from './${kebabName}.js';
|
|
488
|
-
`;
|
|
489
|
-
}
|
|
490
|
-
code += `
|
|
491
|
-
`;
|
|
492
|
-
code += `/**
|
|
493
|
-
`;
|
|
494
|
-
code += ` * Type-safe API client
|
|
495
|
-
`;
|
|
496
|
-
code += ` */
|
|
497
|
-
`;
|
|
498
|
-
code += `export const ${apiName} = {
|
|
499
|
-
`;
|
|
500
|
-
let isFirst = true;
|
|
501
|
-
for (let i = 0; i < resourceNames.length; i++) {
|
|
502
|
-
const resourceName = resourceNames[i];
|
|
503
|
-
const routes = grouped[resourceName];
|
|
504
|
-
for (let j = 0; j < routes.length; j++) {
|
|
505
|
-
const route = routes[j];
|
|
506
|
-
const functionName = generateFunctionName(route);
|
|
507
|
-
if (!isFirst) {
|
|
508
|
-
code += `,
|
|
509
|
-
`;
|
|
510
|
-
}
|
|
511
|
-
code += ` ${functionName}`;
|
|
512
|
-
isFirst = false;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
code += `
|
|
516
|
-
} as const;
|
|
517
|
-
`;
|
|
518
|
-
return code;
|
|
519
|
-
}
|
|
520
|
-
var PinoAdapter = class _PinoAdapter {
|
|
521
|
-
logger;
|
|
522
|
-
constructor(config) {
|
|
523
|
-
this.logger = pino({
|
|
524
|
-
level: config.level,
|
|
525
|
-
// 기본 필드
|
|
526
|
-
base: config.module ? { module: config.module } : void 0
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
child(module) {
|
|
530
|
-
const childLogger = new _PinoAdapter({ level: this.logger.level, module });
|
|
531
|
-
childLogger.logger = this.logger.child({ module });
|
|
532
|
-
return childLogger;
|
|
533
|
-
}
|
|
534
|
-
debug(message, context) {
|
|
535
|
-
this.logger.debug(context || {}, message);
|
|
536
|
-
}
|
|
537
|
-
info(message, context) {
|
|
538
|
-
this.logger.info(context || {}, message);
|
|
252
|
+
/**
|
|
253
|
+
* Info log
|
|
254
|
+
*/
|
|
255
|
+
info(message, context) {
|
|
256
|
+
this.log("info", message, void 0, context);
|
|
539
257
|
}
|
|
540
258
|
warn(message, errorOrContext, context) {
|
|
541
259
|
if (errorOrContext instanceof Error) {
|
|
542
|
-
this.
|
|
260
|
+
this.log("warn", message, errorOrContext, context);
|
|
543
261
|
} else {
|
|
544
|
-
this.
|
|
262
|
+
this.log("warn", message, void 0, errorOrContext);
|
|
545
263
|
}
|
|
546
264
|
}
|
|
547
265
|
error(message, errorOrContext, context) {
|
|
548
266
|
if (errorOrContext instanceof Error) {
|
|
549
|
-
this.
|
|
267
|
+
this.log("error", message, errorOrContext, context);
|
|
550
268
|
} else {
|
|
551
|
-
this.
|
|
269
|
+
this.log("error", message, void 0, errorOrContext);
|
|
552
270
|
}
|
|
553
271
|
}
|
|
554
272
|
fatal(message, errorOrContext, context) {
|
|
555
273
|
if (errorOrContext instanceof Error) {
|
|
556
|
-
this.
|
|
274
|
+
this.log("fatal", message, errorOrContext, context);
|
|
557
275
|
} else {
|
|
558
|
-
this.
|
|
276
|
+
this.log("fatal", message, void 0, errorOrContext);
|
|
559
277
|
}
|
|
560
278
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
"pwd",
|
|
579
|
-
"secret",
|
|
580
|
-
"token",
|
|
581
|
-
"apikey",
|
|
582
|
-
"api_key",
|
|
583
|
-
"accesstoken",
|
|
584
|
-
"access_token",
|
|
585
|
-
"refreshtoken",
|
|
586
|
-
"refresh_token",
|
|
587
|
-
"authorization",
|
|
588
|
-
"auth",
|
|
589
|
-
"cookie",
|
|
590
|
-
"session",
|
|
591
|
-
"sessionid",
|
|
592
|
-
"session_id",
|
|
593
|
-
"privatekey",
|
|
594
|
-
"private_key",
|
|
595
|
-
"creditcard",
|
|
596
|
-
"credit_card",
|
|
597
|
-
"cardnumber",
|
|
598
|
-
"card_number",
|
|
599
|
-
"cvv",
|
|
600
|
-
"ssn",
|
|
601
|
-
"pin"
|
|
602
|
-
];
|
|
603
|
-
var MASKED_VALUE = "***MASKED***";
|
|
604
|
-
function isSensitiveKey(key) {
|
|
605
|
-
const lowerKey = key.toLowerCase();
|
|
606
|
-
return SENSITIVE_KEYS.some((sensitive) => lowerKey.includes(sensitive));
|
|
607
|
-
}
|
|
608
|
-
function maskSensitiveData(data) {
|
|
609
|
-
if (data === null || data === void 0) {
|
|
610
|
-
return data;
|
|
611
|
-
}
|
|
612
|
-
if (Array.isArray(data)) {
|
|
613
|
-
return data.map((item) => maskSensitiveData(item));
|
|
614
|
-
}
|
|
615
|
-
if (typeof data === "object") {
|
|
616
|
-
const masked = {};
|
|
617
|
-
for (const [key, value] of Object.entries(data)) {
|
|
618
|
-
if (isSensitiveKey(key)) {
|
|
619
|
-
masked[key] = MASKED_VALUE;
|
|
620
|
-
} else if (typeof value === "object" && value !== null) {
|
|
621
|
-
masked[key] = maskSensitiveData(value);
|
|
622
|
-
} else {
|
|
623
|
-
masked[key] = value;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
return masked;
|
|
627
|
-
}
|
|
628
|
-
return data;
|
|
629
|
-
}
|
|
630
|
-
var COLORS = {
|
|
631
|
-
reset: "\x1B[0m",
|
|
632
|
-
bright: "\x1B[1m",
|
|
633
|
-
dim: "\x1B[2m",
|
|
634
|
-
// 로그 레벨 컬러
|
|
635
|
-
debug: "\x1B[36m",
|
|
636
|
-
// cyan
|
|
637
|
-
info: "\x1B[32m",
|
|
638
|
-
// green
|
|
639
|
-
warn: "\x1B[33m",
|
|
640
|
-
// yellow
|
|
641
|
-
error: "\x1B[31m",
|
|
642
|
-
// red
|
|
643
|
-
fatal: "\x1B[35m",
|
|
644
|
-
// magenta
|
|
645
|
-
// 추가 컬러
|
|
646
|
-
gray: "\x1B[90m"
|
|
647
|
-
};
|
|
648
|
-
function formatTimestamp(date) {
|
|
649
|
-
return date.toISOString();
|
|
650
|
-
}
|
|
651
|
-
function formatTimestampHuman(date) {
|
|
652
|
-
const year = date.getFullYear();
|
|
653
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
654
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
655
|
-
const hours = String(date.getHours()).padStart(2, "0");
|
|
656
|
-
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
657
|
-
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
658
|
-
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
659
|
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
|
|
660
|
-
}
|
|
661
|
-
function formatError(error) {
|
|
662
|
-
const lines = [];
|
|
663
|
-
lines.push(`${error.name}: ${error.message}`);
|
|
664
|
-
if (error.stack) {
|
|
665
|
-
const stackLines = error.stack.split("\n").slice(1);
|
|
666
|
-
lines.push(...stackLines);
|
|
667
|
-
}
|
|
668
|
-
return lines.join("\n");
|
|
669
|
-
}
|
|
670
|
-
function formatConsole(metadata, colorize = true) {
|
|
671
|
-
const parts = [];
|
|
672
|
-
const timestamp = formatTimestampHuman(metadata.timestamp);
|
|
673
|
-
if (colorize) {
|
|
674
|
-
parts.push(`${COLORS.gray}[${timestamp}]${COLORS.reset}`);
|
|
675
|
-
} else {
|
|
676
|
-
parts.push(`[${timestamp}]`);
|
|
677
|
-
}
|
|
678
|
-
if (metadata.module) {
|
|
679
|
-
if (colorize) {
|
|
680
|
-
parts.push(`${COLORS.dim}[module=${metadata.module}]${COLORS.reset}`);
|
|
681
|
-
} else {
|
|
682
|
-
parts.push(`[module=${metadata.module}]`);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
if (metadata.context && Object.keys(metadata.context).length > 0) {
|
|
686
|
-
Object.entries(metadata.context).forEach(([key, value]) => {
|
|
687
|
-
const valueStr = typeof value === "string" ? value : String(value);
|
|
688
|
-
if (colorize) {
|
|
689
|
-
parts.push(`${COLORS.dim}[${key}=${valueStr}]${COLORS.reset}`);
|
|
690
|
-
} else {
|
|
691
|
-
parts.push(`[${key}=${valueStr}]`);
|
|
692
|
-
}
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
const levelStr = metadata.level.toUpperCase();
|
|
696
|
-
if (colorize) {
|
|
697
|
-
const color = COLORS[metadata.level];
|
|
698
|
-
parts.push(`${color}(${levelStr})${COLORS.reset}:`);
|
|
699
|
-
} else {
|
|
700
|
-
parts.push(`(${levelStr}):`);
|
|
701
|
-
}
|
|
702
|
-
if (colorize) {
|
|
703
|
-
parts.push(`${COLORS.bright}${metadata.message}${COLORS.reset}`);
|
|
704
|
-
} else {
|
|
705
|
-
parts.push(metadata.message);
|
|
706
|
-
}
|
|
707
|
-
let output = parts.join(" ");
|
|
708
|
-
if (metadata.error) {
|
|
709
|
-
output += "\n" + formatError(metadata.error);
|
|
710
|
-
}
|
|
711
|
-
return output;
|
|
712
|
-
}
|
|
713
|
-
function formatJSON(metadata) {
|
|
714
|
-
const obj = {
|
|
715
|
-
timestamp: formatTimestamp(metadata.timestamp),
|
|
716
|
-
level: metadata.level,
|
|
717
|
-
message: metadata.message
|
|
718
|
-
};
|
|
719
|
-
if (metadata.module) {
|
|
720
|
-
obj.module = metadata.module;
|
|
721
|
-
}
|
|
722
|
-
if (metadata.context) {
|
|
723
|
-
obj.context = metadata.context;
|
|
724
|
-
}
|
|
725
|
-
if (metadata.error) {
|
|
726
|
-
obj.error = {
|
|
727
|
-
name: metadata.error.name,
|
|
728
|
-
message: metadata.error.message,
|
|
729
|
-
stack: metadata.error.stack
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
return JSON.stringify(obj);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// src/logger/logger.ts
|
|
736
|
-
var Logger = class _Logger {
|
|
737
|
-
config;
|
|
738
|
-
module;
|
|
739
|
-
constructor(config) {
|
|
740
|
-
this.config = config;
|
|
741
|
-
this.module = config.module;
|
|
742
|
-
}
|
|
743
|
-
/**
|
|
744
|
-
* Get current log level
|
|
745
|
-
*/
|
|
746
|
-
get level() {
|
|
747
|
-
return this.config.level;
|
|
748
|
-
}
|
|
749
|
-
/**
|
|
750
|
-
* Create child logger (per module)
|
|
751
|
-
*/
|
|
752
|
-
child(module) {
|
|
753
|
-
return new _Logger({
|
|
754
|
-
...this.config,
|
|
755
|
-
module
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* Debug log
|
|
760
|
-
*/
|
|
761
|
-
debug(message, context) {
|
|
762
|
-
this.log("debug", message, void 0, context);
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* Info log
|
|
766
|
-
*/
|
|
767
|
-
info(message, context) {
|
|
768
|
-
this.log("info", message, void 0, context);
|
|
769
|
-
}
|
|
770
|
-
warn(message, errorOrContext, context) {
|
|
771
|
-
if (errorOrContext instanceof Error) {
|
|
772
|
-
this.log("warn", message, errorOrContext, context);
|
|
773
|
-
} else {
|
|
774
|
-
this.log("warn", message, void 0, errorOrContext);
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
error(message, errorOrContext, context) {
|
|
778
|
-
if (errorOrContext instanceof Error) {
|
|
779
|
-
this.log("error", message, errorOrContext, context);
|
|
780
|
-
} else {
|
|
781
|
-
this.log("error", message, void 0, errorOrContext);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
fatal(message, errorOrContext, context) {
|
|
785
|
-
if (errorOrContext instanceof Error) {
|
|
786
|
-
this.log("fatal", message, errorOrContext, context);
|
|
787
|
-
} else {
|
|
788
|
-
this.log("fatal", message, void 0, errorOrContext);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Log processing (internal)
|
|
793
|
-
*/
|
|
794
|
-
log(level, message, error, context) {
|
|
795
|
-
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
const metadata = {
|
|
799
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
800
|
-
level,
|
|
801
|
-
message,
|
|
802
|
-
module: this.module,
|
|
803
|
-
error,
|
|
804
|
-
// Mask sensitive information in context to prevent credential leaks
|
|
805
|
-
context: context ? maskSensitiveData(context) : void 0
|
|
806
|
-
};
|
|
807
|
-
this.processTransports(metadata);
|
|
279
|
+
/**
|
|
280
|
+
* Log processing (internal)
|
|
281
|
+
*/
|
|
282
|
+
log(level, message, error, context) {
|
|
283
|
+
if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[this.config.level]) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const metadata = {
|
|
287
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
288
|
+
level,
|
|
289
|
+
message,
|
|
290
|
+
module: this.module,
|
|
291
|
+
error,
|
|
292
|
+
// Mask sensitive information in context to prevent credential leaks
|
|
293
|
+
context: context ? maskSensitiveData(context) : void 0
|
|
294
|
+
};
|
|
295
|
+
this.processTransports(metadata);
|
|
808
296
|
}
|
|
809
297
|
/**
|
|
810
298
|
* Process Transports
|
|
@@ -1043,245 +531,766 @@ var FileTransport = class {
|
|
|
1043
531
|
}
|
|
1044
532
|
}
|
|
1045
533
|
}
|
|
1046
|
-
} catch (error) {
|
|
1047
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1048
|
-
process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage}
|
|
1049
|
-
`);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
536
|
+
process.stderr.write(`[FileTransport] Failed to clean old files: ${errorMessage}
|
|
537
|
+
`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* 날짜별 로그 파일명 생성
|
|
542
|
+
*/
|
|
543
|
+
getLogFilename(date) {
|
|
544
|
+
const year = date.getFullYear();
|
|
545
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
546
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
547
|
+
return `${year}-${month}-${day}.log`;
|
|
548
|
+
}
|
|
549
|
+
async close() {
|
|
550
|
+
await this.closeStream();
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
function isFileLoggingEnabled() {
|
|
554
|
+
return process.env.LOGGER_FILE_ENABLED === "true";
|
|
555
|
+
}
|
|
556
|
+
function getDefaultLogLevel() {
|
|
557
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
558
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
559
|
+
if (isDevelopment) {
|
|
560
|
+
return "debug";
|
|
561
|
+
}
|
|
562
|
+
if (isProduction) {
|
|
563
|
+
return "info";
|
|
564
|
+
}
|
|
565
|
+
return "warn";
|
|
566
|
+
}
|
|
567
|
+
function getConsoleConfig() {
|
|
568
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
569
|
+
return {
|
|
570
|
+
level: "debug",
|
|
571
|
+
enabled: true,
|
|
572
|
+
colorize: !isProduction
|
|
573
|
+
// Dev: colored output, Production: plain text
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function getFileConfig() {
|
|
577
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
578
|
+
return {
|
|
579
|
+
level: "info",
|
|
580
|
+
enabled: isProduction,
|
|
581
|
+
// File logging in production only
|
|
582
|
+
logDir: process.env.LOG_DIR || "./logs",
|
|
583
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
584
|
+
// 10MB
|
|
585
|
+
maxFiles: 10
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function validateDirectoryWritable(dirPath) {
|
|
589
|
+
if (!existsSync(dirPath)) {
|
|
590
|
+
try {
|
|
591
|
+
mkdirSync(dirPath, { recursive: true });
|
|
592
|
+
} catch (error) {
|
|
593
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
594
|
+
throw new Error(`Failed to create log directory "${dirPath}": ${errorMessage}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
accessSync(dirPath, constants.W_OK);
|
|
599
|
+
} catch {
|
|
600
|
+
throw new Error(`Log directory "${dirPath}" is not writable. Please check permissions.`);
|
|
601
|
+
}
|
|
602
|
+
const testFile = join(dirPath, ".logger-write-test");
|
|
603
|
+
try {
|
|
604
|
+
writeFileSync(testFile, "test", "utf-8");
|
|
605
|
+
unlinkSync(testFile);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
608
|
+
throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function validateFileConfig() {
|
|
612
|
+
if (!isFileLoggingEnabled()) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const logDir = process.env.LOG_DIR;
|
|
616
|
+
if (!logDir) {
|
|
617
|
+
throw new Error(
|
|
618
|
+
"LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp"
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
validateDirectoryWritable(logDir);
|
|
622
|
+
}
|
|
623
|
+
function validateSlackConfig() {
|
|
624
|
+
const webhookUrl = process.env.SLACK_WEBHOOK_URL;
|
|
625
|
+
if (!webhookUrl) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (!webhookUrl.startsWith("https://hooks.slack.com/")) {
|
|
629
|
+
throw new Error(
|
|
630
|
+
`Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"`
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function validateEmailConfig() {
|
|
635
|
+
const smtpHost = process.env.SMTP_HOST;
|
|
636
|
+
const smtpPort = process.env.SMTP_PORT;
|
|
637
|
+
const emailFrom = process.env.EMAIL_FROM;
|
|
638
|
+
const emailTo = process.env.EMAIL_TO;
|
|
639
|
+
const hasAnyEmailConfig = smtpHost || smtpPort || emailFrom || emailTo;
|
|
640
|
+
if (!hasAnyEmailConfig) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const missingFields = [];
|
|
644
|
+
if (!smtpHost) missingFields.push("SMTP_HOST");
|
|
645
|
+
if (!smtpPort) missingFields.push("SMTP_PORT");
|
|
646
|
+
if (!emailFrom) missingFields.push("EMAIL_FROM");
|
|
647
|
+
if (!emailTo) missingFields.push("EMAIL_TO");
|
|
648
|
+
if (missingFields.length > 0) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
`Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
const port = parseInt(smtpPort, 10);
|
|
654
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
`Invalid SMTP_PORT: "${smtpPort}". Must be a number between 1 and 65535.`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
660
|
+
if (!emailRegex.test(emailFrom)) {
|
|
661
|
+
throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`);
|
|
662
|
+
}
|
|
663
|
+
const recipients = emailTo.split(",").map((e) => e.trim());
|
|
664
|
+
for (const email of recipients) {
|
|
665
|
+
if (!emailRegex.test(email)) {
|
|
666
|
+
throw new Error(`Invalid email address in EMAIL_TO: "${email}"`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function validateEnvironment() {
|
|
671
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
672
|
+
if (!nodeEnv) {
|
|
673
|
+
process.stderr.write(
|
|
674
|
+
"[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function validateConfig() {
|
|
679
|
+
try {
|
|
680
|
+
validateEnvironment();
|
|
681
|
+
validateFileConfig();
|
|
682
|
+
validateSlackConfig();
|
|
683
|
+
validateEmailConfig();
|
|
684
|
+
} catch (error) {
|
|
685
|
+
if (error instanceof Error) {
|
|
686
|
+
throw new Error(`[Logger] Configuration validation failed: ${error.message}`);
|
|
687
|
+
}
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/logger/adapters/custom.ts
|
|
693
|
+
function initializeTransports() {
|
|
694
|
+
const transports = [];
|
|
695
|
+
const consoleConfig = getConsoleConfig();
|
|
696
|
+
transports.push(new ConsoleTransport(consoleConfig));
|
|
697
|
+
const fileConfig = getFileConfig();
|
|
698
|
+
if (fileConfig.enabled) {
|
|
699
|
+
transports.push(new FileTransport(fileConfig));
|
|
700
|
+
}
|
|
701
|
+
return transports;
|
|
702
|
+
}
|
|
703
|
+
var CustomAdapter = class _CustomAdapter {
|
|
704
|
+
logger;
|
|
705
|
+
constructor(config) {
|
|
706
|
+
this.logger = new Logger({
|
|
707
|
+
level: config.level,
|
|
708
|
+
module: config.module,
|
|
709
|
+
transports: initializeTransports()
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
child(module) {
|
|
713
|
+
const adapter = new _CustomAdapter({ level: this.logger.level, module });
|
|
714
|
+
adapter.logger = this.logger.child(module);
|
|
715
|
+
return adapter;
|
|
716
|
+
}
|
|
717
|
+
debug(message, context) {
|
|
718
|
+
this.logger.debug(message, context);
|
|
719
|
+
}
|
|
720
|
+
info(message, context) {
|
|
721
|
+
this.logger.info(message, context);
|
|
722
|
+
}
|
|
723
|
+
warn(message, errorOrContext, context) {
|
|
724
|
+
if (errorOrContext instanceof Error) {
|
|
725
|
+
this.logger.warn(message, errorOrContext, context);
|
|
726
|
+
} else {
|
|
727
|
+
this.logger.warn(message, errorOrContext);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
error(message, errorOrContext, context) {
|
|
731
|
+
if (errorOrContext instanceof Error) {
|
|
732
|
+
this.logger.error(message, errorOrContext, context);
|
|
733
|
+
} else {
|
|
734
|
+
this.logger.error(message, errorOrContext);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
fatal(message, errorOrContext, context) {
|
|
738
|
+
if (errorOrContext instanceof Error) {
|
|
739
|
+
this.logger.fatal(message, errorOrContext, context);
|
|
740
|
+
} else {
|
|
741
|
+
this.logger.fatal(message, errorOrContext);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async close() {
|
|
745
|
+
await this.logger.close();
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// src/logger/adapter-factory.ts
|
|
750
|
+
function createAdapter(type) {
|
|
751
|
+
const level = getDefaultLogLevel();
|
|
752
|
+
switch (type) {
|
|
753
|
+
case "pino":
|
|
754
|
+
return new PinoAdapter({ level });
|
|
755
|
+
case "custom":
|
|
756
|
+
return new CustomAdapter({ level });
|
|
757
|
+
default:
|
|
758
|
+
return new PinoAdapter({ level });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function getAdapterType() {
|
|
762
|
+
const adapterEnv = process.env.LOGGER_ADAPTER;
|
|
763
|
+
if (adapterEnv === "custom" || adapterEnv === "pino") {
|
|
764
|
+
return adapterEnv;
|
|
765
|
+
}
|
|
766
|
+
return "pino";
|
|
767
|
+
}
|
|
768
|
+
function initializeLogger() {
|
|
769
|
+
validateConfig();
|
|
770
|
+
return createAdapter(getAdapterType());
|
|
771
|
+
}
|
|
772
|
+
var logger = initializeLogger();
|
|
773
|
+
|
|
774
|
+
// src/codegen/built-in/contract/scanner.ts
|
|
775
|
+
var scannerLogger = logger.child("contract-scanner");
|
|
776
|
+
async function scanContracts(contractsDir, packagePrefix) {
|
|
777
|
+
scannerLogger.debug("Starting contract scan", { contractsDir, packagePrefix });
|
|
778
|
+
const contractFiles = await scanContractFiles(contractsDir);
|
|
779
|
+
scannerLogger.debug("Found contract files", { count: contractFiles.length, files: contractFiles });
|
|
780
|
+
const mappings = [];
|
|
781
|
+
for (let i = 0; i < contractFiles.length; i++) {
|
|
782
|
+
const filePath = contractFiles[i];
|
|
783
|
+
scannerLogger.debug("Extracting contracts from file", { filePath });
|
|
784
|
+
const exports = extractContractExports(filePath);
|
|
785
|
+
scannerLogger.debug("Extracted contracts", { filePath, count: exports.length, contracts: exports.map((e) => e.name) });
|
|
786
|
+
for (let j = 0; j < exports.length; j++) {
|
|
787
|
+
const contractExport = exports[j];
|
|
788
|
+
scannerLogger.debug("Processing contract", { name: contractExport.name, method: contractExport.method, path: contractExport.path });
|
|
789
|
+
if (!contractExport.path.startsWith("/")) {
|
|
790
|
+
throw new Error(
|
|
791
|
+
`Contract '${contractExport.name}' in ${filePath} must use absolute path. Found: '${contractExport.path}'. Use '/your-path' instead.`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
if (packagePrefix && !contractExport.path.startsWith(packagePrefix)) {
|
|
795
|
+
throw new Error(
|
|
796
|
+
`Contract '${contractExport.name}' in ${filePath} must include package prefix. Expected path to start with '${packagePrefix}', but found: '${contractExport.path}'. Example: path: '${packagePrefix}/${contractExport.path}'`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
mappings.push({
|
|
800
|
+
method: contractExport.method,
|
|
801
|
+
path: contractExport.path,
|
|
802
|
+
contractName: contractExport.name,
|
|
803
|
+
contractImportPath: getImportPath(filePath),
|
|
804
|
+
routeFile: "",
|
|
805
|
+
contractFile: filePath,
|
|
806
|
+
hasQuery: contractExport.hasQuery,
|
|
807
|
+
hasBody: contractExport.hasBody,
|
|
808
|
+
hasParams: contractExport.hasParams
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
scannerLogger.info("Contract scan completed", { totalMappings: mappings.length });
|
|
813
|
+
return mappings;
|
|
814
|
+
}
|
|
815
|
+
async function scanContractFiles(dir, files = []) {
|
|
816
|
+
try {
|
|
817
|
+
const entries = await readdir(dir);
|
|
818
|
+
for (let i = 0; i < entries.length; i++) {
|
|
819
|
+
const entry = entries[i];
|
|
820
|
+
const fullPath = join(dir, entry);
|
|
821
|
+
const fileStat = await stat(fullPath);
|
|
822
|
+
if (fileStat.isDirectory()) {
|
|
823
|
+
await scanContractFiles(fullPath, files);
|
|
824
|
+
} else {
|
|
825
|
+
if ((entry.endsWith(".ts") || entry.endsWith(".js") || entry.endsWith(".mjs")) && !entry.endsWith(".d.ts") && !entry.endsWith(".test.ts") && !entry.endsWith(".test.js") && !entry.endsWith(".test.mjs")) {
|
|
826
|
+
files.push(fullPath);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
} catch (error) {
|
|
831
|
+
}
|
|
832
|
+
return files;
|
|
833
|
+
}
|
|
834
|
+
function extractContractExports(filePath) {
|
|
835
|
+
const sourceCode = readFileSync(filePath, "utf-8");
|
|
836
|
+
const sourceFile = ts.createSourceFile(
|
|
837
|
+
filePath,
|
|
838
|
+
sourceCode,
|
|
839
|
+
ts.ScriptTarget.Latest,
|
|
840
|
+
true
|
|
841
|
+
);
|
|
842
|
+
const exports = [];
|
|
843
|
+
function visit(node) {
|
|
844
|
+
if (ts.isVariableStatement(node)) {
|
|
845
|
+
const hasExport = node.modifiers?.some(
|
|
846
|
+
(m) => m.kind === ts.SyntaxKind.ExportKeyword
|
|
847
|
+
);
|
|
848
|
+
if (hasExport && node.declarationList.declarations.length > 0) {
|
|
849
|
+
const declaration = node.declarationList.declarations[0];
|
|
850
|
+
if (ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && declaration.initializer) {
|
|
851
|
+
const name = declaration.name.text;
|
|
852
|
+
const hasSatisfiesRouteContract = checkSatisfiesRouteContract(declaration.initializer);
|
|
853
|
+
if (hasSatisfiesRouteContract) {
|
|
854
|
+
const objectLiteral = extractObjectLiteral(declaration.initializer);
|
|
855
|
+
if (objectLiteral) {
|
|
856
|
+
const contractData = extractContractData(objectLiteral);
|
|
857
|
+
if (contractData.method && contractData.path) {
|
|
858
|
+
exports.push({
|
|
859
|
+
name,
|
|
860
|
+
method: contractData.method,
|
|
861
|
+
path: contractData.path,
|
|
862
|
+
hasQuery: contractData.hasQuery,
|
|
863
|
+
hasBody: contractData.hasBody,
|
|
864
|
+
hasParams: contractData.hasParams
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (isContractName(name)) {
|
|
871
|
+
const objectLiteral = extractObjectLiteral(declaration.initializer);
|
|
872
|
+
if (objectLiteral) {
|
|
873
|
+
const contractData = extractContractData(objectLiteral);
|
|
874
|
+
if (contractData.method && contractData.path) {
|
|
875
|
+
exports.push({
|
|
876
|
+
name,
|
|
877
|
+
method: contractData.method,
|
|
878
|
+
path: contractData.path,
|
|
879
|
+
hasQuery: contractData.hasQuery,
|
|
880
|
+
hasBody: contractData.hasBody,
|
|
881
|
+
hasParams: contractData.hasParams
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
ts.forEachChild(node, visit);
|
|
890
|
+
}
|
|
891
|
+
visit(sourceFile);
|
|
892
|
+
return exports;
|
|
893
|
+
}
|
|
894
|
+
function checkSatisfiesRouteContract(initializer) {
|
|
895
|
+
if (!ts.isSatisfiesExpression(initializer)) {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
const typeNode = initializer.type;
|
|
899
|
+
if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
900
|
+
return typeNode.typeName.text === "RouteContract";
|
|
901
|
+
}
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
function extractObjectLiteral(initializer) {
|
|
905
|
+
if (ts.isObjectLiteralExpression(initializer)) {
|
|
906
|
+
return initializer;
|
|
907
|
+
}
|
|
908
|
+
if (ts.isSatisfiesExpression(initializer)) {
|
|
909
|
+
return extractObjectLiteral(initializer.expression);
|
|
910
|
+
}
|
|
911
|
+
if (ts.isAsExpression(initializer)) {
|
|
912
|
+
return extractObjectLiteral(initializer.expression);
|
|
913
|
+
}
|
|
914
|
+
return void 0;
|
|
915
|
+
}
|
|
916
|
+
function extractContractData(objectLiteral) {
|
|
917
|
+
const result = {};
|
|
918
|
+
for (let i = 0; i < objectLiteral.properties.length; i++) {
|
|
919
|
+
const prop = objectLiteral.properties[i];
|
|
920
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
921
|
+
const propName = prop.name.text;
|
|
922
|
+
if (propName === "method") {
|
|
923
|
+
let value;
|
|
924
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
925
|
+
value = prop.initializer.text;
|
|
926
|
+
} else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
|
|
927
|
+
value = prop.initializer.expression.text;
|
|
928
|
+
}
|
|
929
|
+
if (value) result.method = value;
|
|
930
|
+
} else if (propName === "path") {
|
|
931
|
+
let value;
|
|
932
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
933
|
+
value = prop.initializer.text;
|
|
934
|
+
} else if (ts.isAsExpression(prop.initializer) && ts.isStringLiteral(prop.initializer.expression)) {
|
|
935
|
+
value = prop.initializer.expression.text;
|
|
936
|
+
}
|
|
937
|
+
if (value) result.path = value;
|
|
938
|
+
} else if (propName === "query") {
|
|
939
|
+
result.hasQuery = true;
|
|
940
|
+
} else if (propName === "body") {
|
|
941
|
+
result.hasBody = true;
|
|
942
|
+
} else if (propName === "params") {
|
|
943
|
+
result.hasParams = true;
|
|
944
|
+
}
|
|
1050
945
|
}
|
|
1051
946
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
947
|
+
return result;
|
|
948
|
+
}
|
|
949
|
+
function isContractName(name) {
|
|
950
|
+
return name.indexOf("Contract") !== -1 || name.indexOf("contract") !== -1 || name.endsWith("Schema") || name.endsWith("schema");
|
|
951
|
+
}
|
|
952
|
+
function getImportPath(filePath) {
|
|
953
|
+
const srcIndex = filePath.indexOf("/src/");
|
|
954
|
+
if (srcIndex === -1) {
|
|
955
|
+
throw new Error(`Cannot determine import path for ${filePath}: /src/ directory not found`);
|
|
1060
956
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
957
|
+
let cleanPath = filePath.substring(srcIndex + 5);
|
|
958
|
+
if (cleanPath.endsWith(".ts")) {
|
|
959
|
+
cleanPath = cleanPath.slice(0, -3);
|
|
960
|
+
} else if (cleanPath.endsWith(".js")) {
|
|
961
|
+
cleanPath = cleanPath.slice(0, -3);
|
|
962
|
+
} else if (cleanPath.endsWith(".mjs")) {
|
|
963
|
+
cleanPath = cleanPath.slice(0, -4);
|
|
1063
964
|
}
|
|
1064
|
-
|
|
1065
|
-
function isFileLoggingEnabled() {
|
|
1066
|
-
return process.env.LOGGER_FILE_ENABLED === "true";
|
|
965
|
+
return "@/" + cleanPath;
|
|
1067
966
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
967
|
+
|
|
968
|
+
// src/codegen/built-in/contract/helpers.ts
|
|
969
|
+
function groupByResource(mappings) {
|
|
970
|
+
const grouped = {};
|
|
971
|
+
for (let i = 0; i < mappings.length; i++) {
|
|
972
|
+
const mapping = mappings[i];
|
|
973
|
+
const resource = extractResourceName(mapping.path);
|
|
974
|
+
if (!grouped[resource]) {
|
|
975
|
+
grouped[resource] = [];
|
|
976
|
+
}
|
|
977
|
+
grouped[resource].push(mapping);
|
|
1073
978
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
979
|
+
return grouped;
|
|
980
|
+
}
|
|
981
|
+
function extractResourceName(path) {
|
|
982
|
+
let processedPath = path;
|
|
983
|
+
if (!processedPath.startsWith("/")) {
|
|
984
|
+
processedPath = "/" + processedPath;
|
|
1076
985
|
}
|
|
1077
|
-
|
|
986
|
+
const segments = processedPath.slice(1).split("/").filter((s) => s && s !== "*");
|
|
987
|
+
const staticSegments = [];
|
|
988
|
+
for (let i = 0; i < segments.length; i++) {
|
|
989
|
+
const seg = segments[i];
|
|
990
|
+
if (!seg.startsWith(":")) {
|
|
991
|
+
staticSegments.push(seg);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (staticSegments.length === 0) {
|
|
995
|
+
return "root";
|
|
996
|
+
}
|
|
997
|
+
const first = toCamelCase(staticSegments[0], false);
|
|
998
|
+
if (staticSegments.length === 1) {
|
|
999
|
+
return first;
|
|
1000
|
+
}
|
|
1001
|
+
const result = [first];
|
|
1002
|
+
for (let i = 1; i < staticSegments.length; i++) {
|
|
1003
|
+
const seg = staticSegments[i];
|
|
1004
|
+
result.push(toCamelCase(seg, true));
|
|
1005
|
+
}
|
|
1006
|
+
return result.join("");
|
|
1078
1007
|
}
|
|
1079
|
-
function
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1008
|
+
function toCamelCase(str, capitalize) {
|
|
1009
|
+
const parts = str.split(/[-_]/);
|
|
1010
|
+
if (parts.length === 1) {
|
|
1011
|
+
return capitalize ? str.charAt(0).toUpperCase() + str.slice(1) : str;
|
|
1012
|
+
}
|
|
1013
|
+
const result = [];
|
|
1014
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1015
|
+
const part = parts[i];
|
|
1016
|
+
if (i === 0 && !capitalize) {
|
|
1017
|
+
result.push(part);
|
|
1018
|
+
} else {
|
|
1019
|
+
result.push(part.charAt(0).toUpperCase() + part.slice(1));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return result.join("");
|
|
1087
1023
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1024
|
+
|
|
1025
|
+
// src/codegen/built-in/contract/emitter.ts
|
|
1026
|
+
async function generateClient(mappings, options) {
|
|
1027
|
+
const startTime = Date.now();
|
|
1028
|
+
const grouped = groupByResource(mappings);
|
|
1029
|
+
const resourceNames = Object.keys(grouped);
|
|
1030
|
+
await generateSplitClient(mappings, grouped, options);
|
|
1090
1031
|
return {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
maxFiles: 10
|
|
1032
|
+
routesScanned: mappings.length,
|
|
1033
|
+
contractsFound: mappings.length,
|
|
1034
|
+
contractFiles: countUniqueContractFiles(mappings),
|
|
1035
|
+
resourcesGenerated: resourceNames.length,
|
|
1036
|
+
methodsGenerated: mappings.length,
|
|
1037
|
+
duration: Date.now() - startTime
|
|
1098
1038
|
};
|
|
1099
1039
|
}
|
|
1100
|
-
function
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1040
|
+
function generateHeader() {
|
|
1041
|
+
return `/**
|
|
1042
|
+
* Auto-generated API Client
|
|
1043
|
+
*
|
|
1044
|
+
* Generated by @spfn/core codegen
|
|
1045
|
+
* DO NOT EDIT MANUALLY
|
|
1046
|
+
*
|
|
1047
|
+
* @generated ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1048
|
+
*/
|
|
1049
|
+
|
|
1050
|
+
`;
|
|
1051
|
+
}
|
|
1052
|
+
function groupContractsByImportPath(mappings) {
|
|
1053
|
+
const groups = {};
|
|
1054
|
+
for (let i = 0; i < mappings.length; i++) {
|
|
1055
|
+
const mapping = mappings[i];
|
|
1056
|
+
const path = mapping.contractImportPath;
|
|
1057
|
+
if (!groups[path]) {
|
|
1058
|
+
groups[path] = /* @__PURE__ */ new Set();
|
|
1107
1059
|
}
|
|
1060
|
+
groups[path].add(mapping.contractName);
|
|
1108
1061
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
const testFile = join(dirPath, ".logger-write-test");
|
|
1115
|
-
try {
|
|
1116
|
-
writeFileSync(testFile, "test", "utf-8");
|
|
1117
|
-
unlinkSync(testFile);
|
|
1118
|
-
} catch (error) {
|
|
1119
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1120
|
-
throw new Error(`Cannot write to log directory "${dirPath}": ${errorMessage}`);
|
|
1062
|
+
const result = {};
|
|
1063
|
+
const keys = Object.keys(groups);
|
|
1064
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1065
|
+
const key = keys[i];
|
|
1066
|
+
result[key] = Array.from(groups[key]);
|
|
1121
1067
|
}
|
|
1068
|
+
return result;
|
|
1122
1069
|
}
|
|
1123
|
-
function
|
|
1124
|
-
|
|
1125
|
-
|
|
1070
|
+
function generateTypeName(mapping) {
|
|
1071
|
+
let name = mapping.contractName;
|
|
1072
|
+
if (name.endsWith("Contract")) {
|
|
1073
|
+
name = name.slice(0, -8);
|
|
1126
1074
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
throw new Error(
|
|
1130
|
-
"LOG_DIR environment variable is required when LOGGER_FILE_ENABLED=true. Example: LOG_DIR=/var/log/myapp"
|
|
1131
|
-
);
|
|
1075
|
+
if (name.length > 0) {
|
|
1076
|
+
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
1132
1077
|
}
|
|
1133
|
-
|
|
1078
|
+
return name;
|
|
1134
1079
|
}
|
|
1135
|
-
function
|
|
1136
|
-
|
|
1137
|
-
if (
|
|
1138
|
-
|
|
1139
|
-
}
|
|
1140
|
-
if (!webhookUrl.startsWith("https://hooks.slack.com/")) {
|
|
1141
|
-
throw new Error(
|
|
1142
|
-
`Invalid SLACK_WEBHOOK_URL: "${webhookUrl}". Slack webhook URLs must start with "https://hooks.slack.com/"`
|
|
1143
|
-
);
|
|
1080
|
+
function generateFunctionName(mapping) {
|
|
1081
|
+
let name = mapping.contractName;
|
|
1082
|
+
if (name.endsWith("Contract")) {
|
|
1083
|
+
name = name.slice(0, -8);
|
|
1144
1084
|
}
|
|
1085
|
+
return name;
|
|
1145
1086
|
}
|
|
1146
|
-
function
|
|
1147
|
-
const
|
|
1148
|
-
const
|
|
1149
|
-
const
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
if (
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
if (!emailTo) missingFields.push("EMAIL_TO");
|
|
1160
|
-
if (missingFields.length > 0) {
|
|
1161
|
-
throw new Error(
|
|
1162
|
-
`Email transport configuration incomplete. Missing: ${missingFields.join(", ")}. Either set all required fields or remove all email configuration.`
|
|
1163
|
-
);
|
|
1087
|
+
function generateFunctionCode(mapping, options) {
|
|
1088
|
+
const functionName = generateFunctionName(mapping);
|
|
1089
|
+
const hasParams = mapping.hasParams || mapping.path.includes(":");
|
|
1090
|
+
const hasQuery = mapping.hasQuery || false;
|
|
1091
|
+
const hasBody = mapping.hasBody || false;
|
|
1092
|
+
let code = "";
|
|
1093
|
+
if (options.includeJsDoc !== false) {
|
|
1094
|
+
code += `/**
|
|
1095
|
+
`;
|
|
1096
|
+
code += ` * ${mapping.method} ${mapping.path}
|
|
1097
|
+
`;
|
|
1098
|
+
code += ` */
|
|
1099
|
+
`;
|
|
1164
1100
|
}
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
);
|
|
1101
|
+
code += `export const ${functionName} = (`;
|
|
1102
|
+
const params = [];
|
|
1103
|
+
const typeName = generateTypeName(mapping);
|
|
1104
|
+
if (hasParams) {
|
|
1105
|
+
params.push(`params: ${typeName}Params`);
|
|
1170
1106
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
throw new Error(`Invalid EMAIL_FROM format: "${emailFrom}"`);
|
|
1107
|
+
if (hasQuery) {
|
|
1108
|
+
params.push(`query?: ${typeName}Query`);
|
|
1174
1109
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if (!emailRegex.test(email)) {
|
|
1178
|
-
throw new Error(`Invalid email address in EMAIL_TO: "${email}"`);
|
|
1179
|
-
}
|
|
1110
|
+
if (hasBody) {
|
|
1111
|
+
params.push(`body: ${typeName}Body`);
|
|
1180
1112
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const nodeEnv = process.env.NODE_ENV;
|
|
1184
|
-
if (!nodeEnv) {
|
|
1185
|
-
process.stderr.write(
|
|
1186
|
-
"[Logger] Warning: NODE_ENV is not set. Defaulting to test environment.\n"
|
|
1187
|
-
);
|
|
1113
|
+
if (params.length > 0) {
|
|
1114
|
+
code += `options: { ${params.join(", ")} }`;
|
|
1188
1115
|
}
|
|
1116
|
+
code += `) => `;
|
|
1117
|
+
code += `client.call(${mapping.contractName}`;
|
|
1118
|
+
if (params.length > 0) {
|
|
1119
|
+
code += `, options`;
|
|
1120
|
+
}
|
|
1121
|
+
code += `);
|
|
1122
|
+
|
|
1123
|
+
`;
|
|
1124
|
+
return code;
|
|
1189
1125
|
}
|
|
1190
|
-
function
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
validateEmailConfig();
|
|
1196
|
-
} catch (error) {
|
|
1197
|
-
if (error instanceof Error) {
|
|
1198
|
-
throw new Error(`[Logger] Configuration validation failed: ${error.message}`);
|
|
1126
|
+
function countUniqueContractFiles(mappings) {
|
|
1127
|
+
const files = /* @__PURE__ */ new Set();
|
|
1128
|
+
for (let i = 0; i < mappings.length; i++) {
|
|
1129
|
+
if (mappings[i].contractFile) {
|
|
1130
|
+
files.add(mappings[i].contractFile);
|
|
1199
1131
|
}
|
|
1200
|
-
throw error;
|
|
1201
1132
|
}
|
|
1133
|
+
return files.size;
|
|
1202
1134
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const transports = [];
|
|
1207
|
-
const consoleConfig = getConsoleConfig();
|
|
1208
|
-
transports.push(new ConsoleTransport(consoleConfig));
|
|
1209
|
-
const fileConfig = getFileConfig();
|
|
1210
|
-
if (fileConfig.enabled) {
|
|
1211
|
-
transports.push(new FileTransport(fileConfig));
|
|
1135
|
+
function toKebabCase(str) {
|
|
1136
|
+
if (str.length === 0) {
|
|
1137
|
+
return str;
|
|
1212
1138
|
}
|
|
1213
|
-
return
|
|
1139
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1214
1140
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
return adapter;
|
|
1141
|
+
async function generateSplitClient(_mappings, grouped, options) {
|
|
1142
|
+
const outputPath = options.outputPath;
|
|
1143
|
+
const outputDir = outputPath.endsWith(".ts") || outputPath.endsWith(".js") ? outputPath.replace(/\.[jt]s$/, "") : outputPath;
|
|
1144
|
+
await mkdir(outputDir, { recursive: true });
|
|
1145
|
+
const resourceNames = Object.keys(grouped);
|
|
1146
|
+
for (let i = 0; i < resourceNames.length; i++) {
|
|
1147
|
+
const resourceName = resourceNames[i];
|
|
1148
|
+
const routes = grouped[resourceName];
|
|
1149
|
+
const code = generateResourceFile(resourceName, routes, options);
|
|
1150
|
+
const kebabName = toKebabCase(resourceName);
|
|
1151
|
+
const filePath = `${outputDir}/${kebabName}.ts`;
|
|
1152
|
+
await writeFile(filePath, code, "utf-8");
|
|
1228
1153
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1154
|
+
const indexCode = generateIndexFile(grouped, options);
|
|
1155
|
+
const indexPath = `${outputDir}/index.ts`;
|
|
1156
|
+
await writeFile(indexPath, indexCode, "utf-8");
|
|
1157
|
+
}
|
|
1158
|
+
function generateResourceFile(_resourceName, routes, options) {
|
|
1159
|
+
let code = "";
|
|
1160
|
+
code += generateHeader();
|
|
1161
|
+
code += `import { client } from '@spfn/core/client';
|
|
1162
|
+
`;
|
|
1163
|
+
if (options.includeTypes !== false) {
|
|
1164
|
+
code += `import type { InferContract } from '@spfn/core';
|
|
1165
|
+
`;
|
|
1231
1166
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1167
|
+
code += `
|
|
1168
|
+
`;
|
|
1169
|
+
const importGroups = groupContractsByImportPath(routes);
|
|
1170
|
+
const importPaths = Object.keys(importGroups);
|
|
1171
|
+
for (let i = 0; i < importPaths.length; i++) {
|
|
1172
|
+
const importPath = importPaths[i];
|
|
1173
|
+
const contracts = importGroups[importPath];
|
|
1174
|
+
code += `import { ${contracts.join(", ")} } from '${importPath}';
|
|
1175
|
+
`;
|
|
1234
1176
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1177
|
+
code += `
|
|
1178
|
+
`;
|
|
1179
|
+
if (options.includeTypes !== false) {
|
|
1180
|
+
code += `// ============================================
|
|
1181
|
+
`;
|
|
1182
|
+
code += `// Types
|
|
1183
|
+
`;
|
|
1184
|
+
code += `// ============================================
|
|
1185
|
+
|
|
1186
|
+
`;
|
|
1187
|
+
for (let i = 0; i < routes.length; i++) {
|
|
1188
|
+
const route = routes[i];
|
|
1189
|
+
const typeName = generateTypeName(route);
|
|
1190
|
+
const contractType = `typeof ${route.contractName}`;
|
|
1191
|
+
code += `export type ${typeName}Response = InferContract<${contractType}>['response'];
|
|
1192
|
+
`;
|
|
1193
|
+
if (route.hasQuery) {
|
|
1194
|
+
code += `export type ${typeName}Query = InferContract<${contractType}>['query'];
|
|
1195
|
+
`;
|
|
1196
|
+
}
|
|
1197
|
+
if (route.hasParams || route.path.includes(":")) {
|
|
1198
|
+
code += `export type ${typeName}Params = InferContract<${contractType}>['params'];
|
|
1199
|
+
`;
|
|
1200
|
+
}
|
|
1201
|
+
if (route.hasBody) {
|
|
1202
|
+
code += `export type ${typeName}Body = InferContract<${contractType}>['body'];
|
|
1203
|
+
`;
|
|
1204
|
+
}
|
|
1205
|
+
code += `
|
|
1206
|
+
`;
|
|
1240
1207
|
}
|
|
1241
1208
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1209
|
+
code += `// ============================================
|
|
1210
|
+
`;
|
|
1211
|
+
code += `// API Functions
|
|
1212
|
+
`;
|
|
1213
|
+
code += `// ============================================
|
|
1214
|
+
|
|
1215
|
+
`;
|
|
1216
|
+
for (let i = 0; i < routes.length; i++) {
|
|
1217
|
+
const route = routes[i];
|
|
1218
|
+
code += generateFunctionCode(route, options);
|
|
1248
1219
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1220
|
+
return code;
|
|
1221
|
+
}
|
|
1222
|
+
function generateIndexFile(grouped, options) {
|
|
1223
|
+
let code = "";
|
|
1224
|
+
const apiName = options.apiName || "api";
|
|
1225
|
+
const resourceNames = Object.keys(grouped);
|
|
1226
|
+
code += generateHeader();
|
|
1227
|
+
code += `export { client } from '@spfn/core/client';
|
|
1228
|
+
|
|
1229
|
+
`;
|
|
1230
|
+
for (let i = 0; i < resourceNames.length; i++) {
|
|
1231
|
+
const resourceName = resourceNames[i];
|
|
1232
|
+
const routes = grouped[resourceName];
|
|
1233
|
+
const kebabName = toKebabCase(resourceName);
|
|
1234
|
+
const typeNames = [];
|
|
1235
|
+
for (let j = 0; j < routes.length; j++) {
|
|
1236
|
+
const route = routes[j];
|
|
1237
|
+
const typeName = generateTypeName(route);
|
|
1238
|
+
typeNames.push(`${typeName}Response`);
|
|
1239
|
+
if (route.hasQuery) {
|
|
1240
|
+
typeNames.push(`${typeName}Query`);
|
|
1241
|
+
}
|
|
1242
|
+
if (route.hasParams || route.path.includes(":")) {
|
|
1243
|
+
typeNames.push(`${typeName}Params`);
|
|
1244
|
+
}
|
|
1245
|
+
if (route.hasBody) {
|
|
1246
|
+
typeNames.push(`${typeName}Body`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (typeNames.length > 0) {
|
|
1250
|
+
code += `export type { ${typeNames.join(", ")} } from './${kebabName}.js';
|
|
1251
|
+
`;
|
|
1254
1252
|
}
|
|
1255
1253
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
case "pino":
|
|
1266
|
-
return new PinoAdapter({ level });
|
|
1267
|
-
case "custom":
|
|
1268
|
-
return new CustomAdapter({ level });
|
|
1269
|
-
default:
|
|
1270
|
-
return new PinoAdapter({ level });
|
|
1254
|
+
code += `
|
|
1255
|
+
`;
|
|
1256
|
+
for (let i = 0; i < resourceNames.length; i++) {
|
|
1257
|
+
const resourceName = resourceNames[i];
|
|
1258
|
+
const routes = grouped[resourceName];
|
|
1259
|
+
const kebabName = toKebabCase(resourceName);
|
|
1260
|
+
const functionNames = routes.map((route) => generateFunctionName(route));
|
|
1261
|
+
code += `import { ${functionNames.join(", ")} } from './${kebabName}.js';
|
|
1262
|
+
`;
|
|
1271
1263
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1264
|
+
code += `
|
|
1265
|
+
`;
|
|
1266
|
+
code += `/**
|
|
1267
|
+
`;
|
|
1268
|
+
code += ` * Type-safe API client
|
|
1269
|
+
`;
|
|
1270
|
+
code += ` */
|
|
1271
|
+
`;
|
|
1272
|
+
code += `export const ${apiName} = {
|
|
1273
|
+
`;
|
|
1274
|
+
let isFirst = true;
|
|
1275
|
+
for (let i = 0; i < resourceNames.length; i++) {
|
|
1276
|
+
const resourceName = resourceNames[i];
|
|
1277
|
+
const routes = grouped[resourceName];
|
|
1278
|
+
for (let j = 0; j < routes.length; j++) {
|
|
1279
|
+
const route = routes[j];
|
|
1280
|
+
const functionName = generateFunctionName(route);
|
|
1281
|
+
if (!isFirst) {
|
|
1282
|
+
code += `,
|
|
1283
|
+
`;
|
|
1284
|
+
}
|
|
1285
|
+
code += ` ${functionName}`;
|
|
1286
|
+
isFirst = false;
|
|
1287
|
+
}
|
|
1277
1288
|
}
|
|
1278
|
-
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
return createAdapter(getAdapterType());
|
|
1289
|
+
code += `
|
|
1290
|
+
} as const;
|
|
1291
|
+
`;
|
|
1292
|
+
return code;
|
|
1283
1293
|
}
|
|
1284
|
-
var logger = initializeLogger();
|
|
1285
1294
|
|
|
1286
1295
|
// src/codegen/built-in/contract/index.ts
|
|
1287
1296
|
var contractLogger = logger.child("contract-gen");
|