opencroc 0.1.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +37 -6
- package/README.ja.md +37 -6
- package/README.md +37 -6
- package/README.zh-CN.md +37 -6
- package/dist/cli/index.js +1936 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +335 -26
- package/dist/index.js +2621 -45
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,123 +1,2699 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
var init_esm_shims = __esm({
|
|
15
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// src/parsers/controller-parser.ts
|
|
21
|
+
var controller_parser_exports = {};
|
|
22
|
+
__export(controller_parser_exports, {
|
|
23
|
+
createControllerParser: () => createControllerParser,
|
|
24
|
+
inferRelatedTables: () => inferRelatedTables,
|
|
25
|
+
parseControllerDirectory: () => parseControllerDirectory,
|
|
26
|
+
parseControllerFile: () => parseControllerFile
|
|
27
|
+
});
|
|
28
|
+
import * as fs2 from "fs";
|
|
29
|
+
import * as path3 from "path";
|
|
30
|
+
import {
|
|
31
|
+
Project as Project2,
|
|
32
|
+
SyntaxKind as SyntaxKind2
|
|
33
|
+
} from "ts-morph";
|
|
34
|
+
function parseControllerFile(filePath) {
|
|
35
|
+
const absolutePath = path3.resolve(filePath);
|
|
36
|
+
if (!fs2.existsSync(absolutePath)) return [];
|
|
37
|
+
try {
|
|
38
|
+
const project = new Project2({ compilerOptions: { strict: false } });
|
|
39
|
+
const sourceFile = project.addSourceFileAtPath(absolutePath);
|
|
40
|
+
const endpoints = [];
|
|
41
|
+
endpoints.push(...extractRouterCalls(sourceFile));
|
|
42
|
+
endpoints.push(...extractBaseCrudRoutes(sourceFile));
|
|
43
|
+
return deduplicateEndpoints(endpoints);
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function parseControllerDirectory(dirPath) {
|
|
49
|
+
const absoluteDir = path3.resolve(dirPath);
|
|
50
|
+
if (!fs2.existsSync(absoluteDir)) return [];
|
|
51
|
+
const files = fs2.readdirSync(absoluteDir).filter(
|
|
52
|
+
(f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts"
|
|
53
|
+
);
|
|
54
|
+
const endpoints = [];
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
endpoints.push(...parseControllerFile(path3.join(absoluteDir, file)));
|
|
57
|
+
}
|
|
58
|
+
return deduplicateEndpoints(endpoints);
|
|
59
|
+
}
|
|
60
|
+
function extractRouterCalls(sourceFile) {
|
|
61
|
+
const endpoints = [];
|
|
62
|
+
const calls = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
63
|
+
for (const call of calls) {
|
|
64
|
+
const expr = call.getExpression();
|
|
65
|
+
if (expr.getKind() !== SyntaxKind2.PropertyAccessExpression) continue;
|
|
66
|
+
const propAccess = expr;
|
|
67
|
+
const methodName = propAccess.getName().toLowerCase();
|
|
68
|
+
if (!HTTP_METHODS.has(methodName)) continue;
|
|
69
|
+
const objectText = propAccess.getExpression().getText().trim();
|
|
70
|
+
if (!isRouterLike(objectText)) continue;
|
|
71
|
+
const args = call.getArguments();
|
|
72
|
+
if (args.length === 0) continue;
|
|
73
|
+
const routePath = resolveRoutePath(args[0], sourceFile);
|
|
74
|
+
if (!routePath) continue;
|
|
75
|
+
endpoints.push({
|
|
76
|
+
method: METHOD_MAP[methodName],
|
|
77
|
+
path: routePath,
|
|
78
|
+
pathParams: extractPathParams(routePath),
|
|
79
|
+
queryParams: [],
|
|
80
|
+
bodyFields: [],
|
|
81
|
+
responseFields: [],
|
|
82
|
+
relatedTables: [],
|
|
83
|
+
description: extractDescription(call)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return endpoints;
|
|
87
|
+
}
|
|
88
|
+
function isRouterLike(text) {
|
|
89
|
+
return text === "router" || text === "this.router";
|
|
90
|
+
}
|
|
91
|
+
function extractBaseCrudRoutes(sourceFile) {
|
|
92
|
+
const endpoints = [];
|
|
93
|
+
let isBaseCrud = false;
|
|
94
|
+
for (const cls of sourceFile.getClasses()) {
|
|
95
|
+
const heritage = cls.getExtends();
|
|
96
|
+
if (heritage?.getText().includes("BaseCrudController")) {
|
|
97
|
+
isBaseCrud = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!isBaseCrud) return endpoints;
|
|
102
|
+
const calls = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression);
|
|
103
|
+
let resourcePath = null;
|
|
104
|
+
for (const call of calls) {
|
|
105
|
+
const exprText = call.getExpression().getText();
|
|
106
|
+
if ((exprText === "super.registerRoutes" || exprText.endsWith(".registerRoutes")) && !exprText.includes("Custom")) {
|
|
107
|
+
const args = call.getArguments();
|
|
108
|
+
if (args.length >= 2) resourcePath = extractStringLiteral(args[1]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!resourcePath) return endpoints;
|
|
112
|
+
const basePath = `/v1/:tenantId/${resourcePath}`;
|
|
113
|
+
const crudRoutes = [
|
|
114
|
+
{ method: "GET", path: basePath, desc: `List ${resourcePath}` },
|
|
115
|
+
{ method: "GET", path: `${basePath}/:id`, desc: `Get ${resourcePath} by ID` },
|
|
116
|
+
{ method: "POST", path: basePath, desc: `Create ${resourcePath}` },
|
|
117
|
+
{ method: "PUT", path: `${basePath}/:id`, desc: `Update ${resourcePath}` },
|
|
118
|
+
{ method: "DELETE", path: `${basePath}/:id`, desc: `Delete ${resourcePath}` },
|
|
119
|
+
{ method: "POST", path: `${basePath}/batch-delete`, desc: `Batch delete ${resourcePath}` }
|
|
120
|
+
];
|
|
121
|
+
for (const route of crudRoutes) {
|
|
122
|
+
endpoints.push({
|
|
123
|
+
method: route.method,
|
|
124
|
+
path: route.path,
|
|
125
|
+
pathParams: extractPathParams(route.path),
|
|
126
|
+
queryParams: [],
|
|
127
|
+
bodyFields: [],
|
|
128
|
+
responseFields: [],
|
|
129
|
+
relatedTables: [],
|
|
130
|
+
description: route.desc
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return endpoints;
|
|
134
|
+
}
|
|
135
|
+
function inferRelatedTables(servicePaths) {
|
|
136
|
+
const tables = /* @__PURE__ */ new Set();
|
|
137
|
+
for (const sp of servicePaths) {
|
|
138
|
+
const absolutePath = path3.resolve(sp);
|
|
139
|
+
if (!fs2.existsSync(absolutePath)) continue;
|
|
140
|
+
try {
|
|
141
|
+
const content = fs2.readFileSync(absolutePath, "utf-8");
|
|
142
|
+
const importRegex = /import\s*\{([^}]+)\}\s*from\s*['"][^'"]*models[^'"]*['"]/g;
|
|
143
|
+
let match;
|
|
144
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
145
|
+
const names = match[1].split(",").map((s) => s.trim());
|
|
146
|
+
for (const name of names) {
|
|
147
|
+
const cleanName = name.replace(/\s+as\s+\w+/, "").trim();
|
|
148
|
+
if (cleanName) tables.add(pascalToSnake(cleanName));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return Array.from(tables);
|
|
155
|
+
}
|
|
156
|
+
function resolveRoutePath(node, sourceFile) {
|
|
157
|
+
const kind = node.getKind();
|
|
158
|
+
if (kind === SyntaxKind2.StringLiteral) return node.getText().slice(1, -1);
|
|
159
|
+
if (kind === SyntaxKind2.TemplateExpression || kind === SyntaxKind2.NoSubstitutionTemplateLiteral) {
|
|
160
|
+
return resolveTemplateLiteral(node, sourceFile);
|
|
161
|
+
}
|
|
162
|
+
if (kind === SyntaxKind2.Identifier) {
|
|
163
|
+
return resolveVariableValue(sourceFile, node.getText().trim());
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function resolveTemplateLiteral(node, sourceFile) {
|
|
168
|
+
let result = node.getText().slice(1, -1);
|
|
169
|
+
result = result.replace(/\$\{([^}]+)\}/g, (_match, expr) => {
|
|
170
|
+
const resolved = resolveVariableValue(sourceFile, expr.trim());
|
|
171
|
+
return resolved || `{${expr.trim()}}`;
|
|
172
|
+
});
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
function resolveVariableValue(sourceFile, varName) {
|
|
176
|
+
for (const decl of sourceFile.getDescendantsOfKind(SyntaxKind2.VariableDeclaration)) {
|
|
177
|
+
if (decl.getName() === varName) {
|
|
178
|
+
const init = decl.getInitializer();
|
|
179
|
+
if (!init) continue;
|
|
180
|
+
const t = init.getText().trim();
|
|
181
|
+
if (t.startsWith("'") && t.endsWith("'") || t.startsWith('"') && t.endsWith('"'))
|
|
182
|
+
return t.slice(1, -1);
|
|
183
|
+
if (t.startsWith("`") && t.endsWith("`"))
|
|
184
|
+
return resolveTemplateLiteral(init, sourceFile);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
function extractPathParams(routePath) {
|
|
190
|
+
const params = [];
|
|
191
|
+
const regex = /:(\w+)/g;
|
|
192
|
+
let match;
|
|
193
|
+
while ((match = regex.exec(routePath)) !== null) params.push(match[1]);
|
|
194
|
+
return params;
|
|
195
|
+
}
|
|
196
|
+
function extractDescription(call) {
|
|
197
|
+
let current = call;
|
|
198
|
+
while (current.getParent() && current.getParent().getKind() !== SyntaxKind2.SourceFile && current.getParent().getKind() !== SyntaxKind2.Block) {
|
|
199
|
+
current = current.getParent();
|
|
200
|
+
}
|
|
201
|
+
const fullText = current.getFullText();
|
|
202
|
+
const leadingText = fullText.substring(0, fullText.indexOf(current.getText()));
|
|
203
|
+
const jsdocMatch = leadingText.match(/\/\*\*[\s\S]*?\*\s+(.+?)(?:\n|\*\/)/);
|
|
204
|
+
if (jsdocMatch) return jsdocMatch[1].replace(/^\*\s*/, "").trim();
|
|
205
|
+
const lineMatch = leadingText.match(/\/\/\s*(.+)/);
|
|
206
|
+
if (lineMatch) return lineMatch[1].trim();
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
function extractStringLiteral(node) {
|
|
210
|
+
const t = node.getText().trim();
|
|
211
|
+
if (t.startsWith("'") && t.endsWith("'") || t.startsWith('"') && t.endsWith('"'))
|
|
212
|
+
return t.slice(1, -1);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
function pascalToSnake(name) {
|
|
216
|
+
return name.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
217
|
+
}
|
|
218
|
+
function deduplicateEndpoints(endpoints) {
|
|
219
|
+
const seen = /* @__PURE__ */ new Map();
|
|
220
|
+
for (const ep of endpoints) {
|
|
221
|
+
const key = `${ep.method}:${ep.path}`;
|
|
222
|
+
if (!seen.has(key)) {
|
|
223
|
+
seen.set(key, ep);
|
|
224
|
+
} else {
|
|
225
|
+
const existing = seen.get(key);
|
|
226
|
+
const merged = /* @__PURE__ */ new Set([...existing.relatedTables, ...ep.relatedTables]);
|
|
227
|
+
existing.relatedTables = Array.from(merged);
|
|
228
|
+
if (!existing.description && ep.description) existing.description = ep.description;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return Array.from(seen.values());
|
|
232
|
+
}
|
|
233
|
+
function createControllerParser() {
|
|
234
|
+
return {
|
|
235
|
+
async parseFile(filePath) {
|
|
236
|
+
return parseControllerFile(filePath);
|
|
237
|
+
},
|
|
238
|
+
async parseDirectory(dirPath) {
|
|
239
|
+
return parseControllerDirectory(dirPath);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
var HTTP_METHODS, METHOD_MAP;
|
|
244
|
+
var init_controller_parser = __esm({
|
|
245
|
+
"src/parsers/controller-parser.ts"() {
|
|
246
|
+
"use strict";
|
|
247
|
+
init_esm_shims();
|
|
248
|
+
HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "delete", "patch"]);
|
|
249
|
+
METHOD_MAP = {
|
|
250
|
+
get: "GET",
|
|
251
|
+
post: "POST",
|
|
252
|
+
put: "PUT",
|
|
253
|
+
delete: "DELETE",
|
|
254
|
+
patch: "PATCH"
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// src/index.ts
|
|
260
|
+
init_esm_shims();
|
|
261
|
+
|
|
1
262
|
// src/config.ts
|
|
263
|
+
init_esm_shims();
|
|
2
264
|
function defineConfig(config) {
|
|
3
265
|
return config;
|
|
4
266
|
}
|
|
5
267
|
|
|
6
268
|
// src/pipeline/index.ts
|
|
7
|
-
|
|
269
|
+
init_esm_shims();
|
|
270
|
+
import * as fs4 from "fs";
|
|
271
|
+
import * as path5 from "path";
|
|
272
|
+
|
|
273
|
+
// src/parsers/model-parser.ts
|
|
274
|
+
init_esm_shims();
|
|
275
|
+
import * as fs from "fs";
|
|
276
|
+
import * as path2 from "path";
|
|
277
|
+
import {
|
|
278
|
+
Project,
|
|
279
|
+
SyntaxKind
|
|
280
|
+
} from "ts-morph";
|
|
281
|
+
function parseModelFile(filePath) {
|
|
282
|
+
const absolutePath = path2.resolve(filePath);
|
|
283
|
+
if (!fs.existsSync(absolutePath)) return null;
|
|
284
|
+
const project = new Project({ compilerOptions: { strict: false } });
|
|
285
|
+
const sourceFile = project.addSourceFileAtPath(absolutePath);
|
|
286
|
+
const initCall = findInitCall(sourceFile);
|
|
287
|
+
if (!initCall) return null;
|
|
288
|
+
const args = initCall.getArguments();
|
|
289
|
+
if (args.length < 2) return null;
|
|
290
|
+
const fields = parseFieldDefinitions(args[0]);
|
|
291
|
+
const { tableName, indexes } = parseOptions(args[1]);
|
|
292
|
+
if (!tableName) return null;
|
|
293
|
+
return { tableName, fields, indexes };
|
|
294
|
+
}
|
|
295
|
+
function parseModuleModels(modelDir) {
|
|
296
|
+
const absoluteDir = path2.resolve(modelDir);
|
|
297
|
+
if (!fs.existsSync(absoluteDir)) return [];
|
|
298
|
+
const files = fs.readdirSync(absoluteDir).filter(
|
|
299
|
+
(f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts" && f !== "associations.ts"
|
|
300
|
+
);
|
|
301
|
+
const schemas = [];
|
|
302
|
+
for (const file of files) {
|
|
303
|
+
try {
|
|
304
|
+
const schema = parseModelFile(path2.join(absoluteDir, file));
|
|
305
|
+
if (schema) schemas.push(schema);
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return schemas;
|
|
310
|
+
}
|
|
311
|
+
function findInitCall(sourceFile) {
|
|
312
|
+
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
313
|
+
for (const call of calls) {
|
|
314
|
+
const expr = call.getExpression();
|
|
315
|
+
if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
316
|
+
const propAccess = expr.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
|
|
317
|
+
if (propAccess.getName() === "init") return call;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
function parseFieldDefinitions(fieldsNode) {
|
|
323
|
+
const fields = [];
|
|
324
|
+
if (fieldsNode.getKind() !== SyntaxKind.ObjectLiteralExpression) return fields;
|
|
325
|
+
const objLiteral = fieldsNode;
|
|
326
|
+
for (const prop of objLiteral.getProperties()) {
|
|
327
|
+
if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
|
|
328
|
+
const propAssign = prop;
|
|
329
|
+
const initializer = propAssign.getInitializer();
|
|
330
|
+
if (!initializer || initializer.getKind() !== SyntaxKind.ObjectLiteralExpression) continue;
|
|
331
|
+
fields.push(parseFieldObject(propAssign.getName(), initializer));
|
|
332
|
+
}
|
|
333
|
+
return fields;
|
|
334
|
+
}
|
|
335
|
+
function parseFieldObject(fieldName, fieldObj) {
|
|
336
|
+
const field = { name: fieldName, type: "STRING", allowNull: true, primaryKey: false };
|
|
337
|
+
for (const prop of fieldObj.getProperties()) {
|
|
338
|
+
if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
|
|
339
|
+
const propAssign = prop;
|
|
340
|
+
const key = propAssign.getName();
|
|
341
|
+
const init = propAssign.getInitializer();
|
|
342
|
+
if (!init) continue;
|
|
343
|
+
switch (key) {
|
|
344
|
+
case "type":
|
|
345
|
+
field.type = extractDataType(init);
|
|
346
|
+
break;
|
|
347
|
+
case "allowNull":
|
|
348
|
+
field.allowNull = init.getText().trim() === "true";
|
|
349
|
+
break;
|
|
350
|
+
case "primaryKey":
|
|
351
|
+
field.primaryKey = init.getText().trim() === "true";
|
|
352
|
+
break;
|
|
353
|
+
case "defaultValue":
|
|
354
|
+
field.defaultValue = extractDefaultValue(init);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return field;
|
|
359
|
+
}
|
|
360
|
+
function extractDataType(node) {
|
|
361
|
+
const text = node.getText().trim();
|
|
362
|
+
const callMatch = text.match(/^DataTypes\.(\w+)\((.+)\)$/);
|
|
363
|
+
if (callMatch) return `${callMatch[1]}(${callMatch[2]})`;
|
|
364
|
+
const propMatch = text.match(/^DataTypes\.(\w+)$/);
|
|
365
|
+
if (propMatch) return propMatch[1];
|
|
366
|
+
return text;
|
|
367
|
+
}
|
|
368
|
+
function extractDefaultValue(node) {
|
|
369
|
+
const text = node.getText().trim();
|
|
370
|
+
if (text === "DataTypes.NOW") return "DataTypes.NOW";
|
|
371
|
+
if (text.startsWith("'") && text.endsWith("'") || text.startsWith('"') && text.endsWith('"'))
|
|
372
|
+
return text.slice(1, -1);
|
|
373
|
+
if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
|
|
374
|
+
if (text === "true") return true;
|
|
375
|
+
if (text === "false") return false;
|
|
376
|
+
if (text === "null") return null;
|
|
377
|
+
return text;
|
|
378
|
+
}
|
|
379
|
+
function parseOptions(optionsNode) {
|
|
380
|
+
let tableName = null;
|
|
381
|
+
let indexes = [];
|
|
382
|
+
if (optionsNode.getKind() !== SyntaxKind.ObjectLiteralExpression) return { tableName, indexes };
|
|
383
|
+
const objLiteral = optionsNode;
|
|
384
|
+
for (const prop of objLiteral.getProperties()) {
|
|
385
|
+
if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
|
|
386
|
+
const propAssign = prop;
|
|
387
|
+
const key = propAssign.getName();
|
|
388
|
+
const init = propAssign.getInitializer();
|
|
389
|
+
if (!init) continue;
|
|
390
|
+
if (key === "tableName") tableName = extractStringValue(init);
|
|
391
|
+
if (key === "indexes") indexes = parseIndexes(init);
|
|
392
|
+
}
|
|
393
|
+
return { tableName, indexes };
|
|
394
|
+
}
|
|
395
|
+
function extractStringValue(node) {
|
|
396
|
+
const text = node.getText().trim();
|
|
397
|
+
if (text.startsWith("'") && text.endsWith("'") || text.startsWith('"') && text.endsWith('"'))
|
|
398
|
+
return text.slice(1, -1);
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
function parseIndexes(node) {
|
|
402
|
+
if (node.getKind() !== SyntaxKind.ArrayLiteralExpression) return [];
|
|
403
|
+
const arr = node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
404
|
+
const indexes = [];
|
|
405
|
+
for (const el of arr.getElements()) {
|
|
406
|
+
if (el.getKind() !== SyntaxKind.ObjectLiteralExpression) continue;
|
|
407
|
+
const idx = parseIndexObject(el);
|
|
408
|
+
if (idx) indexes.push(idx);
|
|
409
|
+
}
|
|
410
|
+
return indexes;
|
|
411
|
+
}
|
|
412
|
+
function parseIndexObject(obj) {
|
|
413
|
+
let name = "";
|
|
414
|
+
let fields = [];
|
|
415
|
+
let unique = false;
|
|
416
|
+
for (const prop of obj.getProperties()) {
|
|
417
|
+
if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
|
|
418
|
+
const pa = prop;
|
|
419
|
+
const init = pa.getInitializer();
|
|
420
|
+
if (!init) continue;
|
|
421
|
+
switch (pa.getName()) {
|
|
422
|
+
case "name":
|
|
423
|
+
name = extractStringValue(init) || "";
|
|
424
|
+
break;
|
|
425
|
+
case "fields":
|
|
426
|
+
fields = extractStringArray(init);
|
|
427
|
+
break;
|
|
428
|
+
case "unique":
|
|
429
|
+
unique = init.getText().trim() === "true";
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!name || fields.length === 0) return null;
|
|
434
|
+
return { name, fields, unique };
|
|
435
|
+
}
|
|
436
|
+
function extractStringArray(node) {
|
|
437
|
+
if (node.getKind() !== SyntaxKind.ArrayLiteralExpression) return [];
|
|
438
|
+
const arr = node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
439
|
+
return arr.getElements().map((el) => el.getText().trim()).filter((t) => t.startsWith("'") || t.startsWith('"')).map((t) => t.slice(1, -1));
|
|
440
|
+
}
|
|
441
|
+
function createModelParser() {
|
|
8
442
|
return {
|
|
9
|
-
async
|
|
10
|
-
|
|
443
|
+
async parseFile(filePath) {
|
|
444
|
+
return parseModelFile(filePath);
|
|
445
|
+
},
|
|
446
|
+
async parseDirectory(dirPath) {
|
|
447
|
+
return parseModuleModels(dirPath);
|
|
11
448
|
}
|
|
12
449
|
};
|
|
13
450
|
}
|
|
14
451
|
|
|
15
|
-
// src/
|
|
16
|
-
|
|
452
|
+
// src/pipeline/index.ts
|
|
453
|
+
init_controller_parser();
|
|
454
|
+
|
|
455
|
+
// src/parsers/association-parser.ts
|
|
456
|
+
init_esm_shims();
|
|
457
|
+
import * as fs3 from "fs";
|
|
458
|
+
import * as path4 from "path";
|
|
459
|
+
import {
|
|
460
|
+
Project as Project3,
|
|
461
|
+
SyntaxKind as SyntaxKind3
|
|
462
|
+
} from "ts-morph";
|
|
463
|
+
function parseAssociationFile(filePath, classToTableMap, moduleTablePrefix) {
|
|
464
|
+
const absolutePath = path4.resolve(filePath);
|
|
465
|
+
if (!fs3.existsSync(absolutePath)) return [];
|
|
466
|
+
const project = new Project3({ compilerOptions: { strict: false } });
|
|
467
|
+
const sourceFile = project.addSourceFileAtPath(absolutePath);
|
|
468
|
+
const importPathMap = collectImportPaths(sourceFile);
|
|
469
|
+
const rawAssociations = extractAssociationCalls(sourceFile, importPathMap);
|
|
470
|
+
if (rawAssociations.length === 0) return [];
|
|
471
|
+
return deduplicateRelations(rawAssociations, classToTableMap, moduleTablePrefix);
|
|
472
|
+
}
|
|
473
|
+
function buildClassToTableMap(modelDir) {
|
|
474
|
+
const map = /* @__PURE__ */ new Map();
|
|
475
|
+
const absoluteDir = path4.resolve(modelDir);
|
|
476
|
+
if (!fs3.existsSync(absoluteDir)) return map;
|
|
477
|
+
const files = fs3.readdirSync(absoluteDir).filter(
|
|
478
|
+
(f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts" && f !== "associations.ts"
|
|
479
|
+
);
|
|
480
|
+
for (const file of files) {
|
|
481
|
+
try {
|
|
482
|
+
const schema = parseModelFile(path4.join(absoluteDir, file));
|
|
483
|
+
if (schema) {
|
|
484
|
+
const className = file.replace(".ts", "");
|
|
485
|
+
map.set(className, schema.tableName);
|
|
486
|
+
}
|
|
487
|
+
} catch {
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return map;
|
|
491
|
+
}
|
|
492
|
+
function collectImportPaths(sourceFile) {
|
|
493
|
+
const map = /* @__PURE__ */ new Map();
|
|
494
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
495
|
+
const moduleSpecifier = decl.getModuleSpecifierValue();
|
|
496
|
+
for (const named of decl.getNamedImports()) {
|
|
497
|
+
map.set(named.getName(), moduleSpecifier);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return map;
|
|
501
|
+
}
|
|
502
|
+
function extractAssociationCalls(sourceFile, importPathMap) {
|
|
503
|
+
const associations = [];
|
|
504
|
+
const calls = sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression);
|
|
505
|
+
for (const call of calls) {
|
|
506
|
+
const expr = call.getExpression();
|
|
507
|
+
if (expr.getKind() !== SyntaxKind3.PropertyAccessExpression) continue;
|
|
508
|
+
const propAccess = expr.asKindOrThrow(SyntaxKind3.PropertyAccessExpression);
|
|
509
|
+
const methodName = propAccess.getName();
|
|
510
|
+
if (methodName !== "hasMany" && methodName !== "belongsTo" && methodName !== "hasOne") continue;
|
|
511
|
+
const sourceClass = propAccess.getExpression().getText().trim();
|
|
512
|
+
const args = call.getArguments();
|
|
513
|
+
if (args.length < 1) continue;
|
|
514
|
+
const targetClass = args[0].getText().trim();
|
|
515
|
+
let foreignKey = "";
|
|
516
|
+
if (args.length >= 2 && args[1].getKind() === SyntaxKind3.ObjectLiteralExpression) {
|
|
517
|
+
foreignKey = extractStringProperty(args[1], "foreignKey");
|
|
518
|
+
}
|
|
519
|
+
associations.push({
|
|
520
|
+
sourceClass,
|
|
521
|
+
targetClass,
|
|
522
|
+
foreignKey,
|
|
523
|
+
type: methodName,
|
|
524
|
+
importPath: importPathMap.get(targetClass)
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return associations;
|
|
528
|
+
}
|
|
529
|
+
function extractStringProperty(obj, propertyName) {
|
|
530
|
+
for (const prop of obj.getProperties()) {
|
|
531
|
+
if (prop.getKind() !== SyntaxKind3.PropertyAssignment) continue;
|
|
532
|
+
const pa = prop;
|
|
533
|
+
if (pa.getName() !== propertyName) continue;
|
|
534
|
+
const init = pa.getInitializer();
|
|
535
|
+
if (!init) continue;
|
|
536
|
+
const text = init.getText().trim();
|
|
537
|
+
if (text.startsWith("'") && text.endsWith("'") || text.startsWith('"') && text.endsWith('"'))
|
|
538
|
+
return text.slice(1, -1);
|
|
539
|
+
return text;
|
|
540
|
+
}
|
|
541
|
+
return "";
|
|
542
|
+
}
|
|
543
|
+
function classNameToTableName(className) {
|
|
544
|
+
return className.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
545
|
+
}
|
|
546
|
+
function resolveTableName(className, classToTableMap) {
|
|
547
|
+
if (classToTableMap?.has(className)) return classToTableMap.get(className);
|
|
548
|
+
return classNameToTableName(className);
|
|
549
|
+
}
|
|
550
|
+
function isCrossModuleRef(targetTableName, importPath, moduleTablePrefix) {
|
|
551
|
+
if (moduleTablePrefix) return !targetTableName.startsWith(moduleTablePrefix);
|
|
552
|
+
if (importPath) {
|
|
553
|
+
const upLevels = (importPath.match(/\.\.\//g) || []).length;
|
|
554
|
+
return upLevels >= 2;
|
|
555
|
+
}
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
function deduplicateRelations(rawAssociations, classToTableMap, moduleTablePrefix) {
|
|
559
|
+
const seen = /* @__PURE__ */ new Map();
|
|
560
|
+
for (const raw of rawAssociations) {
|
|
561
|
+
const sourceTable = resolveTableName(raw.sourceClass, classToTableMap);
|
|
562
|
+
const targetTable = resolveTableName(raw.targetClass, classToTableMap);
|
|
563
|
+
const crossModule = isCrossModuleRef(targetTable, raw.importPath, moduleTablePrefix);
|
|
564
|
+
let parentTable;
|
|
565
|
+
let childTable;
|
|
566
|
+
let cardinality;
|
|
567
|
+
switch (raw.type) {
|
|
568
|
+
case "hasMany":
|
|
569
|
+
parentTable = sourceTable;
|
|
570
|
+
childTable = targetTable;
|
|
571
|
+
cardinality = "1:N";
|
|
572
|
+
break;
|
|
573
|
+
case "belongsTo":
|
|
574
|
+
parentTable = targetTable;
|
|
575
|
+
childTable = sourceTable;
|
|
576
|
+
cardinality = "N:1";
|
|
577
|
+
break;
|
|
578
|
+
case "hasOne":
|
|
579
|
+
parentTable = sourceTable;
|
|
580
|
+
childTable = targetTable;
|
|
581
|
+
cardinality = "1:1";
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
const dedupeKey = `${parentTable}|${childTable}|${raw.foreignKey}`;
|
|
585
|
+
if (seen.has(dedupeKey)) {
|
|
586
|
+
const existing = seen.get(dedupeKey);
|
|
587
|
+
if (existing.cardinality === "N:1" && (cardinality === "1:N" || cardinality === "1:1")) {
|
|
588
|
+
seen.set(dedupeKey, {
|
|
589
|
+
sourceTable: parentTable,
|
|
590
|
+
sourceField: "id",
|
|
591
|
+
targetTable: childTable,
|
|
592
|
+
targetField: raw.foreignKey,
|
|
593
|
+
cardinality,
|
|
594
|
+
isCrossModule: crossModule || existing.isCrossModule
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
seen.set(dedupeKey, {
|
|
599
|
+
sourceTable: parentTable,
|
|
600
|
+
sourceField: "id",
|
|
601
|
+
targetTable: childTable,
|
|
602
|
+
targetField: raw.foreignKey,
|
|
603
|
+
cardinality,
|
|
604
|
+
isCrossModule: crossModule
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return Array.from(seen.values());
|
|
609
|
+
}
|
|
610
|
+
function createAssociationParser() {
|
|
17
611
|
return {
|
|
18
|
-
async parseFile(
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
async parseDirectory(_dirPath) {
|
|
22
|
-
throw new Error("Model parser not yet implemented");
|
|
612
|
+
async parseFile(filePath) {
|
|
613
|
+
return parseAssociationFile(filePath);
|
|
23
614
|
}
|
|
24
615
|
};
|
|
25
616
|
}
|
|
26
617
|
|
|
27
|
-
// src/
|
|
28
|
-
|
|
618
|
+
// src/analyzers/api-chain-analyzer.ts
|
|
619
|
+
init_esm_shims();
|
|
620
|
+
var EXCLUDED_PARAMS = /* @__PURE__ */ new Set(["tenantId"]);
|
|
621
|
+
function toNodeKey(endpoint) {
|
|
622
|
+
return `${endpoint.method} ${endpoint.path}`;
|
|
623
|
+
}
|
|
624
|
+
function paramToResourceHint(param) {
|
|
625
|
+
const stripped = param.endsWith("Id") ? param.slice(0, -2) : param;
|
|
626
|
+
return stripped.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
|
|
627
|
+
}
|
|
628
|
+
function postProducesResource(postEndpoint, resourceHint) {
|
|
629
|
+
const segments = postEndpoint.path.split("/").filter((s) => s && !s.startsWith(":"));
|
|
630
|
+
if (segments.length === 0) return false;
|
|
631
|
+
const lastSegment = segments[segments.length - 1].toLowerCase();
|
|
632
|
+
if (lastSegment.includes(resourceHint)) return true;
|
|
633
|
+
const parts = lastSegment.split("-");
|
|
634
|
+
if (parts.some((p) => p === resourceHint || p.startsWith(resourceHint))) return true;
|
|
635
|
+
if (resourceHint.length <= 4) {
|
|
636
|
+
const abbreviation = parts.map((p) => p[0]).join("");
|
|
637
|
+
if (abbreviation.startsWith(resourceHint)) return true;
|
|
638
|
+
}
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
function inferDependencies(endpoints) {
|
|
642
|
+
const dependencies = [];
|
|
643
|
+
const postEndpoints = endpoints.filter((ep) => ep.method === "POST");
|
|
644
|
+
for (const consumer of endpoints) {
|
|
645
|
+
const consumedParams = consumer.pathParams.filter((p) => !EXCLUDED_PARAMS.has(p));
|
|
646
|
+
if (consumedParams.length === 0) continue;
|
|
647
|
+
for (const param of consumedParams) {
|
|
648
|
+
if (param === "id") {
|
|
649
|
+
const basePath = consumer.path.replace(/\/:id(\/.*)?$/, "");
|
|
650
|
+
const producer2 = postEndpoints.find((ep) => ep.path === basePath);
|
|
651
|
+
if (producer2 && toNodeKey(producer2) !== toNodeKey(consumer)) {
|
|
652
|
+
dependencies.push({ from: consumer, to: producer2, paramMapping: { [`:${param}`]: "response.data.id" } });
|
|
653
|
+
}
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const resourceHint = paramToResourceHint(param);
|
|
657
|
+
if (!resourceHint) continue;
|
|
658
|
+
const producer = postEndpoints.find((ep) => postProducesResource(ep, resourceHint));
|
|
659
|
+
if (producer && toNodeKey(producer) !== toNodeKey(consumer)) {
|
|
660
|
+
dependencies.push({ from: consumer, to: producer, paramMapping: { [`:${param}`]: "response.data.id" } });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return deduplicateDependencies(dependencies);
|
|
665
|
+
}
|
|
666
|
+
function deduplicateDependencies(deps) {
|
|
667
|
+
const map = /* @__PURE__ */ new Map();
|
|
668
|
+
for (const dep of deps) {
|
|
669
|
+
const key = `${toNodeKey(dep.from)}\u2192${toNodeKey(dep.to)}`;
|
|
670
|
+
if (map.has(key)) {
|
|
671
|
+
Object.assign(map.get(key).paramMapping, dep.paramMapping);
|
|
672
|
+
} else {
|
|
673
|
+
map.set(key, { ...dep, paramMapping: { ...dep.paramMapping } });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return Array.from(map.values());
|
|
677
|
+
}
|
|
678
|
+
function buildGraph(endpoints, dependencies) {
|
|
679
|
+
const nodeSet = /* @__PURE__ */ new Set();
|
|
680
|
+
for (const ep of endpoints) nodeSet.add(toNodeKey(ep));
|
|
681
|
+
const edges = [];
|
|
682
|
+
for (const dep of dependencies) {
|
|
683
|
+
edges.push({
|
|
684
|
+
from: toNodeKey(dep.from),
|
|
685
|
+
to: toNodeKey(dep.to),
|
|
686
|
+
label: Object.keys(dep.paramMapping).join(", ") || void 0
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
return { nodes: Array.from(nodeSet), edges };
|
|
690
|
+
}
|
|
691
|
+
function detectCycles(dag) {
|
|
692
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
693
|
+
for (const node of dag.nodes) adjacency.set(node, []);
|
|
694
|
+
for (const edge of dag.edges) adjacency.get(edge.from)?.push(edge.to);
|
|
695
|
+
const color = /* @__PURE__ */ new Map();
|
|
696
|
+
for (const node of dag.nodes) color.set(node, 0 /* WHITE */);
|
|
697
|
+
const warnings = [];
|
|
698
|
+
const path9 = [];
|
|
699
|
+
function dfs(node) {
|
|
700
|
+
color.set(node, 1 /* GRAY */);
|
|
701
|
+
path9.push(node);
|
|
702
|
+
for (const neighbor of adjacency.get(node) || []) {
|
|
703
|
+
const nc = color.get(neighbor);
|
|
704
|
+
if (nc === 1 /* GRAY */) {
|
|
705
|
+
const cycleStart = path9.indexOf(neighbor);
|
|
706
|
+
warnings.push(`Cycle detected: ${path9.slice(cycleStart).concat(neighbor).join(" \u2192 ")}`);
|
|
707
|
+
} else if (nc === 0 /* WHITE */) {
|
|
708
|
+
dfs(neighbor);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
path9.pop();
|
|
712
|
+
color.set(node, 2 /* BLACK */);
|
|
713
|
+
}
|
|
714
|
+
for (const node of dag.nodes) {
|
|
715
|
+
if (color.get(node) === 0 /* WHITE */) dfs(node);
|
|
716
|
+
}
|
|
717
|
+
return warnings;
|
|
718
|
+
}
|
|
719
|
+
function topologicalSort(dag) {
|
|
720
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
721
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
722
|
+
for (const node of dag.nodes) {
|
|
723
|
+
inDegree.set(node, 0);
|
|
724
|
+
adjacency.set(node, []);
|
|
725
|
+
}
|
|
726
|
+
for (const edge of dag.edges) {
|
|
727
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
728
|
+
inDegree.set(edge.to, (inDegree.get(edge.to) || 0) + 1);
|
|
729
|
+
}
|
|
730
|
+
const queue = [];
|
|
731
|
+
for (const [node, degree] of inDegree) {
|
|
732
|
+
if (degree === 0) queue.push(node);
|
|
733
|
+
}
|
|
734
|
+
const sorted = [];
|
|
735
|
+
while (queue.length > 0) {
|
|
736
|
+
const node = queue.shift();
|
|
737
|
+
sorted.push(node);
|
|
738
|
+
for (const neighbor of adjacency.get(node) || []) {
|
|
739
|
+
const nd = (inDegree.get(neighbor) || 1) - 1;
|
|
740
|
+
inDegree.set(neighbor, nd);
|
|
741
|
+
if (nd === 0) queue.push(neighbor);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return sorted;
|
|
745
|
+
}
|
|
746
|
+
function createApiChainAnalyzer() {
|
|
29
747
|
return {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
748
|
+
analyze(endpoints) {
|
|
749
|
+
const dependencies = inferDependencies(endpoints);
|
|
750
|
+
const dag = buildGraph(endpoints, dependencies);
|
|
751
|
+
const cycleWarnings = detectCycles(dag);
|
|
752
|
+
return {
|
|
753
|
+
moduleName: "",
|
|
754
|
+
endpoints,
|
|
755
|
+
dependencies,
|
|
756
|
+
dag,
|
|
757
|
+
hasCycles: cycleWarnings.length > 0,
|
|
758
|
+
cycleWarnings
|
|
759
|
+
};
|
|
35
760
|
}
|
|
36
761
|
};
|
|
37
762
|
}
|
|
38
763
|
|
|
39
|
-
// src/
|
|
40
|
-
|
|
764
|
+
// src/generators/er-diagram-generator.ts
|
|
765
|
+
init_esm_shims();
|
|
766
|
+
function toMermaidType(fieldType) {
|
|
767
|
+
const upper = fieldType.toUpperCase();
|
|
768
|
+
if (upper.startsWith("STRING")) return "string";
|
|
769
|
+
if (upper === "BIGINT" || upper === "INTEGER") return "bigint";
|
|
770
|
+
if (upper === "BOOLEAN") return "boolean";
|
|
771
|
+
if (upper.startsWith("DATE") || upper === "NOW") return "datetime";
|
|
772
|
+
if (upper === "JSON" || upper === "JSONB") return "json";
|
|
773
|
+
if (upper === "TEXT") return "text";
|
|
774
|
+
if (upper === "FLOAT" || upper === "DOUBLE" || upper === "DECIMAL") return "float";
|
|
775
|
+
if (upper === "UUID") return "uuid";
|
|
776
|
+
if (upper.startsWith("ENUM")) return "enum";
|
|
777
|
+
return "string";
|
|
778
|
+
}
|
|
779
|
+
function sanitizeEntityName(name) {
|
|
780
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
781
|
+
}
|
|
782
|
+
function generateMermaidER(tables, relations) {
|
|
783
|
+
const lines = ["erDiagram"];
|
|
784
|
+
for (const table of tables) {
|
|
785
|
+
const entityName = sanitizeEntityName(table.tableName);
|
|
786
|
+
lines.push(` ${entityName} {`);
|
|
787
|
+
for (const field of table.fields) {
|
|
788
|
+
const mType = toMermaidType(field.type);
|
|
789
|
+
const pk = field.primaryKey ? "PK" : "";
|
|
790
|
+
const comment = field.comment ? ` "${field.comment}"` : "";
|
|
791
|
+
lines.push(` ${mType} ${field.name}${pk ? " " + pk : ""}${comment}`);
|
|
792
|
+
}
|
|
793
|
+
lines.push(" }");
|
|
794
|
+
}
|
|
795
|
+
const tableNames = new Set(tables.map((t) => t.tableName));
|
|
796
|
+
for (const rel of relations) {
|
|
797
|
+
if (!tableNames.has(rel.sourceTable) || !tableNames.has(rel.targetTable)) continue;
|
|
798
|
+
const src = sanitizeEntityName(rel.sourceTable);
|
|
799
|
+
const tgt = sanitizeEntityName(rel.targetTable);
|
|
800
|
+
const linkStyle = rel.isCrossModule ? ".." : "--";
|
|
801
|
+
let cardinality;
|
|
802
|
+
switch (rel.cardinality) {
|
|
803
|
+
case "1:N":
|
|
804
|
+
cardinality = `||${linkStyle}o{`;
|
|
805
|
+
break;
|
|
806
|
+
case "N:1":
|
|
807
|
+
cardinality = `}o${linkStyle}||`;
|
|
808
|
+
break;
|
|
809
|
+
case "1:1":
|
|
810
|
+
cardinality = `||${linkStyle}||`;
|
|
811
|
+
break;
|
|
812
|
+
default:
|
|
813
|
+
cardinality = `||${linkStyle}o{`;
|
|
814
|
+
}
|
|
815
|
+
lines.push(` ${src} ${cardinality} ${tgt} : "${rel.targetField}"`);
|
|
816
|
+
}
|
|
817
|
+
return lines.join("\n");
|
|
818
|
+
}
|
|
819
|
+
function createERDiagramGenerator() {
|
|
41
820
|
return {
|
|
42
|
-
|
|
43
|
-
|
|
821
|
+
generate(tables, relations) {
|
|
822
|
+
const mermaidText = generateMermaidER(tables, relations);
|
|
823
|
+
return { tables, relations, mermaidText };
|
|
44
824
|
}
|
|
45
825
|
};
|
|
46
826
|
}
|
|
47
827
|
|
|
48
828
|
// src/generators/test-code-generator.ts
|
|
829
|
+
init_esm_shims();
|
|
830
|
+
function resolvePathParam(param, ids) {
|
|
831
|
+
if (ids.includes(param)) return `createdIds['${param}']`;
|
|
832
|
+
const stripped = param.endsWith("Id") ? param.slice(0, -2) : param;
|
|
833
|
+
if (ids.includes(stripped)) return `createdIds['${stripped}']`;
|
|
834
|
+
if (param === "id") return `createdIds['id']`;
|
|
835
|
+
return `createdIds['${param}'] || '1'`;
|
|
836
|
+
}
|
|
837
|
+
function buildUrlCode(step) {
|
|
838
|
+
const pathParams = step.endpoint.pathParams;
|
|
839
|
+
if (pathParams.length === 0) return `const url = '${step.endpoint.path}';`;
|
|
840
|
+
let urlTemplate = step.endpoint.path;
|
|
841
|
+
const replacements = [];
|
|
842
|
+
for (const param of pathParams) {
|
|
843
|
+
urlTemplate = urlTemplate.replace(`:${param}`, `\${${resolvePathParam(param, pathParams)}}`);
|
|
844
|
+
replacements.push(param);
|
|
845
|
+
}
|
|
846
|
+
return `const url = \`${urlTemplate}\`;`;
|
|
847
|
+
}
|
|
848
|
+
function generateAssertions(step) {
|
|
849
|
+
const lines = [];
|
|
850
|
+
if (step.assertions.length > 0) {
|
|
851
|
+
for (const assertion of step.assertions) {
|
|
852
|
+
lines.push(` expect(${assertion}).toBeTruthy();`);
|
|
853
|
+
}
|
|
854
|
+
} else {
|
|
855
|
+
if (step.endpoint.method === "POST") {
|
|
856
|
+
lines.push(" expect(response.status()).toBeLessThan(400);");
|
|
857
|
+
lines.push(" const body = await response.json();");
|
|
858
|
+
lines.push(" if (body.data?.id) createdIds['id'] = body.data.id;");
|
|
859
|
+
} else if (step.endpoint.method === "GET") {
|
|
860
|
+
lines.push(" expect(response.ok()).toBeTruthy();");
|
|
861
|
+
} else if (step.endpoint.method === "DELETE") {
|
|
862
|
+
lines.push(" expect(response.status()).toBeLessThan(400);");
|
|
863
|
+
} else {
|
|
864
|
+
lines.push(" expect(response.status()).toBeLessThan(400);");
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return lines;
|
|
868
|
+
}
|
|
869
|
+
function generateTestFile(chain) {
|
|
870
|
+
const lines = [];
|
|
871
|
+
lines.push(`import { test, expect } from '@playwright/test';`);
|
|
872
|
+
lines.push("");
|
|
873
|
+
lines.push(`test.describe('${chain.name}', () => {`);
|
|
874
|
+
lines.push(" const createdIds: Record<string, string> = {};");
|
|
875
|
+
lines.push("");
|
|
876
|
+
for (const step of chain.steps) {
|
|
877
|
+
lines.push(` test('Step ${step.order}: ${step.description}', async ({ request }) => {`);
|
|
878
|
+
lines.push(` // ${step.action}: ${step.endpoint.method} ${step.endpoint.path}`);
|
|
879
|
+
lines.push(` ${buildUrlCode(step)}`);
|
|
880
|
+
lines.push("");
|
|
881
|
+
if (step.endpoint.method === "GET") {
|
|
882
|
+
lines.push(" const response = await request.get(url);");
|
|
883
|
+
} else if (step.endpoint.method === "POST") {
|
|
884
|
+
lines.push(" const response = await request.post(url, { data: {} });");
|
|
885
|
+
} else if (step.endpoint.method === "PUT") {
|
|
886
|
+
lines.push(" const response = await request.put(url, { data: {} });");
|
|
887
|
+
} else if (step.endpoint.method === "DELETE") {
|
|
888
|
+
lines.push(" const response = await request.delete(url);");
|
|
889
|
+
} else if (step.endpoint.method === "PATCH") {
|
|
890
|
+
lines.push(" const response = await request.patch(url, { data: {} });");
|
|
891
|
+
}
|
|
892
|
+
lines.push("");
|
|
893
|
+
lines.push(...generateAssertions(step));
|
|
894
|
+
lines.push(" });");
|
|
895
|
+
lines.push("");
|
|
896
|
+
}
|
|
897
|
+
lines.push("});");
|
|
898
|
+
return lines.join("\n");
|
|
899
|
+
}
|
|
49
900
|
function createTestCodeGenerator() {
|
|
50
901
|
return {
|
|
51
|
-
generate(
|
|
52
|
-
|
|
902
|
+
generate(chains) {
|
|
903
|
+
return chains.map((chain) => ({
|
|
904
|
+
filePath: `${chain.module}/${chain.name.replace(/\s+/g, "-").toLowerCase()}.spec.ts`,
|
|
905
|
+
content: generateTestFile(chain),
|
|
906
|
+
module: chain.module,
|
|
907
|
+
chain: chain.name
|
|
908
|
+
}));
|
|
53
909
|
}
|
|
54
910
|
};
|
|
55
911
|
}
|
|
56
912
|
|
|
913
|
+
// src/validators/config-validator.ts
|
|
914
|
+
init_esm_shims();
|
|
915
|
+
var REQUIRED_FIELDS = ["backendRoot"];
|
|
916
|
+
var VALID_ADAPTERS = ["sequelize", "typeorm", "prisma"];
|
|
917
|
+
var VALID_STEPS = ["scan", "er-diagram", "api-chain", "plan", "codegen", "validate"];
|
|
918
|
+
var VALID_LLM_PROVIDERS = ["openai", "zhipu", "ollama", "custom"];
|
|
919
|
+
var VALID_REPORT_FORMATS = ["html", "json", "markdown"];
|
|
920
|
+
var VALID_HEAL_MODES = ["config-only", "config-and-source"];
|
|
921
|
+
function validateConfig(config) {
|
|
922
|
+
const errors = [];
|
|
923
|
+
for (const field of REQUIRED_FIELDS) {
|
|
924
|
+
if (!config[field]) {
|
|
925
|
+
errors.push({
|
|
926
|
+
module: "config",
|
|
927
|
+
field,
|
|
928
|
+
message: `Missing required field: ${field}`,
|
|
929
|
+
severity: "error"
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (config.backendRoot && typeof config.backendRoot !== "string") {
|
|
934
|
+
errors.push({
|
|
935
|
+
module: "config",
|
|
936
|
+
field: "backendRoot",
|
|
937
|
+
message: "backendRoot must be a string path",
|
|
938
|
+
severity: "error"
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
if (config.adapter && typeof config.adapter === "string") {
|
|
942
|
+
if (!VALID_ADAPTERS.includes(config.adapter)) {
|
|
943
|
+
errors.push({
|
|
944
|
+
module: "config",
|
|
945
|
+
field: "adapter",
|
|
946
|
+
message: `Invalid adapter: ${config.adapter}. Must be one of: ${VALID_ADAPTERS.join(", ")}`,
|
|
947
|
+
severity: "error"
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (config.steps && Array.isArray(config.steps)) {
|
|
952
|
+
for (const step of config.steps) {
|
|
953
|
+
if (!VALID_STEPS.includes(step)) {
|
|
954
|
+
errors.push({
|
|
955
|
+
module: "config",
|
|
956
|
+
field: "steps",
|
|
957
|
+
message: `Invalid pipeline step: ${step}. Must be one of: ${VALID_STEPS.join(", ")}`,
|
|
958
|
+
severity: "error"
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (config.llm && typeof config.llm === "object") {
|
|
964
|
+
const llm = config.llm;
|
|
965
|
+
if (llm.provider && !VALID_LLM_PROVIDERS.includes(llm.provider)) {
|
|
966
|
+
errors.push({
|
|
967
|
+
module: "config",
|
|
968
|
+
field: "llm.provider",
|
|
969
|
+
message: `Invalid LLM provider: ${llm.provider}. Must be one of: ${VALID_LLM_PROVIDERS.join(", ")}`,
|
|
970
|
+
severity: "error"
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
if (llm.provider && llm.provider !== "ollama" && !llm.apiKey) {
|
|
974
|
+
errors.push({
|
|
975
|
+
module: "config",
|
|
976
|
+
field: "llm.apiKey",
|
|
977
|
+
message: "LLM apiKey is required for cloud providers",
|
|
978
|
+
severity: "warning"
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (config.report && typeof config.report === "object") {
|
|
983
|
+
const report = config.report;
|
|
984
|
+
if (report.format && Array.isArray(report.format)) {
|
|
985
|
+
for (const fmt of report.format) {
|
|
986
|
+
if (!VALID_REPORT_FORMATS.includes(fmt)) {
|
|
987
|
+
errors.push({
|
|
988
|
+
module: "config",
|
|
989
|
+
field: "report.format",
|
|
990
|
+
message: `Invalid report format: ${fmt}. Must be one of: ${VALID_REPORT_FORMATS.join(", ")}`,
|
|
991
|
+
severity: "error"
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (config.selfHealing && typeof config.selfHealing === "object") {
|
|
998
|
+
const sh = config.selfHealing;
|
|
999
|
+
if (sh.mode && !VALID_HEAL_MODES.includes(sh.mode)) {
|
|
1000
|
+
errors.push({
|
|
1001
|
+
module: "config",
|
|
1002
|
+
field: "selfHealing.mode",
|
|
1003
|
+
message: `Invalid self-healing mode: ${sh.mode}. Must be one of: ${VALID_HEAL_MODES.join(", ")}`,
|
|
1004
|
+
severity: "error"
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
if (sh.maxIterations && (typeof sh.maxIterations !== "number" || sh.maxIterations < 1)) {
|
|
1008
|
+
errors.push({
|
|
1009
|
+
module: "config",
|
|
1010
|
+
field: "selfHealing.maxIterations",
|
|
1011
|
+
message: "maxIterations must be a positive number",
|
|
1012
|
+
severity: "error"
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return errors;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/pipeline/index.ts
|
|
1020
|
+
var ALL_STEPS = ["scan", "er-diagram", "api-chain", "plan", "codegen", "validate"];
|
|
1021
|
+
function createPipeline(config) {
|
|
1022
|
+
return {
|
|
1023
|
+
async run(steps) {
|
|
1024
|
+
const startTime = Date.now();
|
|
1025
|
+
const activeSteps = steps || config.steps || ALL_STEPS;
|
|
1026
|
+
const result = {
|
|
1027
|
+
modules: [],
|
|
1028
|
+
erDiagrams: /* @__PURE__ */ new Map(),
|
|
1029
|
+
chainPlans: /* @__PURE__ */ new Map(),
|
|
1030
|
+
generatedFiles: [],
|
|
1031
|
+
validationErrors: [],
|
|
1032
|
+
duration: 0
|
|
1033
|
+
};
|
|
1034
|
+
if (activeSteps.includes("scan")) {
|
|
1035
|
+
const backendRoot = path5.resolve(config.backendRoot);
|
|
1036
|
+
const modelsDir = path5.join(backendRoot, "models");
|
|
1037
|
+
if (fs4.existsSync(modelsDir)) {
|
|
1038
|
+
const dirs = fs4.readdirSync(modelsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1039
|
+
const moduleFilter = config.modules;
|
|
1040
|
+
for (const dir of dirs) {
|
|
1041
|
+
if (moduleFilter && !moduleFilter.includes(dir)) continue;
|
|
1042
|
+
result.modules.push(dir);
|
|
1043
|
+
}
|
|
1044
|
+
if (result.modules.length === 0) {
|
|
1045
|
+
result.modules.push("default");
|
|
1046
|
+
} else {
|
|
1047
|
+
const rootFiles = fs4.readdirSync(modelsDir).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && f !== "index.ts");
|
|
1048
|
+
if (rootFiles.length > 0) {
|
|
1049
|
+
result.modules.unshift("default");
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const resolveModelDir = (backendRoot, mod) => mod === "default" ? path5.join(backendRoot, "models") : path5.join(backendRoot, "models", mod);
|
|
1055
|
+
const resolveControllerDir = (backendRoot, mod) => mod === "default" ? path5.join(backendRoot, "controllers") : path5.join(backendRoot, "controllers", mod);
|
|
1056
|
+
if (activeSteps.includes("er-diagram")) {
|
|
1057
|
+
const erGen = createERDiagramGenerator();
|
|
1058
|
+
const backendRoot = path5.resolve(config.backendRoot);
|
|
1059
|
+
for (const mod of result.modules) {
|
|
1060
|
+
const modelDir = resolveModelDir(backendRoot, mod);
|
|
1061
|
+
const tables = fs4.existsSync(modelDir) ? parseModuleModels(modelDir) : [];
|
|
1062
|
+
const relations = [];
|
|
1063
|
+
const assocFile = path5.join(modelDir, "associations.ts");
|
|
1064
|
+
if (fs4.existsSync(assocFile)) {
|
|
1065
|
+
relations.push(...parseAssociationFile(assocFile));
|
|
1066
|
+
}
|
|
1067
|
+
if (fs4.existsSync(modelDir)) {
|
|
1068
|
+
const modelFiles = fs4.readdirSync(modelDir).filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && f !== "index.ts" && f !== "associations.ts");
|
|
1069
|
+
for (const file of modelFiles) {
|
|
1070
|
+
try {
|
|
1071
|
+
const embedded = parseAssociationFile(path5.join(modelDir, file));
|
|
1072
|
+
relations.push(...embedded);
|
|
1073
|
+
} catch {
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const erResult = erGen.generate(tables, relations);
|
|
1078
|
+
result.erDiagrams.set(mod, erResult);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (activeSteps.includes("api-chain")) {
|
|
1082
|
+
const chainAnalyzer = createApiChainAnalyzer();
|
|
1083
|
+
const backendRoot = path5.resolve(config.backendRoot);
|
|
1084
|
+
for (const mod of result.modules) {
|
|
1085
|
+
const controllerDir = resolveControllerDir(backendRoot, mod);
|
|
1086
|
+
const endpoints = fs4.existsSync(controllerDir) ? parseControllerDirectory(controllerDir) : [];
|
|
1087
|
+
const analysis = chainAnalyzer.analyze(endpoints);
|
|
1088
|
+
analysis.moduleName = mod;
|
|
1089
|
+
if (analysis.hasCycles) {
|
|
1090
|
+
for (const warning of analysis.cycleWarnings) {
|
|
1091
|
+
result.validationErrors.push({
|
|
1092
|
+
module: mod,
|
|
1093
|
+
field: "api-chain",
|
|
1094
|
+
message: warning,
|
|
1095
|
+
severity: "warning"
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (activeSteps.includes("plan")) {
|
|
1102
|
+
const backendRoot = path5.resolve(config.backendRoot);
|
|
1103
|
+
const chainAnalyzer = createApiChainAnalyzer();
|
|
1104
|
+
for (const mod of result.modules) {
|
|
1105
|
+
const controllerDir = resolveControllerDir(backendRoot, mod);
|
|
1106
|
+
const endpoints = fs4.existsSync(controllerDir) ? parseControllerDirectory(controllerDir) : [];
|
|
1107
|
+
const analysis = chainAnalyzer.analyze(endpoints);
|
|
1108
|
+
const topoOrder = topologicalSort(analysis.dag);
|
|
1109
|
+
const chains = generateChainPlan(mod, endpoints, topoOrder);
|
|
1110
|
+
result.chainPlans.set(mod, chains);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (activeSteps.includes("codegen")) {
|
|
1114
|
+
const testGen = createTestCodeGenerator();
|
|
1115
|
+
const outDir = config.outDir || "./opencroc-output";
|
|
1116
|
+
for (const [_mod, plan] of result.chainPlans) {
|
|
1117
|
+
const files = testGen.generate(plan.chains);
|
|
1118
|
+
for (const file of files) {
|
|
1119
|
+
file.filePath = path5.join(outDir, file.filePath);
|
|
1120
|
+
}
|
|
1121
|
+
result.generatedFiles.push(...files);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (activeSteps.includes("validate")) {
|
|
1125
|
+
const configErrors = validateConfig(config);
|
|
1126
|
+
result.validationErrors.push(...configErrors);
|
|
1127
|
+
}
|
|
1128
|
+
result.duration = Date.now() - startTime;
|
|
1129
|
+
return result;
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
function generateChainPlan(moduleName, endpoints, _topoOrder) {
|
|
1134
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1135
|
+
for (const ep of endpoints) {
|
|
1136
|
+
const segments = ep.path.split("/").filter((s) => s && !s.startsWith(":"));
|
|
1137
|
+
const resource = segments[segments.length - 1] || "default";
|
|
1138
|
+
if (!groups.has(resource)) groups.set(resource, []);
|
|
1139
|
+
groups.get(resource).push(ep);
|
|
1140
|
+
}
|
|
1141
|
+
const chains = [];
|
|
1142
|
+
let totalSteps = 0;
|
|
1143
|
+
for (const [resource, eps] of groups) {
|
|
1144
|
+
const steps = eps.map((ep, i) => ({
|
|
1145
|
+
order: i + 1,
|
|
1146
|
+
action: ep.method,
|
|
1147
|
+
endpoint: ep,
|
|
1148
|
+
description: ep.description || `${ep.method} ${ep.path}`,
|
|
1149
|
+
assertions: []
|
|
1150
|
+
}));
|
|
1151
|
+
chains.push({ name: `${resource} CRUD chain`, module: moduleName, steps });
|
|
1152
|
+
totalSteps += steps.length;
|
|
1153
|
+
}
|
|
1154
|
+
return { chains, totalSteps };
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/index.ts
|
|
1158
|
+
init_controller_parser();
|
|
1159
|
+
|
|
57
1160
|
// src/generators/mock-data-generator.ts
|
|
1161
|
+
init_esm_shims();
|
|
1162
|
+
function randomInt(min, max) {
|
|
1163
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
1164
|
+
}
|
|
1165
|
+
function randomString(prefix, fieldName) {
|
|
1166
|
+
const ts = Date.now().toString(36);
|
|
1167
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
1168
|
+
return `${prefix}${fieldName}_${ts}_${rand}`;
|
|
1169
|
+
}
|
|
1170
|
+
function generateUUID() {
|
|
1171
|
+
const hex = () => Math.random().toString(16).slice(2, 6);
|
|
1172
|
+
return `${hex()}${hex()}-${hex()}-4${hex().slice(1)}-${(8 + randomInt(0, 3)).toString(16)}${hex().slice(1)}-${hex()}${hex()}${hex()}`;
|
|
1173
|
+
}
|
|
1174
|
+
function generateFieldValue(fieldName, fieldType, isForeignKey, parentTable) {
|
|
1175
|
+
const upper = fieldType.toUpperCase();
|
|
1176
|
+
if (isForeignKey && parentTable) {
|
|
1177
|
+
return `{{parentRecordIds.${parentTable}}}`;
|
|
1178
|
+
}
|
|
1179
|
+
if (upper.startsWith("STRING") || upper === "TEXT") return randomString("test_", fieldName);
|
|
1180
|
+
if (upper === "BIGINT" || upper === "INTEGER") return randomInt(1, 999999);
|
|
1181
|
+
if (upper === "BOOLEAN") return true;
|
|
1182
|
+
if (upper.startsWith("DATE") || upper === "NOW") return (/* @__PURE__ */ new Date()).toISOString();
|
|
1183
|
+
if (upper === "UUID") return generateUUID();
|
|
1184
|
+
if (upper.startsWith("ENUM")) return "ACTIVE";
|
|
1185
|
+
if (upper === "JSON" || upper === "JSONB") return {};
|
|
1186
|
+
if (upper === "FLOAT" || upper === "DOUBLE" || upper === "DECIMAL") return Math.round(Math.random() * 1e4) / 100;
|
|
1187
|
+
return randomString("val_", fieldName);
|
|
1188
|
+
}
|
|
58
1189
|
function createMockDataGenerator() {
|
|
59
1190
|
return {
|
|
60
|
-
generateForTable(
|
|
61
|
-
|
|
1191
|
+
generateForTable(schema) {
|
|
1192
|
+
const record = {};
|
|
1193
|
+
const ts = Date.now().toString(36);
|
|
1194
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
1195
|
+
for (const field of schema.fields) {
|
|
1196
|
+
if (field.primaryKey) continue;
|
|
1197
|
+
if (field.defaultValue !== void 0) continue;
|
|
1198
|
+
const isForeignKey = field.name.endsWith("_id") && !field.primaryKey;
|
|
1199
|
+
const parentTable = isForeignKey ? field.name.replace(/_id$/, "") : void 0;
|
|
1200
|
+
let value = generateFieldValue(field.name, field.type, isForeignKey, parentTable);
|
|
1201
|
+
if (field.unique && typeof value === "string") {
|
|
1202
|
+
value = `${value}__e2e_test_${ts}_${rand}`;
|
|
1203
|
+
}
|
|
1204
|
+
record[field.name] = value;
|
|
1205
|
+
}
|
|
1206
|
+
return record;
|
|
62
1207
|
},
|
|
63
|
-
generateForTables(
|
|
64
|
-
|
|
1208
|
+
generateForTables(schemas) {
|
|
1209
|
+
const result = /* @__PURE__ */ new Map();
|
|
1210
|
+
for (const schema of schemas) {
|
|
1211
|
+
const record = this.generateForTable(schema);
|
|
1212
|
+
result.set(schema.tableName, [record]);
|
|
1213
|
+
}
|
|
1214
|
+
return result;
|
|
65
1215
|
}
|
|
66
1216
|
};
|
|
67
1217
|
}
|
|
68
1218
|
|
|
69
|
-
// src/
|
|
70
|
-
|
|
1219
|
+
// src/analyzers/impact-reporter.ts
|
|
1220
|
+
init_esm_shims();
|
|
1221
|
+
var MAX_BFS_DEPTH = 5;
|
|
1222
|
+
function extractTablesFromErrorChain(errorChainPath) {
|
|
1223
|
+
const segments = errorChainPath.split("\u2192").map((s) => s.trim());
|
|
1224
|
+
return segments.filter((s) => !s.includes("/") && !s.includes(" ") && s.includes("_"));
|
|
1225
|
+
}
|
|
1226
|
+
function buildTableAdjacency(relations) {
|
|
1227
|
+
const adj = /* @__PURE__ */ new Map();
|
|
1228
|
+
for (const rel of relations) {
|
|
1229
|
+
if (!adj.has(rel.sourceTable)) adj.set(rel.sourceTable, /* @__PURE__ */ new Set());
|
|
1230
|
+
if (!adj.has(rel.targetTable)) adj.set(rel.targetTable, /* @__PURE__ */ new Set());
|
|
1231
|
+
adj.get(rel.sourceTable).add(rel.targetTable);
|
|
1232
|
+
adj.get(rel.targetTable).add(rel.sourceTable);
|
|
1233
|
+
}
|
|
1234
|
+
return adj;
|
|
1235
|
+
}
|
|
1236
|
+
function bfsTraversal(seedTables, adjacency, maxDepth = MAX_BFS_DEPTH) {
|
|
1237
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1238
|
+
const queue = [];
|
|
1239
|
+
for (const t of seedTables) {
|
|
1240
|
+
if (adjacency.has(t)) {
|
|
1241
|
+
queue.push({ table: t, depth: 0 });
|
|
1242
|
+
visited.add(t);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
while (queue.length > 0) {
|
|
1246
|
+
const { table, depth } = queue.shift();
|
|
1247
|
+
if (depth >= maxDepth) continue;
|
|
1248
|
+
for (const neighbor of adjacency.get(table) || []) {
|
|
1249
|
+
if (!visited.has(neighbor)) {
|
|
1250
|
+
visited.add(neighbor);
|
|
1251
|
+
queue.push({ table: neighbor, depth: depth + 1 });
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return Array.from(visited);
|
|
1256
|
+
}
|
|
1257
|
+
function findAffectedEndpoints(tables, analysisResults) {
|
|
1258
|
+
const tableSet = new Set(tables);
|
|
1259
|
+
const affected = [];
|
|
1260
|
+
for (const result of analysisResults) {
|
|
1261
|
+
for (const ep of result.endpoints) {
|
|
1262
|
+
if (ep.relatedTables.some((t) => tableSet.has(t))) {
|
|
1263
|
+
affected.push(ep);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return affected;
|
|
1268
|
+
}
|
|
1269
|
+
function generateMermaidDiagram(seedTables, affectedTables, relations) {
|
|
1270
|
+
const relevantTables = /* @__PURE__ */ new Set([...seedTables, ...affectedTables]);
|
|
1271
|
+
const lines = ["flowchart TD"];
|
|
1272
|
+
const seedSet = new Set(seedTables);
|
|
1273
|
+
for (const t of relevantTables) {
|
|
1274
|
+
const label = seedSet.has(t) ? `${t}:::error` : t;
|
|
1275
|
+
lines.push(` ${sanitizeId(t)}["${label}"]`);
|
|
1276
|
+
}
|
|
1277
|
+
for (const rel of relations) {
|
|
1278
|
+
if (relevantTables.has(rel.sourceTable) && relevantTables.has(rel.targetTable)) {
|
|
1279
|
+
const arrow = rel.isCrossModule ? "-.->" : "-->";
|
|
1280
|
+
lines.push(` ${sanitizeId(rel.sourceTable)} ${arrow}|${rel.targetField}| ${sanitizeId(rel.targetTable)}`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
lines.push(" classDef error fill:#f96,stroke:#333,stroke-width:2px");
|
|
1284
|
+
return lines.join("\n");
|
|
1285
|
+
}
|
|
1286
|
+
function sanitizeId(name) {
|
|
1287
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1288
|
+
}
|
|
1289
|
+
function createImpactReporter() {
|
|
71
1290
|
return {
|
|
72
|
-
|
|
73
|
-
|
|
1291
|
+
analyze(failures, erDiagrams, analysisResults) {
|
|
1292
|
+
const allRelations = [];
|
|
1293
|
+
for (const er of erDiagrams.values()) {
|
|
1294
|
+
allRelations.push(...er.relations);
|
|
1295
|
+
}
|
|
1296
|
+
const seedTables = [];
|
|
1297
|
+
for (const failure of failures) {
|
|
1298
|
+
if (failure.errorChainPath) {
|
|
1299
|
+
seedTables.push(...extractTablesFromErrorChain(failure.errorChainPath));
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const adjacency = buildTableAdjacency(allRelations);
|
|
1303
|
+
const affectedTables = bfsTraversal(seedTables, adjacency);
|
|
1304
|
+
const affectedEndpoints = findAffectedEndpoints(affectedTables, analysisResults);
|
|
1305
|
+
const affectedModules = [...new Set(analysisResults.filter((r) => r.endpoints.some((ep) => affectedEndpoints.includes(ep))).map((r) => r.moduleName))];
|
|
1306
|
+
const affectedChains = failures.map((f) => f.chain);
|
|
1307
|
+
const mermaidText = generateMermaidDiagram(seedTables, affectedTables, allRelations);
|
|
1308
|
+
const count = affectedEndpoints.length;
|
|
1309
|
+
const severity = count > 10 ? "critical" : count > 5 ? "high" : count > 2 ? "medium" : "low";
|
|
1310
|
+
return {
|
|
1311
|
+
affectedModules,
|
|
1312
|
+
affectedChains,
|
|
1313
|
+
affectedEndpoints,
|
|
1314
|
+
affectedTables,
|
|
1315
|
+
severity,
|
|
1316
|
+
mermaidText
|
|
1317
|
+
};
|
|
74
1318
|
}
|
|
75
1319
|
};
|
|
76
1320
|
}
|
|
77
1321
|
|
|
78
|
-
// src/
|
|
79
|
-
|
|
1322
|
+
// src/self-healing/index.ts
|
|
1323
|
+
init_esm_shims();
|
|
1324
|
+
|
|
1325
|
+
// src/llm/index.ts
|
|
1326
|
+
init_esm_shims();
|
|
1327
|
+
|
|
1328
|
+
// src/llm/openai.ts
|
|
1329
|
+
init_esm_shims();
|
|
1330
|
+
var DEFAULT_MODELS = {
|
|
1331
|
+
openai: "gpt-4o-mini",
|
|
1332
|
+
zhipu: "glm-4"
|
|
1333
|
+
};
|
|
1334
|
+
var DEFAULT_BASE_URLS = {
|
|
1335
|
+
openai: "https://api.openai.com/v1",
|
|
1336
|
+
zhipu: "https://open.bigmodel.cn/api/paas/v4"
|
|
1337
|
+
};
|
|
1338
|
+
function createOpenAIProvider(config) {
|
|
1339
|
+
const provider = config.provider === "zhipu" ? "zhipu" : "openai";
|
|
1340
|
+
const baseUrl = config.baseUrl || DEFAULT_BASE_URLS[provider];
|
|
1341
|
+
const model = config.model || DEFAULT_MODELS[provider];
|
|
1342
|
+
const maxTokens = config.maxTokens || 2048;
|
|
1343
|
+
const temperature = config.temperature ?? 0.3;
|
|
1344
|
+
if (!config.apiKey) {
|
|
1345
|
+
throw new Error(
|
|
1346
|
+
`API key is required for ${provider}. Set it in config or via OPENCROC_LLM_API_KEY env variable.`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
80
1349
|
return {
|
|
81
|
-
|
|
82
|
-
|
|
1350
|
+
name: provider,
|
|
1351
|
+
async chat(messages) {
|
|
1352
|
+
const url = `${baseUrl}/chat/completions`;
|
|
1353
|
+
const response = await fetch(url, {
|
|
1354
|
+
method: "POST",
|
|
1355
|
+
headers: {
|
|
1356
|
+
"Content-Type": "application/json",
|
|
1357
|
+
"Authorization": `Bearer ${config.apiKey}`
|
|
1358
|
+
},
|
|
1359
|
+
body: JSON.stringify({
|
|
1360
|
+
model,
|
|
1361
|
+
messages,
|
|
1362
|
+
max_tokens: maxTokens,
|
|
1363
|
+
temperature
|
|
1364
|
+
})
|
|
1365
|
+
});
|
|
1366
|
+
if (!response.ok) {
|
|
1367
|
+
const errorText = await response.text().catch(() => "unknown error");
|
|
1368
|
+
throw new Error(`LLM API error (${response.status}): ${errorText}`);
|
|
1369
|
+
}
|
|
1370
|
+
const data = await response.json();
|
|
1371
|
+
const content = data.choices?.[0]?.message?.content;
|
|
1372
|
+
if (!content) {
|
|
1373
|
+
throw new Error("LLM returned empty response");
|
|
1374
|
+
}
|
|
1375
|
+
return content;
|
|
1376
|
+
},
|
|
1377
|
+
estimateTokens(text) {
|
|
1378
|
+
const cjkChars = (text.match(/[\u4e00-\u9fff\u3000-\u303f]/g) || []).length;
|
|
1379
|
+
const otherChars = text.length - cjkChars;
|
|
1380
|
+
return Math.ceil(otherChars / 4 + cjkChars / 2);
|
|
83
1381
|
}
|
|
84
1382
|
};
|
|
85
1383
|
}
|
|
86
1384
|
|
|
87
|
-
// src/
|
|
88
|
-
|
|
1385
|
+
// src/llm/ollama.ts
|
|
1386
|
+
init_esm_shims();
|
|
1387
|
+
function createOllamaProvider(config) {
|
|
1388
|
+
const baseUrl = config.baseUrl || "http://localhost:11434";
|
|
1389
|
+
const model = config.model || "llama3";
|
|
89
1390
|
return {
|
|
90
|
-
|
|
91
|
-
|
|
1391
|
+
name: "ollama",
|
|
1392
|
+
async chat(messages) {
|
|
1393
|
+
const url = `${baseUrl}/api/chat`;
|
|
1394
|
+
const response = await fetch(url, {
|
|
1395
|
+
method: "POST",
|
|
1396
|
+
headers: { "Content-Type": "application/json" },
|
|
1397
|
+
body: JSON.stringify({
|
|
1398
|
+
model,
|
|
1399
|
+
messages,
|
|
1400
|
+
stream: false
|
|
1401
|
+
})
|
|
1402
|
+
});
|
|
1403
|
+
if (!response.ok) {
|
|
1404
|
+
const errorText = await response.text().catch(() => "unknown error");
|
|
1405
|
+
throw new Error(`Ollama API error (${response.status}): ${errorText}`);
|
|
1406
|
+
}
|
|
1407
|
+
const data = await response.json();
|
|
1408
|
+
const content = data.message?.content;
|
|
1409
|
+
if (!content) {
|
|
1410
|
+
throw new Error("Ollama returned empty response");
|
|
1411
|
+
}
|
|
1412
|
+
return content;
|
|
1413
|
+
},
|
|
1414
|
+
estimateTokens(text) {
|
|
1415
|
+
const cjkChars = (text.match(/[\u4e00-\u9fff\u3000-\u303f]/g) || []).length;
|
|
1416
|
+
const otherChars = text.length - cjkChars;
|
|
1417
|
+
return Math.ceil(otherChars / 4 + cjkChars / 2);
|
|
92
1418
|
}
|
|
93
1419
|
};
|
|
94
1420
|
}
|
|
95
1421
|
|
|
96
|
-
// src/
|
|
97
|
-
function
|
|
98
|
-
|
|
1422
|
+
// src/llm/index.ts
|
|
1423
|
+
function createLlmProvider(config) {
|
|
1424
|
+
const resolved = {
|
|
1425
|
+
...config,
|
|
1426
|
+
apiKey: config.apiKey || process.env.OPENCROC_LLM_API_KEY
|
|
1427
|
+
};
|
|
1428
|
+
switch (config.provider) {
|
|
1429
|
+
case "openai":
|
|
1430
|
+
case "zhipu":
|
|
1431
|
+
return createOpenAIProvider(resolved);
|
|
1432
|
+
case "ollama":
|
|
1433
|
+
return createOllamaProvider(resolved);
|
|
1434
|
+
default:
|
|
1435
|
+
throw new Error(
|
|
1436
|
+
`Unknown LLM provider: "${config.provider}". Available: openai, zhipu, ollama`
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
function createTokenTracker(provider) {
|
|
1441
|
+
let total = 0;
|
|
1442
|
+
return {
|
|
1443
|
+
track(text) {
|
|
1444
|
+
total += provider.estimateTokens(text);
|
|
1445
|
+
},
|
|
1446
|
+
trackChat(messages, response) {
|
|
1447
|
+
for (const msg of messages) {
|
|
1448
|
+
total += provider.estimateTokens(msg.content);
|
|
1449
|
+
}
|
|
1450
|
+
total += provider.estimateTokens(response);
|
|
1451
|
+
},
|
|
1452
|
+
get total() {
|
|
1453
|
+
return total;
|
|
1454
|
+
},
|
|
1455
|
+
reset() {
|
|
1456
|
+
total = 0;
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
99
1459
|
}
|
|
1460
|
+
var SYSTEM_PROMPTS = {
|
|
1461
|
+
failureAnalysis: `You are an expert test failure analyst for an E2E testing framework.
|
|
1462
|
+
Given a test failure error message and its context, analyze the root cause and suggest a fix.
|
|
1463
|
+
Respond in JSON format: { "rootCause": string, "category": string, "suggestedFix": string, "confidence": number }
|
|
1464
|
+
Categories: backend-5xx, timeout, endpoint-not-found, data-constraint, network, frontend-render, test-script, unknown.`,
|
|
1465
|
+
chainPlanning: `You are an API test chain planner.
|
|
1466
|
+
Given a list of API endpoints and their dependencies, generate an optimal test execution order.
|
|
1467
|
+
Consider data dependencies, authentication requirements, and cleanup steps.
|
|
1468
|
+
Respond in JSON format: { "chains": [{ "name": string, "steps": [{ "endpoint": string, "method": string, "description": string }] }] }`
|
|
1469
|
+
};
|
|
100
1470
|
|
|
101
1471
|
// src/self-healing/index.ts
|
|
102
|
-
function
|
|
1472
|
+
function categorizeFailure(errorMessage) {
|
|
1473
|
+
const msg = errorMessage.toLowerCase();
|
|
1474
|
+
if (/5\d{2}|internal server error/.test(msg))
|
|
1475
|
+
return { category: "backend-5xx", confidence: 0.9 };
|
|
1476
|
+
if (/timeout|timed?\s*out/.test(msg))
|
|
1477
|
+
return { category: "timeout", confidence: 0.8 };
|
|
1478
|
+
if (/404|not found/.test(msg))
|
|
1479
|
+
return { category: "endpoint-not-found", confidence: 0.85 };
|
|
1480
|
+
if (/4[0-2]\d|validation|constraint/.test(msg))
|
|
1481
|
+
return { category: "data-constraint", confidence: 0.75 };
|
|
1482
|
+
if (/econnrefused|enotfound|network/.test(msg))
|
|
1483
|
+
return { category: "network", confidence: 0.9 };
|
|
1484
|
+
if (/selector|locator|element/.test(msg))
|
|
1485
|
+
return { category: "frontend-render", confidence: 0.7 };
|
|
1486
|
+
if (/storage\s*state|auth|login/.test(msg))
|
|
1487
|
+
return { category: "test-script", confidence: 0.8 };
|
|
1488
|
+
return { category: "unknown", confidence: 0.5 };
|
|
1489
|
+
}
|
|
1490
|
+
async function analyzeFailureWithLLM(errorMessage, llm) {
|
|
1491
|
+
const heuristic = categorizeFailure(errorMessage);
|
|
1492
|
+
if (!llm) {
|
|
1493
|
+
return {
|
|
1494
|
+
rootCause: errorMessage,
|
|
1495
|
+
category: heuristic.category,
|
|
1496
|
+
suggestedFix: "",
|
|
1497
|
+
confidence: heuristic.confidence
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
const response = await llm.chat([
|
|
1502
|
+
{ role: "system", content: SYSTEM_PROMPTS.failureAnalysis },
|
|
1503
|
+
{ role: "user", content: `Analyze this test failure:
|
|
1504
|
+
|
|
1505
|
+
${errorMessage}` }
|
|
1506
|
+
]);
|
|
1507
|
+
const parsed = JSON.parse(response);
|
|
1508
|
+
return {
|
|
1509
|
+
rootCause: parsed.rootCause || errorMessage,
|
|
1510
|
+
category: parsed.category || heuristic.category,
|
|
1511
|
+
suggestedFix: parsed.suggestedFix || "",
|
|
1512
|
+
confidence: parsed.confidence || heuristic.confidence
|
|
1513
|
+
};
|
|
1514
|
+
} catch {
|
|
1515
|
+
return {
|
|
1516
|
+
rootCause: errorMessage,
|
|
1517
|
+
category: heuristic.category,
|
|
1518
|
+
suggestedFix: "",
|
|
1519
|
+
confidence: heuristic.confidence
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
async function attemptConfigFix(_testResultsDir, _mode, _llm) {
|
|
1524
|
+
return {
|
|
1525
|
+
success: false,
|
|
1526
|
+
scope: "config-only",
|
|
1527
|
+
fixedItems: [],
|
|
1528
|
+
rolledBack: false
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function createSelfHealingLoop(config, llm) {
|
|
1532
|
+
return {
|
|
1533
|
+
async run(testResultsDir) {
|
|
1534
|
+
const maxIterations = config.maxIterations || 3;
|
|
1535
|
+
const mode = config.mode || "config-only";
|
|
1536
|
+
const fixed = [];
|
|
1537
|
+
const remaining = [];
|
|
1538
|
+
let iterations = 0;
|
|
1539
|
+
let totalTokensUsed = 0;
|
|
1540
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
1541
|
+
iterations = i + 1;
|
|
1542
|
+
const outcome = await attemptConfigFix(testResultsDir, mode, llm);
|
|
1543
|
+
if (outcome.success) {
|
|
1544
|
+
fixed.push(...outcome.fixedItems);
|
|
1545
|
+
} else {
|
|
1546
|
+
remaining.push(`iteration-${i + 1}: no fix applied`);
|
|
1547
|
+
}
|
|
1548
|
+
if (llm) {
|
|
1549
|
+
totalTokensUsed += llm.estimateTokens(`iteration-${i + 1}`);
|
|
1550
|
+
}
|
|
1551
|
+
if (outcome.success && outcome.fixedItems.length > 0) break;
|
|
1552
|
+
}
|
|
1553
|
+
return {
|
|
1554
|
+
iterations,
|
|
1555
|
+
fixed,
|
|
1556
|
+
remaining,
|
|
1557
|
+
totalTokensUsed
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// src/adapters/sequelize.ts
|
|
1564
|
+
init_esm_shims();
|
|
1565
|
+
init_controller_parser();
|
|
1566
|
+
function createSequelizeAdapter() {
|
|
1567
|
+
return {
|
|
1568
|
+
name: "sequelize",
|
|
1569
|
+
async parseModels(dir) {
|
|
1570
|
+
return parseModuleModels(dir);
|
|
1571
|
+
},
|
|
1572
|
+
async parseAssociations(file) {
|
|
1573
|
+
return parseAssociationFile(file);
|
|
1574
|
+
},
|
|
1575
|
+
async parseControllers(dir) {
|
|
1576
|
+
const endpoints = parseControllerDirectory(dir);
|
|
1577
|
+
return endpoints.map((ep) => ({
|
|
1578
|
+
method: ep.method,
|
|
1579
|
+
path: ep.path,
|
|
1580
|
+
handler: "",
|
|
1581
|
+
controllerClass: ""
|
|
1582
|
+
}));
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// src/adapters/typeorm.ts
|
|
1588
|
+
init_esm_shims();
|
|
1589
|
+
import * as fs5 from "fs";
|
|
1590
|
+
import * as path6 from "path";
|
|
1591
|
+
import {
|
|
1592
|
+
Project as Project4
|
|
1593
|
+
} from "ts-morph";
|
|
1594
|
+
var TYPEORM_TYPE_MAP = {
|
|
1595
|
+
"PrimaryGeneratedColumn": "BIGINT",
|
|
1596
|
+
"PrimaryColumn": "BIGINT",
|
|
1597
|
+
"CreateDateColumn": "DATE",
|
|
1598
|
+
"UpdateDateColumn": "DATE",
|
|
1599
|
+
"DeleteDateColumn": "DATE",
|
|
1600
|
+
"VersionColumn": "INTEGER"
|
|
1601
|
+
};
|
|
1602
|
+
var TYPEORM_COLUMN_TYPE_MAP = {
|
|
1603
|
+
"varchar": "STRING",
|
|
1604
|
+
"text": "TEXT",
|
|
1605
|
+
"int": "INTEGER",
|
|
1606
|
+
"integer": "INTEGER",
|
|
1607
|
+
"bigint": "BIGINT",
|
|
1608
|
+
"float": "FLOAT",
|
|
1609
|
+
"double": "DOUBLE",
|
|
1610
|
+
"decimal": "DECIMAL",
|
|
1611
|
+
"boolean": "BOOLEAN",
|
|
1612
|
+
"bool": "BOOLEAN",
|
|
1613
|
+
"date": "DATEONLY",
|
|
1614
|
+
"datetime": "DATE",
|
|
1615
|
+
"timestamp": "DATE",
|
|
1616
|
+
"json": "JSON",
|
|
1617
|
+
"jsonb": "JSONB",
|
|
1618
|
+
"enum": "ENUM",
|
|
1619
|
+
"uuid": "UUID"
|
|
1620
|
+
};
|
|
1621
|
+
function tsTypeToFieldType(tsType) {
|
|
1622
|
+
const t = tsType.toLowerCase().trim();
|
|
1623
|
+
if (t === "string") return "STRING";
|
|
1624
|
+
if (t === "number") return "INTEGER";
|
|
1625
|
+
if (t === "boolean") return "BOOLEAN";
|
|
1626
|
+
if (t === "date") return "DATE";
|
|
1627
|
+
return "STRING";
|
|
1628
|
+
}
|
|
1629
|
+
function classNameToTableName2(name) {
|
|
1630
|
+
return name.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
1631
|
+
}
|
|
1632
|
+
function extractDecoratorStringArg(decoratorText) {
|
|
1633
|
+
const match = decoratorText.match(/\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
1634
|
+
return match?.[1];
|
|
1635
|
+
}
|
|
1636
|
+
function extractDecoratorObjectArg(decoratorText) {
|
|
1637
|
+
const result = {};
|
|
1638
|
+
const objMatch = decoratorText.match(/\(\s*\{([^}]*)\}\s*\)/);
|
|
1639
|
+
if (!objMatch) return result;
|
|
1640
|
+
const body = objMatch[1];
|
|
1641
|
+
const pairs = body.matchAll(/(\w+)\s*:\s*['"]([^'"]*)['"]/g);
|
|
1642
|
+
for (const pair of pairs) {
|
|
1643
|
+
result[pair[1]] = pair[2];
|
|
1644
|
+
}
|
|
1645
|
+
const boolPairs = body.matchAll(/(\w+)\s*:\s*(true|false)/g);
|
|
1646
|
+
for (const pair of boolPairs) {
|
|
1647
|
+
result[pair[1]] = pair[2];
|
|
1648
|
+
}
|
|
1649
|
+
return result;
|
|
1650
|
+
}
|
|
1651
|
+
function parseTypeORMFile(filePath) {
|
|
1652
|
+
const absolutePath = path6.resolve(filePath);
|
|
1653
|
+
if (!fs5.existsSync(absolutePath)) return null;
|
|
1654
|
+
const project = new Project4({ compilerOptions: { strict: false } });
|
|
1655
|
+
const sourceFile = project.addSourceFileAtPath(absolutePath);
|
|
1656
|
+
const classes = sourceFile.getClasses();
|
|
1657
|
+
for (const cls of classes) {
|
|
1658
|
+
const entityDecorator = cls.getDecorator("Entity");
|
|
1659
|
+
if (!entityDecorator) continue;
|
|
1660
|
+
const tableName = extractDecoratorStringArg(entityDecorator.getText()) || extractDecoratorObjectArg(entityDecorator.getText()).name || classNameToTableName2(cls.getName() || "unknown");
|
|
1661
|
+
const fields = extractTypeORMFields(cls);
|
|
1662
|
+
return { tableName, className: cls.getName(), fields };
|
|
1663
|
+
}
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
function extractTypeORMFields(cls) {
|
|
1667
|
+
const fields = [];
|
|
1668
|
+
for (const prop of cls.getProperties()) {
|
|
1669
|
+
const field = parseTypeORMProperty(prop);
|
|
1670
|
+
if (field) fields.push(field);
|
|
1671
|
+
}
|
|
1672
|
+
return fields;
|
|
1673
|
+
}
|
|
1674
|
+
function parseTypeORMProperty(prop) {
|
|
1675
|
+
const decorators = prop.getDecorators();
|
|
1676
|
+
if (decorators.length === 0) return null;
|
|
1677
|
+
const name = prop.getName();
|
|
1678
|
+
let type = "STRING";
|
|
1679
|
+
let primaryKey = false;
|
|
1680
|
+
let allowNull = true;
|
|
1681
|
+
let unique = false;
|
|
1682
|
+
for (const dec of decorators) {
|
|
1683
|
+
const decName = dec.getName();
|
|
1684
|
+
const decText = dec.getText();
|
|
1685
|
+
if (decName === "PrimaryGeneratedColumn" || decName === "PrimaryColumn") {
|
|
1686
|
+
primaryKey = true;
|
|
1687
|
+
type = TYPEORM_TYPE_MAP[decName] || "BIGINT";
|
|
1688
|
+
allowNull = false;
|
|
1689
|
+
const argType = extractDecoratorStringArg(decText);
|
|
1690
|
+
if (argType === "uuid") type = "UUID";
|
|
1691
|
+
if (argType === "increment") type = "BIGINT";
|
|
1692
|
+
}
|
|
1693
|
+
if (decName === "Column") {
|
|
1694
|
+
const objArgs = extractDecoratorObjectArg(decText);
|
|
1695
|
+
if (objArgs.type && TYPEORM_COLUMN_TYPE_MAP[objArgs.type]) {
|
|
1696
|
+
type = TYPEORM_COLUMN_TYPE_MAP[objArgs.type];
|
|
1697
|
+
} else {
|
|
1698
|
+
const simpleType = extractDecoratorStringArg(decText);
|
|
1699
|
+
if (simpleType && TYPEORM_COLUMN_TYPE_MAP[simpleType]) {
|
|
1700
|
+
type = TYPEORM_COLUMN_TYPE_MAP[simpleType];
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (objArgs.nullable === "false") allowNull = false;
|
|
1704
|
+
if (objArgs.unique === "true") unique = true;
|
|
1705
|
+
if (type === "STRING") {
|
|
1706
|
+
const tsType = prop.getType().getText();
|
|
1707
|
+
type = tsTypeToFieldType(tsType);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (decName in TYPEORM_TYPE_MAP) {
|
|
1711
|
+
type = TYPEORM_TYPE_MAP[decName];
|
|
1712
|
+
}
|
|
1713
|
+
if (decName === "CreateDateColumn" || decName === "UpdateDateColumn" || decName === "DeleteDateColumn") {
|
|
1714
|
+
allowNull = true;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const recognizedDecorators = [
|
|
1718
|
+
"Column",
|
|
1719
|
+
"PrimaryGeneratedColumn",
|
|
1720
|
+
"PrimaryColumn",
|
|
1721
|
+
"CreateDateColumn",
|
|
1722
|
+
"UpdateDateColumn",
|
|
1723
|
+
"DeleteDateColumn",
|
|
1724
|
+
"VersionColumn",
|
|
1725
|
+
"ManyToOne",
|
|
1726
|
+
"OneToMany",
|
|
1727
|
+
"OneToOne",
|
|
1728
|
+
"ManyToMany",
|
|
1729
|
+
"JoinColumn",
|
|
1730
|
+
"JoinTable"
|
|
1731
|
+
];
|
|
1732
|
+
const hasRecognized = decorators.some((d) => recognizedDecorators.includes(d.getName()));
|
|
1733
|
+
if (!hasRecognized) return null;
|
|
1734
|
+
const isRelationOnly = decorators.every(
|
|
1735
|
+
(d) => ["ManyToOne", "OneToMany", "OneToOne", "ManyToMany", "JoinColumn", "JoinTable"].includes(d.getName())
|
|
1736
|
+
);
|
|
1737
|
+
if (isRelationOnly) return null;
|
|
1738
|
+
return { name, type, allowNull, primaryKey, unique };
|
|
1739
|
+
}
|
|
1740
|
+
function parseTypeORMAssociations(filePath) {
|
|
1741
|
+
const absolutePath = path6.resolve(filePath);
|
|
1742
|
+
if (!fs5.existsSync(absolutePath)) return [];
|
|
1743
|
+
const project = new Project4({ compilerOptions: { strict: false } });
|
|
1744
|
+
const sourceFile = project.addSourceFileAtPath(absolutePath);
|
|
1745
|
+
const relations = [];
|
|
1746
|
+
for (const cls of sourceFile.getClasses()) {
|
|
1747
|
+
const entityDecorator = cls.getDecorator("Entity");
|
|
1748
|
+
if (!entityDecorator) continue;
|
|
1749
|
+
const sourceTable = extractDecoratorStringArg(entityDecorator.getText()) || classNameToTableName2(cls.getName() || "unknown");
|
|
1750
|
+
for (const prop of cls.getProperties()) {
|
|
1751
|
+
const rel = extractRelationFromProperty(prop, sourceTable);
|
|
1752
|
+
if (rel) relations.push(rel);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return relations;
|
|
1756
|
+
}
|
|
1757
|
+
function extractRelationFromProperty(prop, sourceTable) {
|
|
1758
|
+
const decorators = prop.getDecorators();
|
|
1759
|
+
for (const dec of decorators) {
|
|
1760
|
+
const decName = dec.getName();
|
|
1761
|
+
const decText = dec.getText();
|
|
1762
|
+
if (decName === "ManyToOne") {
|
|
1763
|
+
const targetClass = extractRelationTarget(decText);
|
|
1764
|
+
if (!targetClass) continue;
|
|
1765
|
+
const targetTable = classNameToTableName2(targetClass);
|
|
1766
|
+
const fkField = findJoinColumnField(decorators) || `${prop.getName()}_id`;
|
|
1767
|
+
return {
|
|
1768
|
+
sourceTable,
|
|
1769
|
+
sourceField: fkField,
|
|
1770
|
+
targetTable,
|
|
1771
|
+
targetField: "id",
|
|
1772
|
+
cardinality: "N:1"
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
if (decName === "OneToMany") {
|
|
1776
|
+
const targetClass = extractRelationTarget(decText);
|
|
1777
|
+
if (!targetClass) continue;
|
|
1778
|
+
const targetTable = classNameToTableName2(targetClass);
|
|
1779
|
+
return {
|
|
1780
|
+
sourceTable,
|
|
1781
|
+
sourceField: "id",
|
|
1782
|
+
targetTable,
|
|
1783
|
+
targetField: `${classNameToTableName2(sourceTable)}_id`,
|
|
1784
|
+
cardinality: "1:N"
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
if (decName === "OneToOne") {
|
|
1788
|
+
const targetClass = extractRelationTarget(decText);
|
|
1789
|
+
if (!targetClass) continue;
|
|
1790
|
+
const targetTable = classNameToTableName2(targetClass);
|
|
1791
|
+
return {
|
|
1792
|
+
sourceTable,
|
|
1793
|
+
sourceField: "id",
|
|
1794
|
+
targetTable,
|
|
1795
|
+
targetField: `${classNameToTableName2(sourceTable)}_id`,
|
|
1796
|
+
cardinality: "1:1"
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
function extractRelationTarget(decoratorText) {
|
|
1803
|
+
const match = decoratorText.match(/\(\s*(?:\(\)\s*=>|type\s*=>|\w+\s*=>)\s*(\w+)/);
|
|
1804
|
+
return match?.[1] || null;
|
|
1805
|
+
}
|
|
1806
|
+
function findJoinColumnField(decorators) {
|
|
1807
|
+
for (const dec of decorators) {
|
|
1808
|
+
if (dec.getName() === "JoinColumn") {
|
|
1809
|
+
const args = extractDecoratorObjectArg(dec.getText());
|
|
1810
|
+
if (args.name) return args.name;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
function parseTypeORMDirectory(dir) {
|
|
1816
|
+
const absoluteDir = path6.resolve(dir);
|
|
1817
|
+
if (!fs5.existsSync(absoluteDir)) return [];
|
|
1818
|
+
const files = fs5.readdirSync(absoluteDir).filter(
|
|
1819
|
+
(f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts"
|
|
1820
|
+
);
|
|
1821
|
+
const schemas = [];
|
|
1822
|
+
for (const file of files) {
|
|
1823
|
+
try {
|
|
1824
|
+
const schema = parseTypeORMFile(path6.join(absoluteDir, file));
|
|
1825
|
+
if (schema) schemas.push(schema);
|
|
1826
|
+
} catch {
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
return schemas;
|
|
1830
|
+
}
|
|
1831
|
+
function parseTypeORMAssociationsFromDir(dir) {
|
|
1832
|
+
const absoluteDir = path6.resolve(dir);
|
|
1833
|
+
if (!fs5.existsSync(absoluteDir)) return [];
|
|
1834
|
+
const files = fs5.readdirSync(absoluteDir).filter(
|
|
1835
|
+
(f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".spec.ts") && f !== "index.ts"
|
|
1836
|
+
);
|
|
1837
|
+
const relations = [];
|
|
1838
|
+
for (const file of files) {
|
|
1839
|
+
try {
|
|
1840
|
+
relations.push(...parseTypeORMAssociations(path6.join(absoluteDir, file)));
|
|
1841
|
+
} catch {
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
return relations;
|
|
1845
|
+
}
|
|
1846
|
+
function createTypeORMAdapter() {
|
|
103
1847
|
return {
|
|
104
|
-
|
|
105
|
-
|
|
1848
|
+
name: "typeorm",
|
|
1849
|
+
async parseModels(dir) {
|
|
1850
|
+
return parseTypeORMDirectory(dir);
|
|
1851
|
+
},
|
|
1852
|
+
async parseAssociations(file) {
|
|
1853
|
+
const dir = path6.dirname(file);
|
|
1854
|
+
return parseTypeORMAssociationsFromDir(dir);
|
|
1855
|
+
},
|
|
1856
|
+
async parseControllers(dir) {
|
|
1857
|
+
const { parseControllerDirectory: parseControllerDirectory2 } = await Promise.resolve().then(() => (init_controller_parser(), controller_parser_exports));
|
|
1858
|
+
const endpoints = parseControllerDirectory2(dir);
|
|
1859
|
+
return endpoints.map((ep) => ({
|
|
1860
|
+
method: ep.method,
|
|
1861
|
+
path: ep.path,
|
|
1862
|
+
handler: "",
|
|
1863
|
+
controllerClass: ""
|
|
1864
|
+
}));
|
|
106
1865
|
}
|
|
107
1866
|
};
|
|
108
1867
|
}
|
|
1868
|
+
|
|
1869
|
+
// src/adapters/prisma.ts
|
|
1870
|
+
init_esm_shims();
|
|
1871
|
+
import * as fs6 from "fs";
|
|
1872
|
+
import * as path7 from "path";
|
|
1873
|
+
var PRISMA_TYPE_MAP = {
|
|
1874
|
+
"String": "STRING",
|
|
1875
|
+
"Int": "INTEGER",
|
|
1876
|
+
"BigInt": "BIGINT",
|
|
1877
|
+
"Float": "FLOAT",
|
|
1878
|
+
"Decimal": "DECIMAL",
|
|
1879
|
+
"Boolean": "BOOLEAN",
|
|
1880
|
+
"DateTime": "DATE",
|
|
1881
|
+
"Json": "JSON",
|
|
1882
|
+
"Bytes": "BLOB"
|
|
1883
|
+
};
|
|
1884
|
+
function parsePrismaSchema(content) {
|
|
1885
|
+
const models = [];
|
|
1886
|
+
const modelRegex = /model\s+(\w+)\s*\{([^}]*)\}/g;
|
|
1887
|
+
let match;
|
|
1888
|
+
while ((match = modelRegex.exec(content)) !== null) {
|
|
1889
|
+
const modelName = match[1];
|
|
1890
|
+
const body = match[2];
|
|
1891
|
+
const fields = parsePrismaFields(body);
|
|
1892
|
+
const mapDirective = body.match(/@@map\(["']([^"']+)["']\)/);
|
|
1893
|
+
models.push({
|
|
1894
|
+
name: modelName,
|
|
1895
|
+
fields,
|
|
1896
|
+
tableName: mapDirective?.[1]
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
return models;
|
|
1900
|
+
}
|
|
1901
|
+
function parsePrismaFields(body) {
|
|
1902
|
+
const fields = [];
|
|
1903
|
+
const lines = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"));
|
|
1904
|
+
for (const line of lines) {
|
|
1905
|
+
const field = parsePrismaFieldLine(line);
|
|
1906
|
+
if (field) fields.push(field);
|
|
1907
|
+
}
|
|
1908
|
+
return fields;
|
|
1909
|
+
}
|
|
1910
|
+
function parsePrismaFieldLine(line) {
|
|
1911
|
+
const match = line.match(/^(\w+)\s+(\w+)(\[\])?\??/);
|
|
1912
|
+
if (!match) return null;
|
|
1913
|
+
const name = match[1];
|
|
1914
|
+
const rawType = match[2];
|
|
1915
|
+
const isList = !!match[3];
|
|
1916
|
+
const isOptional = line.includes("?");
|
|
1917
|
+
const field = {
|
|
1918
|
+
name,
|
|
1919
|
+
type: rawType,
|
|
1920
|
+
isOptional,
|
|
1921
|
+
isList,
|
|
1922
|
+
isId: /@id\b/.test(line),
|
|
1923
|
+
isUnique: /@unique\b/.test(line),
|
|
1924
|
+
isUpdatedAt: /@updatedAt\b/.test(line)
|
|
1925
|
+
};
|
|
1926
|
+
const defaultMatch = line.match(/@default\(([^)]+)\)/);
|
|
1927
|
+
if (defaultMatch) field.defaultValue = defaultMatch[1];
|
|
1928
|
+
const mapMatch = line.match(/@map\(["']([^"']+)["']\)/);
|
|
1929
|
+
if (mapMatch) field.mapName = mapMatch[1];
|
|
1930
|
+
const nativeMatch = line.match(/@db\.(\w+(?:\([^)]*\))?)/);
|
|
1931
|
+
if (nativeMatch) field.nativeType = nativeMatch[1];
|
|
1932
|
+
const relMatch = line.match(/@relation\(([^)]*)\)/);
|
|
1933
|
+
if (relMatch) {
|
|
1934
|
+
field.relation = parseRelationDirective(relMatch[1]);
|
|
1935
|
+
}
|
|
1936
|
+
return field;
|
|
1937
|
+
}
|
|
1938
|
+
function parseRelationDirective(content) {
|
|
1939
|
+
const rel = {};
|
|
1940
|
+
const nameMatch = content.match(/(?:name:\s*)?["']([^"']+)["']/);
|
|
1941
|
+
if (nameMatch) rel.name = nameMatch[1];
|
|
1942
|
+
const fieldsMatch = content.match(/fields:\s*\[([^\]]+)\]/);
|
|
1943
|
+
if (fieldsMatch) {
|
|
1944
|
+
rel.fields = fieldsMatch[1].split(",").map((s) => s.trim());
|
|
1945
|
+
}
|
|
1946
|
+
const refsMatch = content.match(/references:\s*\[([^\]]+)\]/);
|
|
1947
|
+
if (refsMatch) {
|
|
1948
|
+
rel.references = refsMatch[1].split(",").map((s) => s.trim());
|
|
1949
|
+
}
|
|
1950
|
+
return rel;
|
|
1951
|
+
}
|
|
1952
|
+
function modelNameToTableName(name) {
|
|
1953
|
+
return name.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
1954
|
+
}
|
|
1955
|
+
function prismaModelsToSchemas(models) {
|
|
1956
|
+
return models.map((model) => {
|
|
1957
|
+
const tableName = model.tableName || modelNameToTableName(model.name);
|
|
1958
|
+
const fields = [];
|
|
1959
|
+
for (const f of model.fields) {
|
|
1960
|
+
if (f.isList) continue;
|
|
1961
|
+
if (!PRISMA_TYPE_MAP[f.type] && !f.relation) continue;
|
|
1962
|
+
if (!PRISMA_TYPE_MAP[f.type] && f.relation && !f.relation.fields) continue;
|
|
1963
|
+
const fieldType = PRISMA_TYPE_MAP[f.type] || "STRING";
|
|
1964
|
+
fields.push({
|
|
1965
|
+
name: f.mapName || f.name,
|
|
1966
|
+
type: fieldType,
|
|
1967
|
+
allowNull: f.isOptional,
|
|
1968
|
+
primaryKey: f.isId,
|
|
1969
|
+
unique: f.isUnique,
|
|
1970
|
+
defaultValue: f.defaultValue
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
return { tableName, className: model.name, fields };
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
function prismaModelsToRelations(models) {
|
|
1977
|
+
const relations = [];
|
|
1978
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1979
|
+
for (const model of models) {
|
|
1980
|
+
const sourceTable = model.tableName || modelNameToTableName(model.name);
|
|
1981
|
+
for (const field of model.fields) {
|
|
1982
|
+
if (!field.relation?.fields || !field.relation?.references) continue;
|
|
1983
|
+
const targetModel = models.find((m) => m.name === field.type);
|
|
1984
|
+
const targetTable = targetModel ? targetModel.tableName || modelNameToTableName(targetModel.name) : modelNameToTableName(field.type);
|
|
1985
|
+
const sourceField = field.relation.fields[0];
|
|
1986
|
+
const targetField = field.relation.references[0];
|
|
1987
|
+
const key = `${sourceTable}|${sourceField}|${targetTable}|${targetField}`;
|
|
1988
|
+
if (seen.has(key)) continue;
|
|
1989
|
+
seen.add(key);
|
|
1990
|
+
const isList = field.isList;
|
|
1991
|
+
relations.push({
|
|
1992
|
+
sourceTable,
|
|
1993
|
+
sourceField,
|
|
1994
|
+
targetTable,
|
|
1995
|
+
targetField,
|
|
1996
|
+
cardinality: isList ? "1:N" : "N:1"
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return relations;
|
|
2001
|
+
}
|
|
2002
|
+
function parsePrismaFile(filePath) {
|
|
2003
|
+
const absolutePath = path7.resolve(filePath);
|
|
2004
|
+
if (!fs6.existsSync(absolutePath)) return { schemas: [], relations: [] };
|
|
2005
|
+
const content = fs6.readFileSync(absolutePath, "utf-8");
|
|
2006
|
+
const models = parsePrismaSchema(content);
|
|
2007
|
+
return {
|
|
2008
|
+
schemas: prismaModelsToSchemas(models),
|
|
2009
|
+
relations: prismaModelsToRelations(models)
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
function findPrismaSchemaFile(dir) {
|
|
2013
|
+
const candidates = [
|
|
2014
|
+
path7.join(dir, "schema.prisma"),
|
|
2015
|
+
path7.join(dir, "prisma", "schema.prisma"),
|
|
2016
|
+
path7.join(dir, "..", "prisma", "schema.prisma")
|
|
2017
|
+
];
|
|
2018
|
+
for (const c of candidates) {
|
|
2019
|
+
if (fs6.existsSync(c)) return c;
|
|
2020
|
+
}
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
function createPrismaAdapter() {
|
|
2024
|
+
return {
|
|
2025
|
+
name: "prisma",
|
|
2026
|
+
async parseModels(dir) {
|
|
2027
|
+
const schemaFile = findPrismaSchemaFile(dir);
|
|
2028
|
+
if (!schemaFile) return [];
|
|
2029
|
+
const { schemas } = parsePrismaFile(schemaFile);
|
|
2030
|
+
return schemas;
|
|
2031
|
+
},
|
|
2032
|
+
async parseAssociations(file) {
|
|
2033
|
+
const schemaFile = findPrismaSchemaFile(path7.dirname(file)) || file;
|
|
2034
|
+
const { relations } = parsePrismaFile(schemaFile);
|
|
2035
|
+
return relations;
|
|
2036
|
+
},
|
|
2037
|
+
async parseControllers(dir) {
|
|
2038
|
+
const { parseControllerDirectory: parseControllerDirectory2 } = await Promise.resolve().then(() => (init_controller_parser(), controller_parser_exports));
|
|
2039
|
+
const endpoints = parseControllerDirectory2(dir);
|
|
2040
|
+
return endpoints.map((ep) => ({
|
|
2041
|
+
method: ep.method,
|
|
2042
|
+
path: ep.path,
|
|
2043
|
+
handler: "",
|
|
2044
|
+
controllerClass: ""
|
|
2045
|
+
}));
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// src/adapters/registry.ts
|
|
2051
|
+
init_esm_shims();
|
|
2052
|
+
import * as fs7 from "fs";
|
|
2053
|
+
import * as path8 from "path";
|
|
2054
|
+
var ADAPTER_FACTORIES = {
|
|
2055
|
+
sequelize: createSequelizeAdapter,
|
|
2056
|
+
typeorm: createTypeORMAdapter,
|
|
2057
|
+
prisma: createPrismaAdapter
|
|
2058
|
+
};
|
|
2059
|
+
function createAdapter(name) {
|
|
2060
|
+
const factory = ADAPTER_FACTORIES[name];
|
|
2061
|
+
if (!factory) {
|
|
2062
|
+
throw new Error(`Unknown adapter: "${name}". Available: ${Object.keys(ADAPTER_FACTORIES).join(", ")}`);
|
|
2063
|
+
}
|
|
2064
|
+
return factory();
|
|
2065
|
+
}
|
|
2066
|
+
function detectAdapter(backendRoot) {
|
|
2067
|
+
const root = path8.resolve(backendRoot);
|
|
2068
|
+
const prismaLocations = [
|
|
2069
|
+
path8.join(root, "prisma", "schema.prisma"),
|
|
2070
|
+
path8.join(root, "schema.prisma"),
|
|
2071
|
+
path8.join(root, "..", "prisma", "schema.prisma")
|
|
2072
|
+
];
|
|
2073
|
+
for (const loc of prismaLocations) {
|
|
2074
|
+
if (fs7.existsSync(loc)) return "prisma";
|
|
2075
|
+
}
|
|
2076
|
+
const modelsDir = path8.join(root, "models");
|
|
2077
|
+
if (fs7.existsSync(modelsDir)) {
|
|
2078
|
+
try {
|
|
2079
|
+
const files = fs7.readdirSync(modelsDir, { recursive: true });
|
|
2080
|
+
for (const file of files) {
|
|
2081
|
+
const filePath = path8.join(modelsDir, String(file));
|
|
2082
|
+
if (!filePath.endsWith(".ts")) continue;
|
|
2083
|
+
try {
|
|
2084
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
2085
|
+
if (/@Entity\s*\(/.test(content)) return "typeorm";
|
|
2086
|
+
} catch {
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
} catch {
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return "sequelize";
|
|
2093
|
+
}
|
|
2094
|
+
function resolveAdapter(adapterOrName, backendRoot) {
|
|
2095
|
+
if (typeof adapterOrName === "object" && adapterOrName !== null) {
|
|
2096
|
+
return adapterOrName;
|
|
2097
|
+
}
|
|
2098
|
+
const name = adapterOrName === "auto" || !adapterOrName ? detectAdapter(backendRoot) : adapterOrName;
|
|
2099
|
+
return createAdapter(name);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// src/plugins/index.ts
|
|
2103
|
+
init_esm_shims();
|
|
2104
|
+
function createPluginRegistry() {
|
|
2105
|
+
const plugins = [];
|
|
2106
|
+
function register(plugin) {
|
|
2107
|
+
if (!plugin.name) {
|
|
2108
|
+
throw new Error("Plugin must have a name");
|
|
2109
|
+
}
|
|
2110
|
+
if (plugins.some((p) => p.name === plugin.name)) {
|
|
2111
|
+
throw new Error(`Plugin "${plugin.name}" is already registered`);
|
|
2112
|
+
}
|
|
2113
|
+
plugins.push(plugin);
|
|
2114
|
+
}
|
|
2115
|
+
async function unregister(name) {
|
|
2116
|
+
const idx = plugins.findIndex((p) => p.name === name);
|
|
2117
|
+
if (idx === -1) return;
|
|
2118
|
+
const plugin = plugins[idx];
|
|
2119
|
+
if (plugin.teardown) {
|
|
2120
|
+
await plugin.teardown();
|
|
2121
|
+
}
|
|
2122
|
+
plugins.splice(idx, 1);
|
|
2123
|
+
}
|
|
2124
|
+
function get(name) {
|
|
2125
|
+
return plugins.find((p) => p.name === name);
|
|
2126
|
+
}
|
|
2127
|
+
function list() {
|
|
2128
|
+
return plugins.map((p) => p.name);
|
|
2129
|
+
}
|
|
2130
|
+
async function invoke(hook, ...args) {
|
|
2131
|
+
for (const plugin of plugins) {
|
|
2132
|
+
const fn = plugin[hook];
|
|
2133
|
+
if (typeof fn === "function") {
|
|
2134
|
+
await fn.apply(plugin, args);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
async function applyConfigTransforms(config) {
|
|
2139
|
+
let result = config;
|
|
2140
|
+
for (const plugin of plugins) {
|
|
2141
|
+
if (plugin.transformConfig) {
|
|
2142
|
+
result = await plugin.transformConfig(result);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return result;
|
|
2146
|
+
}
|
|
2147
|
+
return { register, unregister, get, list, invoke, applyConfigTransforms };
|
|
2148
|
+
}
|
|
2149
|
+
function definePlugin(plugin) {
|
|
2150
|
+
return plugin;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// src/ci/index.ts
|
|
2154
|
+
init_esm_shims();
|
|
2155
|
+
function generateGitHubActionsTemplate(opts = {}) {
|
|
2156
|
+
const nodeVersions = opts.nodeVersions ?? ["20.x"];
|
|
2157
|
+
const install = opts.installCommand ?? "npm ci";
|
|
2158
|
+
const genArgs = opts.generateArgs ?? "--all";
|
|
2159
|
+
const testArgs = opts.testArgs ?? "";
|
|
2160
|
+
const healStep = opts.selfHeal ? `
|
|
2161
|
+
- name: Self-heal failures
|
|
2162
|
+
if: failure()
|
|
2163
|
+
run: npx opencroc heal --max-iterations 3` : "";
|
|
2164
|
+
const matrix = nodeVersions.length > 1 ? `
|
|
2165
|
+
strategy:
|
|
2166
|
+
matrix:
|
|
2167
|
+
node-version: [${nodeVersions.join(", ")}]` : "";
|
|
2168
|
+
const nodeSetup = nodeVersions.length > 1 ? "${{ matrix.node-version }}" : nodeVersions[0];
|
|
2169
|
+
return `# Generated by OpenCroc \u2014 AI-native E2E testing
|
|
2170
|
+
# https://github.com/opencroc/opencroc
|
|
2171
|
+
|
|
2172
|
+
name: OpenCroc E2E
|
|
2173
|
+
|
|
2174
|
+
on:
|
|
2175
|
+
push:
|
|
2176
|
+
branches: [main]
|
|
2177
|
+
pull_request:
|
|
2178
|
+
branches: [main]
|
|
2179
|
+
|
|
2180
|
+
jobs:
|
|
2181
|
+
e2e:
|
|
2182
|
+
runs-on: ubuntu-latest${matrix}
|
|
2183
|
+
steps:
|
|
2184
|
+
- uses: actions/checkout@v4
|
|
2185
|
+
|
|
2186
|
+
- uses: actions/setup-node@v4
|
|
2187
|
+
with:
|
|
2188
|
+
node-version: '${nodeSetup}'
|
|
2189
|
+
|
|
2190
|
+
- name: Install dependencies
|
|
2191
|
+
run: ${install}
|
|
2192
|
+
|
|
2193
|
+
- name: Install Playwright browsers
|
|
2194
|
+
run: npx playwright install --with-deps chromium
|
|
2195
|
+
|
|
2196
|
+
- name: Generate E2E tests
|
|
2197
|
+
run: npx opencroc generate ${genArgs}
|
|
2198
|
+
|
|
2199
|
+
- name: Run E2E tests
|
|
2200
|
+
run: npx opencroc test ${testArgs}
|
|
2201
|
+
${healStep}
|
|
2202
|
+
- name: Upload test report
|
|
2203
|
+
if: always()
|
|
2204
|
+
uses: actions/upload-artifact@v4
|
|
2205
|
+
with:
|
|
2206
|
+
name: opencroc-report
|
|
2207
|
+
path: opencroc-output/
|
|
2208
|
+
retention-days: 14
|
|
2209
|
+
`;
|
|
2210
|
+
}
|
|
2211
|
+
function generateGitLabCITemplate(opts = {}) {
|
|
2212
|
+
const install = opts.installCommand ?? "npm ci";
|
|
2213
|
+
const genArgs = opts.generateArgs ?? "--all";
|
|
2214
|
+
const testArgs = opts.testArgs ?? "";
|
|
2215
|
+
const nodeVersion = opts.nodeVersions?.[0] ?? "20";
|
|
2216
|
+
return `# Generated by OpenCroc \u2014 AI-native E2E testing
|
|
2217
|
+
# https://github.com/opencroc/opencroc
|
|
2218
|
+
|
|
2219
|
+
image: node:${nodeVersion}
|
|
2220
|
+
|
|
2221
|
+
stages:
|
|
2222
|
+
- generate
|
|
2223
|
+
- test
|
|
2224
|
+
|
|
2225
|
+
variables:
|
|
2226
|
+
PLAYWRIGHT_BROWSERS_PATH: \${CI_PROJECT_DIR}/.cache/ms-playwright
|
|
2227
|
+
|
|
2228
|
+
cache:
|
|
2229
|
+
key: \${CI_COMMIT_REF_SLUG}
|
|
2230
|
+
paths:
|
|
2231
|
+
- node_modules/
|
|
2232
|
+
- .cache/ms-playwright/
|
|
2233
|
+
|
|
2234
|
+
generate:
|
|
2235
|
+
stage: generate
|
|
2236
|
+
script:
|
|
2237
|
+
- ${install}
|
|
2238
|
+
- npx opencroc generate ${genArgs}
|
|
2239
|
+
artifacts:
|
|
2240
|
+
paths:
|
|
2241
|
+
- opencroc-output/
|
|
2242
|
+
expire_in: 1 day
|
|
2243
|
+
|
|
2244
|
+
e2e:
|
|
2245
|
+
stage: test
|
|
2246
|
+
needs: [generate]
|
|
2247
|
+
before_script:
|
|
2248
|
+
- ${install}
|
|
2249
|
+
- npx playwright install --with-deps chromium
|
|
2250
|
+
script:
|
|
2251
|
+
- npx opencroc test ${testArgs}
|
|
2252
|
+
artifacts:
|
|
2253
|
+
when: always
|
|
2254
|
+
paths:
|
|
2255
|
+
- opencroc-output/
|
|
2256
|
+
expire_in: 14 days
|
|
2257
|
+
`;
|
|
2258
|
+
}
|
|
2259
|
+
var TEMPLATES = {
|
|
2260
|
+
github: generateGitHubActionsTemplate,
|
|
2261
|
+
gitlab: generateGitLabCITemplate
|
|
2262
|
+
};
|
|
2263
|
+
function listCiPlatforms() {
|
|
2264
|
+
return Object.keys(TEMPLATES);
|
|
2265
|
+
}
|
|
2266
|
+
function generateCiTemplate(platform, opts = {}) {
|
|
2267
|
+
const generator = TEMPLATES[platform];
|
|
2268
|
+
if (!generator) {
|
|
2269
|
+
throw new Error(
|
|
2270
|
+
`Unknown CI platform: "${platform}". Available: ${Object.keys(TEMPLATES).join(", ")}`
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
return generator(opts);
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// src/reporters/index.ts
|
|
2277
|
+
init_esm_shims();
|
|
2278
|
+
function generateJsonReport(result) {
|
|
2279
|
+
const serializable = {
|
|
2280
|
+
modules: result.modules,
|
|
2281
|
+
erDiagrams: Object.fromEntries(
|
|
2282
|
+
Array.from(result.erDiagrams.entries()).map(([k, v]) => [
|
|
2283
|
+
k,
|
|
2284
|
+
{ tables: v.tables.length, relations: v.relations.length, mermaidText: v.mermaidText }
|
|
2285
|
+
])
|
|
2286
|
+
),
|
|
2287
|
+
chainPlans: Object.fromEntries(
|
|
2288
|
+
Array.from(result.chainPlans.entries()).map(([k, v]) => [
|
|
2289
|
+
k,
|
|
2290
|
+
{ chains: v.chains.length, totalSteps: v.totalSteps }
|
|
2291
|
+
])
|
|
2292
|
+
),
|
|
2293
|
+
generatedFiles: result.generatedFiles.map((f) => ({
|
|
2294
|
+
filePath: f.filePath,
|
|
2295
|
+
module: f.module,
|
|
2296
|
+
chain: f.chain
|
|
2297
|
+
})),
|
|
2298
|
+
validationErrors: result.validationErrors,
|
|
2299
|
+
duration: result.duration
|
|
2300
|
+
};
|
|
2301
|
+
return {
|
|
2302
|
+
format: "json",
|
|
2303
|
+
content: JSON.stringify(serializable, null, 2),
|
|
2304
|
+
filename: "opencroc-report.json"
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
function generateMarkdownReport(result) {
|
|
2308
|
+
const lines = [
|
|
2309
|
+
"# OpenCroc Report",
|
|
2310
|
+
"",
|
|
2311
|
+
`**Duration**: ${result.duration}ms`,
|
|
2312
|
+
`**Modules**: ${result.modules.length} (${result.modules.join(", ")})`,
|
|
2313
|
+
"",
|
|
2314
|
+
"## ER Diagrams",
|
|
2315
|
+
""
|
|
2316
|
+
];
|
|
2317
|
+
for (const [mod, er] of result.erDiagrams) {
|
|
2318
|
+
lines.push(`### ${mod}`);
|
|
2319
|
+
lines.push(`- Tables: ${er.tables.length}`);
|
|
2320
|
+
lines.push(`- Relations: ${er.relations.length}`);
|
|
2321
|
+
lines.push("");
|
|
2322
|
+
}
|
|
2323
|
+
lines.push("## Chain Plans", "");
|
|
2324
|
+
for (const [mod, plan] of result.chainPlans) {
|
|
2325
|
+
lines.push(`### ${mod}`);
|
|
2326
|
+
lines.push(`- Chains: ${plan.chains.length}`);
|
|
2327
|
+
lines.push(`- Total Steps: ${plan.totalSteps}`);
|
|
2328
|
+
lines.push("");
|
|
2329
|
+
}
|
|
2330
|
+
lines.push(`## Generated Files (${result.generatedFiles.length})`, "");
|
|
2331
|
+
for (const f of result.generatedFiles) {
|
|
2332
|
+
lines.push(`- \`${f.filePath}\` (${f.module} / ${f.chain})`);
|
|
2333
|
+
}
|
|
2334
|
+
if (result.validationErrors.length > 0) {
|
|
2335
|
+
lines.push("", "## Validation Issues", "");
|
|
2336
|
+
const errors = result.validationErrors.filter((e) => e.severity === "error");
|
|
2337
|
+
const warnings = result.validationErrors.filter((e) => e.severity === "warning");
|
|
2338
|
+
if (errors.length > 0) {
|
|
2339
|
+
lines.push(`### Errors (${errors.length})`, "");
|
|
2340
|
+
for (const e of errors) {
|
|
2341
|
+
lines.push(`- **[${e.module}]** ${e.field}: ${e.message}`);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
if (warnings.length > 0) {
|
|
2345
|
+
lines.push(`### Warnings (${warnings.length})`, "");
|
|
2346
|
+
for (const w of warnings) {
|
|
2347
|
+
lines.push(`- **[${w.module}]** ${w.field}: ${w.message}`);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
lines.push("", "---", "*Generated by [OpenCroc](https://github.com/opencroc/opencroc)*");
|
|
2352
|
+
return {
|
|
2353
|
+
format: "markdown",
|
|
2354
|
+
content: lines.join("\n"),
|
|
2355
|
+
filename: "opencroc-report.md"
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
function escapeHtml(s) {
|
|
2359
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2360
|
+
}
|
|
2361
|
+
function erSummaryRows(erDiagrams) {
|
|
2362
|
+
const rows = [];
|
|
2363
|
+
for (const [mod, er] of erDiagrams) {
|
|
2364
|
+
rows.push(`<tr><td>${escapeHtml(mod)}</td><td>${er.tables.length}</td><td>${er.relations.length}</td></tr>`);
|
|
2365
|
+
}
|
|
2366
|
+
return rows.join("\n");
|
|
2367
|
+
}
|
|
2368
|
+
function chainSummaryRows(chainPlans) {
|
|
2369
|
+
const rows = [];
|
|
2370
|
+
for (const [mod, plan] of chainPlans) {
|
|
2371
|
+
rows.push(`<tr><td>${escapeHtml(mod)}</td><td>${plan.chains.length}</td><td>${plan.totalSteps}</td></tr>`);
|
|
2372
|
+
}
|
|
2373
|
+
return rows.join("\n");
|
|
2374
|
+
}
|
|
2375
|
+
function fileListRows(files) {
|
|
2376
|
+
return files.map((f) => `<tr><td><code>${escapeHtml(f.filePath)}</code></td><td>${escapeHtml(f.module)}</td><td>${escapeHtml(f.chain)}</td></tr>`).join("\n");
|
|
2377
|
+
}
|
|
2378
|
+
function validationRows(errors) {
|
|
2379
|
+
return errors.map(
|
|
2380
|
+
(e) => `<tr class="${e.severity}"><td><span class="badge ${e.severity}">${e.severity}</span></td><td>${escapeHtml(e.module)}</td><td>${escapeHtml(e.field)}</td><td>${escapeHtml(e.message)}</td></tr>`
|
|
2381
|
+
).join("\n");
|
|
2382
|
+
}
|
|
2383
|
+
function generateHtmlReport(result) {
|
|
2384
|
+
const totalTables = Array.from(result.erDiagrams.values()).reduce((s, e) => s + e.tables.length, 0);
|
|
2385
|
+
const totalRelations = Array.from(result.erDiagrams.values()).reduce((s, e) => s + e.relations.length, 0);
|
|
2386
|
+
const totalChains = Array.from(result.chainPlans.values()).reduce((s, p) => s + p.chains.length, 0);
|
|
2387
|
+
const totalSteps = Array.from(result.chainPlans.values()).reduce((s, p) => s + p.totalSteps, 0);
|
|
2388
|
+
const errorCount = result.validationErrors.filter((e) => e.severity === "error").length;
|
|
2389
|
+
const warnCount = result.validationErrors.filter((e) => e.severity === "warning").length;
|
|
2390
|
+
const html = `<!DOCTYPE html>
|
|
2391
|
+
<html lang="en">
|
|
2392
|
+
<head>
|
|
2393
|
+
<meta charset="utf-8" />
|
|
2394
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2395
|
+
<title>OpenCroc Report</title>
|
|
2396
|
+
<style>
|
|
2397
|
+
:root { --bg: #0d1117; --fg: #c9d1d9; --card: #161b22; --border: #30363d; --accent: #58a6ff; --green: #3fb950; --yellow: #d29922; --red: #f85149; }
|
|
2398
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2399
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--fg); padding: 2rem; }
|
|
2400
|
+
h1 { color: var(--accent); margin-bottom: 0.25rem; }
|
|
2401
|
+
.subtitle { color: #8b949e; margin-bottom: 2rem; }
|
|
2402
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
2403
|
+
.card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; }
|
|
2404
|
+
.card .label { font-size: 0.85rem; color: #8b949e; }
|
|
2405
|
+
.card .value { font-size: 2rem; font-weight: 700; color: var(--accent); }
|
|
2406
|
+
.card .value.green { color: var(--green); }
|
|
2407
|
+
.card .value.yellow { color: var(--yellow); }
|
|
2408
|
+
.card .value.red { color: var(--red); }
|
|
2409
|
+
section { margin-bottom: 2rem; }
|
|
2410
|
+
h2 { color: var(--fg); border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; margin-bottom: 1rem; }
|
|
2411
|
+
table { width: 100%; border-collapse: collapse; background: var(--card); border-radius: 8px; overflow: hidden; }
|
|
2412
|
+
th, td { text-align: left; padding: 0.6rem 1rem; border-bottom: 1px solid var(--border); }
|
|
2413
|
+
th { background: #21262d; color: #8b949e; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; }
|
|
2414
|
+
tr:last-child td { border-bottom: none; }
|
|
2415
|
+
code { background: #21262d; padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.85rem; }
|
|
2416
|
+
.badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
|
|
2417
|
+
.badge.error { background: rgba(248,81,73,0.15); color: var(--red); }
|
|
2418
|
+
.badge.warning { background: rgba(210,153,34,0.15); color: var(--yellow); }
|
|
2419
|
+
footer { margin-top: 3rem; text-align: center; color: #484f58; font-size: 0.85rem; }
|
|
2420
|
+
footer a { color: var(--accent); text-decoration: none; }
|
|
2421
|
+
</style>
|
|
2422
|
+
</head>
|
|
2423
|
+
<body>
|
|
2424
|
+
<h1>OpenCroc Report</h1>
|
|
2425
|
+
<p class="subtitle">Generated in ${result.duration}ms · ${result.modules.length} module(s)</p>
|
|
2426
|
+
|
|
2427
|
+
<div class="grid">
|
|
2428
|
+
<div class="card"><div class="label">Modules</div><div class="value">${result.modules.length}</div></div>
|
|
2429
|
+
<div class="card"><div class="label">Tables</div><div class="value">${totalTables}</div></div>
|
|
2430
|
+
<div class="card"><div class="label">Relations</div><div class="value">${totalRelations}</div></div>
|
|
2431
|
+
<div class="card"><div class="label">Chains</div><div class="value">${totalChains}</div></div>
|
|
2432
|
+
<div class="card"><div class="label">Steps</div><div class="value">${totalSteps}</div></div>
|
|
2433
|
+
<div class="card"><div class="label">Files</div><div class="value green">${result.generatedFiles.length}</div></div>
|
|
2434
|
+
<div class="card"><div class="label">Errors</div><div class="value${errorCount > 0 ? " red" : ""}">${errorCount}</div></div>
|
|
2435
|
+
<div class="card"><div class="label">Warnings</div><div class="value${warnCount > 0 ? " yellow" : ""}">${warnCount}</div></div>
|
|
2436
|
+
</div>
|
|
2437
|
+
|
|
2438
|
+
<section>
|
|
2439
|
+
<h2>ER Diagrams</h2>
|
|
2440
|
+
<table>
|
|
2441
|
+
<thead><tr><th>Module</th><th>Tables</th><th>Relations</th></tr></thead>
|
|
2442
|
+
<tbody>${erSummaryRows(result.erDiagrams)}</tbody>
|
|
2443
|
+
</table>
|
|
2444
|
+
</section>
|
|
2445
|
+
|
|
2446
|
+
<section>
|
|
2447
|
+
<h2>Chain Plans</h2>
|
|
2448
|
+
<table>
|
|
2449
|
+
<thead><tr><th>Module</th><th>Chains</th><th>Steps</th></tr></thead>
|
|
2450
|
+
<tbody>${chainSummaryRows(result.chainPlans)}</tbody>
|
|
2451
|
+
</table>
|
|
2452
|
+
</section>
|
|
2453
|
+
|
|
2454
|
+
<section>
|
|
2455
|
+
<h2>Generated Files (${result.generatedFiles.length})</h2>
|
|
2456
|
+
<table>
|
|
2457
|
+
<thead><tr><th>File</th><th>Module</th><th>Chain</th></tr></thead>
|
|
2458
|
+
<tbody>${fileListRows(result.generatedFiles)}</tbody>
|
|
2459
|
+
</table>
|
|
2460
|
+
</section>
|
|
2461
|
+
|
|
2462
|
+
${result.validationErrors.length > 0 ? `<section>
|
|
2463
|
+
<h2>Validation Issues (${result.validationErrors.length})</h2>
|
|
2464
|
+
<table>
|
|
2465
|
+
<thead><tr><th>Severity</th><th>Module</th><th>Field</th><th>Message</th></tr></thead>
|
|
2466
|
+
<tbody>${validationRows(result.validationErrors)}</tbody>
|
|
2467
|
+
</table>
|
|
2468
|
+
</section>` : ""}
|
|
2469
|
+
|
|
2470
|
+
<footer>
|
|
2471
|
+
Generated by <a href="https://github.com/opencroc/opencroc">OpenCroc</a>
|
|
2472
|
+
</footer>
|
|
2473
|
+
</body>
|
|
2474
|
+
</html>`;
|
|
2475
|
+
return {
|
|
2476
|
+
format: "html",
|
|
2477
|
+
content: html,
|
|
2478
|
+
filename: "opencroc-report.html"
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
var REPORTERS = {
|
|
2482
|
+
html: generateHtmlReport,
|
|
2483
|
+
json: generateJsonReport,
|
|
2484
|
+
markdown: generateMarkdownReport
|
|
2485
|
+
};
|
|
2486
|
+
function generateReports(result, formats = ["html"]) {
|
|
2487
|
+
return formats.map((fmt) => {
|
|
2488
|
+
const gen = REPORTERS[fmt];
|
|
2489
|
+
if (!gen) throw new Error(`Unknown report format: "${fmt}". Available: ${Object.keys(REPORTERS).join(", ")}`);
|
|
2490
|
+
return gen(result);
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// src/vscode/index.ts
|
|
2495
|
+
init_esm_shims();
|
|
2496
|
+
var COMMANDS = [
|
|
2497
|
+
{ command: "opencroc.init", title: "Initialize Project", category: "OpenCroc" },
|
|
2498
|
+
{ command: "opencroc.generate", title: "Generate Tests", category: "OpenCroc" },
|
|
2499
|
+
{ command: "opencroc.generateModule", title: "Generate Tests for Module...", category: "OpenCroc" },
|
|
2500
|
+
{ command: "opencroc.test", title: "Run Tests", category: "OpenCroc" },
|
|
2501
|
+
{ command: "opencroc.testModule", title: "Run Tests for Module...", category: "OpenCroc" },
|
|
2502
|
+
{ command: "opencroc.validate", title: "Validate Configuration", category: "OpenCroc" },
|
|
2503
|
+
{ command: "opencroc.heal", title: "Self-Heal Failures", category: "OpenCroc" },
|
|
2504
|
+
{ command: "opencroc.openReport", title: "Open Report", category: "OpenCroc" },
|
|
2505
|
+
{ command: "opencroc.ci", title: "Generate CI Template", category: "OpenCroc" }
|
|
2506
|
+
];
|
|
2507
|
+
function buildModuleTree(modules) {
|
|
2508
|
+
return modules.map((mod) => ({
|
|
2509
|
+
label: mod,
|
|
2510
|
+
description: "module",
|
|
2511
|
+
iconId: "symbol-module",
|
|
2512
|
+
children: [
|
|
2513
|
+
{ label: "Generate Tests", command: "opencroc.generateModule", iconId: "play" },
|
|
2514
|
+
{ label: "Run Tests", command: "opencroc.testModule", iconId: "testing-run-icon" },
|
|
2515
|
+
{ label: "View ER Diagram", command: "opencroc.openReport", iconId: "graph" }
|
|
2516
|
+
]
|
|
2517
|
+
}));
|
|
2518
|
+
}
|
|
2519
|
+
function buildStatusTree(stats) {
|
|
2520
|
+
return [
|
|
2521
|
+
{ label: `Modules: ${stats.modules}`, iconId: "symbol-module" },
|
|
2522
|
+
{ label: `Tables: ${stats.tables}`, iconId: "database" },
|
|
2523
|
+
{ label: `Relations: ${stats.relations}`, iconId: "git-merge" },
|
|
2524
|
+
{ label: `Generated: ${stats.generatedFiles} files`, iconId: "file-code" },
|
|
2525
|
+
{
|
|
2526
|
+
label: stats.errors > 0 ? `Errors: ${stats.errors}` : "No errors",
|
|
2527
|
+
iconId: stats.errors > 0 ? "error" : "pass"
|
|
2528
|
+
}
|
|
2529
|
+
];
|
|
2530
|
+
}
|
|
2531
|
+
function generateExtensionManifest() {
|
|
2532
|
+
return {
|
|
2533
|
+
name: "opencroc",
|
|
2534
|
+
displayName: "OpenCroc",
|
|
2535
|
+
description: "AI-native E2E testing \u2014 generate, run, and self-heal tests from VS Code",
|
|
2536
|
+
version: "0.1.0",
|
|
2537
|
+
publisher: "opencroc",
|
|
2538
|
+
license: "MIT",
|
|
2539
|
+
repository: { type: "git", url: "https://github.com/opencroc/opencroc" },
|
|
2540
|
+
engines: { vscode: "^1.85.0" },
|
|
2541
|
+
categories: ["Testing"],
|
|
2542
|
+
keywords: ["e2e", "testing", "playwright", "ai", "self-healing"],
|
|
2543
|
+
activationEvents: ["workspaceContains:opencroc.config.ts", "workspaceContains:opencroc.config.js"],
|
|
2544
|
+
main: "./out/extension.js",
|
|
2545
|
+
contributes: {
|
|
2546
|
+
commands: COMMANDS.map((c) => ({
|
|
2547
|
+
command: c.command,
|
|
2548
|
+
title: c.title,
|
|
2549
|
+
category: c.category
|
|
2550
|
+
})),
|
|
2551
|
+
viewsContainers: {
|
|
2552
|
+
activitybar: [
|
|
2553
|
+
{
|
|
2554
|
+
id: "opencroc",
|
|
2555
|
+
title: "OpenCroc",
|
|
2556
|
+
icon: "resources/opencroc.svg"
|
|
2557
|
+
}
|
|
2558
|
+
]
|
|
2559
|
+
},
|
|
2560
|
+
views: {
|
|
2561
|
+
opencroc: [
|
|
2562
|
+
{ id: "opencroc.status", name: "Status" },
|
|
2563
|
+
{ id: "opencroc.modules", name: "Modules" }
|
|
2564
|
+
]
|
|
2565
|
+
},
|
|
2566
|
+
configuration: {
|
|
2567
|
+
title: "OpenCroc",
|
|
2568
|
+
properties: {
|
|
2569
|
+
"opencroc.autoGenerate": {
|
|
2570
|
+
type: "boolean",
|
|
2571
|
+
default: false,
|
|
2572
|
+
description: "Automatically regenerate tests on file save"
|
|
2573
|
+
},
|
|
2574
|
+
"opencroc.reportFormat": {
|
|
2575
|
+
type: "string",
|
|
2576
|
+
default: "html",
|
|
2577
|
+
enum: ["html", "json", "markdown"],
|
|
2578
|
+
description: "Default report format"
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
function generateExtensionEntrypoint() {
|
|
2586
|
+
return `import * as vscode from 'vscode';
|
|
2587
|
+
import { exec } from 'child_process';
|
|
2588
|
+
import { promisify } from 'util';
|
|
2589
|
+
|
|
2590
|
+
const run = promisify(exec);
|
|
2591
|
+
|
|
2592
|
+
export function activate(context: vscode.ExtensionContext) {
|
|
2593
|
+
const outputChannel = vscode.window.createOutputChannel('OpenCroc');
|
|
2594
|
+
|
|
2595
|
+
async function runCommand(cmd: string) {
|
|
2596
|
+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
|
2597
|
+
if (!workspaceFolder) {
|
|
2598
|
+
vscode.window.showErrorMessage('No workspace folder open');
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
outputChannel.show();
|
|
2602
|
+
outputChannel.appendLine(\`> \${cmd}\`);
|
|
2603
|
+
try {
|
|
2604
|
+
const { stdout, stderr } = await run(cmd, { cwd: workspaceFolder.uri.fsPath });
|
|
2605
|
+
if (stdout) outputChannel.appendLine(stdout);
|
|
2606
|
+
if (stderr) outputChannel.appendLine(stderr);
|
|
2607
|
+
vscode.window.showInformationMessage(\`OpenCroc: \${cmd} completed\`);
|
|
2608
|
+
} catch (err: unknown) {
|
|
2609
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2610
|
+
outputChannel.appendLine(\`Error: \${message}\`);
|
|
2611
|
+
vscode.window.showErrorMessage(\`OpenCroc: \${message}\`);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
context.subscriptions.push(
|
|
2616
|
+
vscode.commands.registerCommand('opencroc.init', () => runCommand('npx opencroc init --yes')),
|
|
2617
|
+
vscode.commands.registerCommand('opencroc.generate', () => runCommand('npx opencroc generate --all')),
|
|
2618
|
+
vscode.commands.registerCommand('opencroc.test', () => runCommand('npx opencroc test')),
|
|
2619
|
+
vscode.commands.registerCommand('opencroc.validate', () => runCommand('npx opencroc validate')),
|
|
2620
|
+
vscode.commands.registerCommand('opencroc.heal', () => runCommand('npx opencroc heal')),
|
|
2621
|
+
vscode.commands.registerCommand('opencroc.ci', async () => {
|
|
2622
|
+
const platform = await vscode.window.showQuickPick(['github', 'gitlab'], {
|
|
2623
|
+
placeHolder: 'Select CI platform',
|
|
2624
|
+
});
|
|
2625
|
+
if (platform) {
|
|
2626
|
+
await runCommand(\`npx opencroc ci --platform=\${platform}\`);
|
|
2627
|
+
}
|
|
2628
|
+
}),
|
|
2629
|
+
vscode.commands.registerCommand('opencroc.generateModule', async () => {
|
|
2630
|
+
const mod = await vscode.window.showInputBox({ prompt: 'Module name' });
|
|
2631
|
+
if (mod) await runCommand(\`npx opencroc generate --module=\${mod}\`);
|
|
2632
|
+
}),
|
|
2633
|
+
vscode.commands.registerCommand('opencroc.testModule', async () => {
|
|
2634
|
+
const mod = await vscode.window.showInputBox({ prompt: 'Module name' });
|
|
2635
|
+
if (mod) await runCommand(\`npx opencroc test --module=\${mod}\`);
|
|
2636
|
+
}),
|
|
2637
|
+
);
|
|
2638
|
+
|
|
2639
|
+
outputChannel.appendLine('OpenCroc extension activated');
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
export function deactivate() {}
|
|
2643
|
+
`;
|
|
2644
|
+
}
|
|
109
2645
|
export {
|
|
2646
|
+
SYSTEM_PROMPTS,
|
|
2647
|
+
COMMANDS as VSCODE_COMMANDS,
|
|
2648
|
+
analyzeFailureWithLLM,
|
|
2649
|
+
buildClassToTableMap,
|
|
2650
|
+
buildGraph,
|
|
2651
|
+
buildModuleTree,
|
|
2652
|
+
buildStatusTree,
|
|
2653
|
+
categorizeFailure,
|
|
2654
|
+
classNameToTableName,
|
|
2655
|
+
createAdapter,
|
|
110
2656
|
createApiChainAnalyzer,
|
|
111
2657
|
createAssociationParser,
|
|
112
2658
|
createControllerParser,
|
|
113
2659
|
createERDiagramGenerator,
|
|
114
2660
|
createImpactReporter,
|
|
2661
|
+
createLlmProvider,
|
|
115
2662
|
createMockDataGenerator,
|
|
116
2663
|
createModelParser,
|
|
2664
|
+
createOllamaProvider,
|
|
2665
|
+
createOpenAIProvider,
|
|
117
2666
|
createPipeline,
|
|
2667
|
+
createPluginRegistry,
|
|
2668
|
+
createPrismaAdapter,
|
|
118
2669
|
createSelfHealingLoop,
|
|
2670
|
+
createSequelizeAdapter,
|
|
119
2671
|
createTestCodeGenerator,
|
|
2672
|
+
createTokenTracker,
|
|
2673
|
+
createTypeORMAdapter,
|
|
120
2674
|
defineConfig,
|
|
2675
|
+
definePlugin,
|
|
2676
|
+
detectAdapter,
|
|
2677
|
+
detectCycles,
|
|
2678
|
+
generateCiTemplate,
|
|
2679
|
+
generateExtensionEntrypoint,
|
|
2680
|
+
generateExtensionManifest,
|
|
2681
|
+
generateGitHubActionsTemplate,
|
|
2682
|
+
generateGitLabCITemplate,
|
|
2683
|
+
generateHtmlReport,
|
|
2684
|
+
generateJsonReport,
|
|
2685
|
+
generateMarkdownReport,
|
|
2686
|
+
generateReports,
|
|
2687
|
+
inferDependencies,
|
|
2688
|
+
inferRelatedTables,
|
|
2689
|
+
listCiPlatforms,
|
|
2690
|
+
parseAssociationFile,
|
|
2691
|
+
parseControllerDirectory,
|
|
2692
|
+
parseControllerFile,
|
|
2693
|
+
parseModelFile,
|
|
2694
|
+
parseModuleModels,
|
|
2695
|
+
resolveAdapter,
|
|
2696
|
+
topologicalSort,
|
|
121
2697
|
validateConfig
|
|
122
2698
|
};
|
|
123
2699
|
//# sourceMappingURL=index.js.map
|