codesight 1.1.1 → 1.3.0
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.md +114 -31
- package/dist/ast/extract-components.d.ts +12 -0
- package/dist/ast/extract-components.js +180 -0
- package/dist/ast/extract-routes.d.ts +13 -0
- package/dist/ast/extract-routes.js +271 -0
- package/dist/ast/extract-schema.d.ts +15 -0
- package/dist/ast/extract-schema.js +302 -0
- package/dist/ast/loader.d.ts +20 -0
- package/dist/ast/loader.js +105 -0
- package/dist/detectors/blast-radius.d.ts +11 -0
- package/dist/detectors/blast-radius.js +102 -0
- package/dist/detectors/components.js +11 -0
- package/dist/detectors/routes.js +91 -15
- package/dist/detectors/schema.js +20 -0
- package/dist/generators/ai-config.d.ts +5 -0
- package/dist/generators/ai-config.js +97 -0
- package/dist/index.js +60 -2
- package/dist/mcp-server.js +288 -30
- package/dist/types.d.ts +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { parseSourceFile, getDecorators, parseDecorator, getText } from "./loader.js";
|
|
2
|
+
const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
|
|
3
|
+
function extractPathParams(path) {
|
|
4
|
+
const params = [];
|
|
5
|
+
const regex = /[:{}](\w+)/g;
|
|
6
|
+
let m;
|
|
7
|
+
while ((m = regex.exec(path)) !== null)
|
|
8
|
+
params.push(m[1]);
|
|
9
|
+
return params;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Try AST-based route extraction for a single file.
|
|
13
|
+
* Returns routes with confidence: 'ast', or empty array if AST cannot handle this file.
|
|
14
|
+
*/
|
|
15
|
+
export function extractRoutesAST(ts, filePath, content, framework, tags) {
|
|
16
|
+
try {
|
|
17
|
+
const sf = parseSourceFile(ts, filePath, content);
|
|
18
|
+
switch (framework) {
|
|
19
|
+
case "express":
|
|
20
|
+
case "hono":
|
|
21
|
+
case "fastify":
|
|
22
|
+
case "koa":
|
|
23
|
+
case "elysia":
|
|
24
|
+
return extractHttpFrameworkRoutes(ts, sf, filePath, content, framework, tags);
|
|
25
|
+
case "nestjs":
|
|
26
|
+
return extractNestJSRoutes(ts, sf, filePath, content, tags);
|
|
27
|
+
case "trpc":
|
|
28
|
+
return extractTRPCRoutes(ts, sf, filePath, content, tags);
|
|
29
|
+
default:
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return []; // AST parsing failed — caller falls back to regex
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ─── Express / Hono / Fastify / Koa / Elysia ───
|
|
38
|
+
function extractHttpFrameworkRoutes(ts, sf, filePath, _content, framework, tags) {
|
|
39
|
+
const routes = [];
|
|
40
|
+
const SK = ts.SyntaxKind;
|
|
41
|
+
// Track router.use('/prefix', subRouter) for prefix resolution
|
|
42
|
+
const prefixMap = new Map(); // variable name -> prefix
|
|
43
|
+
function visit(node) {
|
|
44
|
+
if (node.kind === SK.CallExpression) {
|
|
45
|
+
const expr = node.expression;
|
|
46
|
+
if (expr?.kind === SK.PropertyAccessExpression) {
|
|
47
|
+
const methodName = getText(sf, expr.name).toLowerCase();
|
|
48
|
+
const receiverName = expr.expression?.kind === SK.Identifier
|
|
49
|
+
? getText(sf, expr.expression)
|
|
50
|
+
: "";
|
|
51
|
+
// Track .use('/prefix', variable) for prefix chains
|
|
52
|
+
if (methodName === "use" && node.arguments?.length >= 2) {
|
|
53
|
+
const first = node.arguments[0];
|
|
54
|
+
const second = node.arguments[1];
|
|
55
|
+
if ((first.kind === SK.StringLiteral || first.kind === SK.NoSubstitutionTemplateLiteral) &&
|
|
56
|
+
second.kind === SK.Identifier) {
|
|
57
|
+
const prefix = first.text;
|
|
58
|
+
const routerVar = getText(sf, second);
|
|
59
|
+
prefixMap.set(routerVar, prefix);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Route registration: .get('/path', ...) .post('/path', ...) etc.
|
|
63
|
+
if (HTTP_METHODS.has(methodName) && node.arguments?.length > 0) {
|
|
64
|
+
const pathArg = node.arguments[0];
|
|
65
|
+
let path = null;
|
|
66
|
+
if (pathArg.kind === SK.StringLiteral || pathArg.kind === SK.NoSubstitutionTemplateLiteral) {
|
|
67
|
+
path = pathArg.text;
|
|
68
|
+
}
|
|
69
|
+
if (path !== null) {
|
|
70
|
+
// Filter: route paths must start with / or : — skip context.get("key") calls
|
|
71
|
+
if (!path.startsWith("/") && !path.startsWith(":")) {
|
|
72
|
+
ts.forEachChild(node, visit);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Apply prefix if this receiver has one registered
|
|
76
|
+
const prefix = prefixMap.get(receiverName) || "";
|
|
77
|
+
const fullPath = prefix ? (prefix + path).replace(/\/\//g, "/") : path;
|
|
78
|
+
// Extract middleware names from intermediate arguments
|
|
79
|
+
const middleware = [];
|
|
80
|
+
for (let i = 1; i < node.arguments.length; i++) {
|
|
81
|
+
const arg = node.arguments[i];
|
|
82
|
+
if (arg.kind === SK.Identifier) {
|
|
83
|
+
middleware.push(getText(sf, arg));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
routes.push({
|
|
87
|
+
method: methodName.toUpperCase() === "ALL" ? "ALL" : methodName.toUpperCase(),
|
|
88
|
+
path: fullPath,
|
|
89
|
+
file: filePath,
|
|
90
|
+
tags,
|
|
91
|
+
framework,
|
|
92
|
+
params: extractPathParams(fullPath),
|
|
93
|
+
confidence: "ast",
|
|
94
|
+
middleware,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
ts.forEachChild(node, visit);
|
|
101
|
+
}
|
|
102
|
+
visit(sf);
|
|
103
|
+
return routes;
|
|
104
|
+
}
|
|
105
|
+
// ─── NestJS ───
|
|
106
|
+
const NEST_METHOD_MAP = {
|
|
107
|
+
Get: "GET",
|
|
108
|
+
Post: "POST",
|
|
109
|
+
Put: "PUT",
|
|
110
|
+
Patch: "PATCH",
|
|
111
|
+
Delete: "DELETE",
|
|
112
|
+
Options: "OPTIONS",
|
|
113
|
+
Head: "HEAD",
|
|
114
|
+
All: "ALL",
|
|
115
|
+
};
|
|
116
|
+
function extractNestJSRoutes(ts, sf, filePath, _content, tags) {
|
|
117
|
+
const routes = [];
|
|
118
|
+
const SK = ts.SyntaxKind;
|
|
119
|
+
function visitNode(node) {
|
|
120
|
+
if (node.kind === SK.ClassDeclaration) {
|
|
121
|
+
const decorators = getDecorators(ts, node);
|
|
122
|
+
// Find @Controller decorator and extract prefix
|
|
123
|
+
let controllerPrefix = "";
|
|
124
|
+
let isController = false;
|
|
125
|
+
for (const dec of decorators) {
|
|
126
|
+
const parsed = parseDecorator(ts, sf, dec);
|
|
127
|
+
if (parsed.name === "Controller") {
|
|
128
|
+
isController = true;
|
|
129
|
+
controllerPrefix = parsed.arg || "";
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!isController) {
|
|
134
|
+
ts.forEachChild(node, visitNode);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Extract guards at class level
|
|
138
|
+
const classGuards = [];
|
|
139
|
+
for (const dec of decorators) {
|
|
140
|
+
const parsed = parseDecorator(ts, sf, dec);
|
|
141
|
+
if (parsed.name === "UseGuards" && parsed.arg) {
|
|
142
|
+
classGuards.push(parsed.arg);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Visit methods
|
|
146
|
+
for (const member of node.members || []) {
|
|
147
|
+
if (member.kind !== SK.MethodDeclaration)
|
|
148
|
+
continue;
|
|
149
|
+
const methodDecorators = getDecorators(ts, member);
|
|
150
|
+
for (const dec of methodDecorators) {
|
|
151
|
+
const parsed = parseDecorator(ts, sf, dec);
|
|
152
|
+
if (!parsed.name || !NEST_METHOD_MAP[parsed.name])
|
|
153
|
+
continue;
|
|
154
|
+
const methodPath = parsed.arg || "";
|
|
155
|
+
const combined = [controllerPrefix, methodPath].filter(Boolean).join("/");
|
|
156
|
+
const fullPath = "/" + combined.replace(/^\/+/, "").replace(/\/+/g, "/");
|
|
157
|
+
const normalizedPath = fullPath.replace(/\/$/, "") || "/";
|
|
158
|
+
// Extract @Param, @Body, @Query from method parameters
|
|
159
|
+
const params = [];
|
|
160
|
+
const middleware = [...classGuards];
|
|
161
|
+
for (const param of member.parameters || []) {
|
|
162
|
+
const paramDecs = getDecorators(ts, param);
|
|
163
|
+
for (const pd of paramDecs) {
|
|
164
|
+
const pp = parseDecorator(ts, sf, pd);
|
|
165
|
+
if (pp.name === "Param" && pp.arg)
|
|
166
|
+
params.push(pp.arg);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Method-level guards
|
|
170
|
+
for (const mdec of methodDecorators) {
|
|
171
|
+
const mp = parseDecorator(ts, sf, mdec);
|
|
172
|
+
if (mp.name === "UseGuards" && mp.arg)
|
|
173
|
+
middleware.push(mp.arg);
|
|
174
|
+
}
|
|
175
|
+
routes.push({
|
|
176
|
+
method: NEST_METHOD_MAP[parsed.name],
|
|
177
|
+
path: normalizedPath,
|
|
178
|
+
file: filePath,
|
|
179
|
+
tags,
|
|
180
|
+
framework: "nestjs",
|
|
181
|
+
params: params.length > 0 ? params : extractPathParams(normalizedPath),
|
|
182
|
+
confidence: "ast",
|
|
183
|
+
middleware: middleware.length > 0 ? middleware : undefined,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
ts.forEachChild(node, visitNode);
|
|
189
|
+
}
|
|
190
|
+
visitNode(sf);
|
|
191
|
+
return routes;
|
|
192
|
+
}
|
|
193
|
+
// ─── tRPC ───
|
|
194
|
+
function extractTRPCRoutes(ts, sf, filePath, _content, tags) {
|
|
195
|
+
const routes = [];
|
|
196
|
+
const SK = ts.SyntaxKind;
|
|
197
|
+
function isRouterCall(node) {
|
|
198
|
+
if (node.kind !== SK.CallExpression)
|
|
199
|
+
return false;
|
|
200
|
+
const callee = node.expression;
|
|
201
|
+
if (callee.kind === SK.Identifier) {
|
|
202
|
+
const name = getText(sf, callee);
|
|
203
|
+
return name === "router" || name === "createTRPCRouter";
|
|
204
|
+
}
|
|
205
|
+
if (callee.kind === SK.PropertyAccessExpression) {
|
|
206
|
+
return getText(sf, callee.name) === "router";
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
function findProcedureMethod(node) {
|
|
211
|
+
if (!node || node.kind !== SK.CallExpression)
|
|
212
|
+
return null;
|
|
213
|
+
const expr = node.expression;
|
|
214
|
+
if (expr?.kind === SK.PropertyAccessExpression) {
|
|
215
|
+
const name = getText(sf, expr.name);
|
|
216
|
+
if (name === "query")
|
|
217
|
+
return "QUERY";
|
|
218
|
+
if (name === "mutation")
|
|
219
|
+
return "MUTATION";
|
|
220
|
+
if (name === "subscription")
|
|
221
|
+
return "SUBSCRIPTION";
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function extractFromRouter(node, prefix) {
|
|
226
|
+
if (!isRouterCall(node) || !node.arguments?.length)
|
|
227
|
+
return;
|
|
228
|
+
const arg = node.arguments[0];
|
|
229
|
+
if (arg.kind !== SK.ObjectLiteralExpression)
|
|
230
|
+
return;
|
|
231
|
+
for (const prop of arg.properties || []) {
|
|
232
|
+
if (prop.kind === SK.PropertyAssignment) {
|
|
233
|
+
const name = prop.name ? getText(sf, prop.name) : "";
|
|
234
|
+
if (!name)
|
|
235
|
+
continue;
|
|
236
|
+
const init = prop.initializer;
|
|
237
|
+
// Nested router
|
|
238
|
+
if (isRouterCall(init)) {
|
|
239
|
+
extractFromRouter(init, prefix ? `${prefix}.${name}` : name);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// Procedure: look for .query() / .mutation() / .subscription()
|
|
243
|
+
const method = findProcedureMethod(init);
|
|
244
|
+
if (method) {
|
|
245
|
+
routes.push({
|
|
246
|
+
method,
|
|
247
|
+
path: prefix ? `${prefix}.${name}` : name,
|
|
248
|
+
file: filePath,
|
|
249
|
+
tags,
|
|
250
|
+
framework: "trpc",
|
|
251
|
+
confidence: "ast",
|
|
252
|
+
});
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// Could be a reference to another router variable — can't resolve without types
|
|
256
|
+
}
|
|
257
|
+
if (prop.kind === SK.SpreadAssignment) {
|
|
258
|
+
// ...otherRoutes — can't resolve statically
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Find all router() calls in the file
|
|
263
|
+
function visit(node) {
|
|
264
|
+
if (isRouterCall(node)) {
|
|
265
|
+
extractFromRouter(node, "");
|
|
266
|
+
}
|
|
267
|
+
ts.forEachChild(node, visit);
|
|
268
|
+
}
|
|
269
|
+
visit(sf);
|
|
270
|
+
return routes;
|
|
271
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-based schema extraction for TypeScript/JavaScript ORMs.
|
|
3
|
+
* Provides higher accuracy than regex for:
|
|
4
|
+
* - Drizzle: pgTable/mysqlTable/sqliteTable with field types and chained modifiers
|
|
5
|
+
* - TypeORM: @Entity + @Column/@PrimaryGeneratedColumn decorators
|
|
6
|
+
*/
|
|
7
|
+
import type { SchemaModel } from "../types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Extract Drizzle schema from a file using AST.
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractDrizzleSchemaAST(ts: any, filePath: string, content: string): SchemaModel[];
|
|
12
|
+
/**
|
|
13
|
+
* Extract TypeORM entities from a file using AST.
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractTypeORMSchemaAST(ts: any, filePath: string, content: string): SchemaModel[];
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { parseSourceFile, getDecorators, parseDecorator, getText } from "./loader.js";
|
|
2
|
+
const AUDIT_FIELDS = new Set([
|
|
3
|
+
"createdAt", "updatedAt", "deletedAt",
|
|
4
|
+
"created_at", "updated_at", "deleted_at",
|
|
5
|
+
]);
|
|
6
|
+
/**
|
|
7
|
+
* Extract Drizzle schema from a file using AST.
|
|
8
|
+
*/
|
|
9
|
+
export function extractDrizzleSchemaAST(ts, filePath, content) {
|
|
10
|
+
try {
|
|
11
|
+
const sf = parseSourceFile(ts, filePath, content);
|
|
12
|
+
return extractDrizzleTables(ts, sf, content);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Extract TypeORM entities from a file using AST.
|
|
20
|
+
*/
|
|
21
|
+
export function extractTypeORMSchemaAST(ts, filePath, content) {
|
|
22
|
+
try {
|
|
23
|
+
const sf = parseSourceFile(ts, filePath, content);
|
|
24
|
+
return extractTypeORMEntities(ts, sf);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// ─── Drizzle ───
|
|
31
|
+
const DRIZZLE_TABLE_FUNCS = new Set(["pgTable", "mysqlTable", "sqliteTable"]);
|
|
32
|
+
function extractDrizzleTables(ts, sf, _content) {
|
|
33
|
+
const models = [];
|
|
34
|
+
const SK = ts.SyntaxKind;
|
|
35
|
+
function visit(node) {
|
|
36
|
+
// Look for: const xxx = pgTable("name", { ... })
|
|
37
|
+
if (node.kind === SK.CallExpression) {
|
|
38
|
+
const callee = node.expression;
|
|
39
|
+
const funcName = callee?.kind === SK.Identifier ? getText(sf, callee) : "";
|
|
40
|
+
if (DRIZZLE_TABLE_FUNCS.has(funcName) && node.arguments?.length >= 2) {
|
|
41
|
+
const nameArg = node.arguments[0];
|
|
42
|
+
const fieldsArg = node.arguments[1];
|
|
43
|
+
// Table name from first argument
|
|
44
|
+
let tableName = "";
|
|
45
|
+
if (nameArg.kind === SK.StringLiteral || nameArg.kind === SK.NoSubstitutionTemplateLiteral) {
|
|
46
|
+
tableName = nameArg.text;
|
|
47
|
+
}
|
|
48
|
+
if (!tableName) {
|
|
49
|
+
ts.forEachChild(node, visit);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Fields from second argument (ObjectLiteralExpression or arrow returning one)
|
|
53
|
+
let fieldsObj = null;
|
|
54
|
+
if (fieldsArg.kind === SK.ObjectLiteralExpression) {
|
|
55
|
+
fieldsObj = fieldsArg;
|
|
56
|
+
}
|
|
57
|
+
else if (fieldsArg.kind === SK.ArrowFunction || fieldsArg.kind === SK.FunctionExpression) {
|
|
58
|
+
// (t) => ({ ... }) — body might be ParenthesizedExpression containing ObjectLiteralExpression
|
|
59
|
+
const body = fieldsArg.body;
|
|
60
|
+
if (body?.kind === SK.ObjectLiteralExpression) {
|
|
61
|
+
fieldsObj = body;
|
|
62
|
+
}
|
|
63
|
+
else if (body?.kind === SK.ParenthesizedExpression && body.expression?.kind === SK.ObjectLiteralExpression) {
|
|
64
|
+
fieldsObj = body.expression;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!fieldsObj) {
|
|
68
|
+
ts.forEachChild(node, visit);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const fields = [];
|
|
72
|
+
const relations = [];
|
|
73
|
+
for (const prop of fieldsObj.properties || []) {
|
|
74
|
+
if (prop.kind !== SK.PropertyAssignment)
|
|
75
|
+
continue;
|
|
76
|
+
const fieldName = prop.name ? getText(sf, prop.name) : "";
|
|
77
|
+
if (!fieldName || AUDIT_FIELDS.has(fieldName))
|
|
78
|
+
continue;
|
|
79
|
+
// Parse the initializer chain: serial("id").primaryKey()
|
|
80
|
+
const { type, flags, refTarget } = parseFieldChain(ts, sf, prop.initializer);
|
|
81
|
+
if (refTarget) {
|
|
82
|
+
relations.push(`${fieldName} -> ${refTarget}`);
|
|
83
|
+
}
|
|
84
|
+
if (fieldName.endsWith("Id") || fieldName.endsWith("_id")) {
|
|
85
|
+
if (!flags.includes("fk"))
|
|
86
|
+
flags.push("fk");
|
|
87
|
+
}
|
|
88
|
+
fields.push({ name: fieldName, type, flags });
|
|
89
|
+
}
|
|
90
|
+
if (fields.length > 0) {
|
|
91
|
+
models.push({ name: tableName, fields, relations, orm: "drizzle", confidence: "ast" });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
ts.forEachChild(node, visit);
|
|
96
|
+
}
|
|
97
|
+
visit(sf);
|
|
98
|
+
// Also extract Drizzle relations() calls
|
|
99
|
+
extractDrizzleRelations(ts, sf, models);
|
|
100
|
+
return models;
|
|
101
|
+
}
|
|
102
|
+
function parseFieldChain(ts, sf, node) {
|
|
103
|
+
const SK = ts.SyntaxKind;
|
|
104
|
+
const flags = [];
|
|
105
|
+
let type = "unknown";
|
|
106
|
+
let refTarget = null;
|
|
107
|
+
// Walk the chain from outermost to innermost call
|
|
108
|
+
// e.g., serial("id").primaryKey().notNull() is:
|
|
109
|
+
// CallExpression(.notNull)
|
|
110
|
+
// expression: PropertyAccessExpression
|
|
111
|
+
// expression: CallExpression(.primaryKey)
|
|
112
|
+
// expression: PropertyAccessExpression
|
|
113
|
+
// expression: CallExpression(serial)
|
|
114
|
+
function walkChain(n) {
|
|
115
|
+
if (!n)
|
|
116
|
+
return;
|
|
117
|
+
if (n.kind === SK.CallExpression) {
|
|
118
|
+
const expr = n.expression;
|
|
119
|
+
if (expr?.kind === SK.PropertyAccessExpression) {
|
|
120
|
+
const methodName = getText(sf, expr.name);
|
|
121
|
+
switch (methodName) {
|
|
122
|
+
case "primaryKey":
|
|
123
|
+
flags.push("pk");
|
|
124
|
+
break;
|
|
125
|
+
case "notNull":
|
|
126
|
+
flags.push("required");
|
|
127
|
+
break;
|
|
128
|
+
case "unique":
|
|
129
|
+
flags.push("unique");
|
|
130
|
+
break;
|
|
131
|
+
case "default":
|
|
132
|
+
case "defaultNow":
|
|
133
|
+
case "$default":
|
|
134
|
+
case "$defaultFn":
|
|
135
|
+
flags.push("default");
|
|
136
|
+
break;
|
|
137
|
+
case "references":
|
|
138
|
+
flags.push("fk");
|
|
139
|
+
// Try to extract reference target: .references(() => users.id)
|
|
140
|
+
if (n.arguments?.length > 0) {
|
|
141
|
+
const refArg = n.arguments[0];
|
|
142
|
+
if (refArg.kind === SK.ArrowFunction || refArg.kind === SK.FunctionExpression) {
|
|
143
|
+
const refBody = refArg.body;
|
|
144
|
+
if (refBody?.kind === SK.PropertyAccessExpression) {
|
|
145
|
+
refTarget = getText(sf, refBody);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
// Recurse into the receiver
|
|
152
|
+
walkChain(expr.expression);
|
|
153
|
+
}
|
|
154
|
+
else if (expr?.kind === SK.Identifier) {
|
|
155
|
+
// Base function call: serial("id"), text("name"), etc.
|
|
156
|
+
type = getText(sf, expr);
|
|
157
|
+
}
|
|
158
|
+
else if (expr?.kind === SK.PropertyAccessExpression) {
|
|
159
|
+
// Could be t.serial("id") — method on a prefix
|
|
160
|
+
type = getText(sf, expr.name);
|
|
161
|
+
// Walk further for nested chains
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
walkChain(node);
|
|
166
|
+
return { type, flags, refTarget };
|
|
167
|
+
}
|
|
168
|
+
function extractDrizzleRelations(ts, sf, models) {
|
|
169
|
+
const SK = ts.SyntaxKind;
|
|
170
|
+
function visit(node) {
|
|
171
|
+
// relations(tableVar, ({ one, many }) => ({ ... }))
|
|
172
|
+
if (node.kind === SK.CallExpression) {
|
|
173
|
+
const callee = node.expression;
|
|
174
|
+
const funcName = callee?.kind === SK.Identifier ? getText(sf, callee) : "";
|
|
175
|
+
if (funcName === "relations" && node.arguments?.length >= 2) {
|
|
176
|
+
const tableArg = node.arguments[0];
|
|
177
|
+
const tableName = tableArg?.kind === SK.Identifier ? getText(sf, tableArg) : "";
|
|
178
|
+
// Find matching model by variable name (approximate match)
|
|
179
|
+
const model = models.find((m) => m.name === tableName ||
|
|
180
|
+
m.name === tableName.replace(/s$/, "") ||
|
|
181
|
+
tableName.startsWith(m.name));
|
|
182
|
+
if (!model) {
|
|
183
|
+
ts.forEachChild(node, visit);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const relArg = node.arguments[1];
|
|
187
|
+
// Arrow function body should be an ObjectLiteralExpression
|
|
188
|
+
let relObj = null;
|
|
189
|
+
if (relArg?.kind === SK.ArrowFunction) {
|
|
190
|
+
const body = relArg.body;
|
|
191
|
+
if (body?.kind === SK.ObjectLiteralExpression) {
|
|
192
|
+
relObj = body;
|
|
193
|
+
}
|
|
194
|
+
else if (body?.kind === SK.ParenthesizedExpression && body.expression?.kind === SK.ObjectLiteralExpression) {
|
|
195
|
+
relObj = body.expression;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!relObj) {
|
|
199
|
+
ts.forEachChild(node, visit);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
for (const prop of relObj.properties || []) {
|
|
203
|
+
if (prop.kind !== SK.PropertyAssignment)
|
|
204
|
+
continue;
|
|
205
|
+
const relName = prop.name ? getText(sf, prop.name) : "";
|
|
206
|
+
if (!relName)
|
|
207
|
+
continue;
|
|
208
|
+
const init = prop.initializer;
|
|
209
|
+
if (init?.kind === SK.CallExpression && init.expression?.kind === SK.Identifier) {
|
|
210
|
+
const relType = getText(sf, init.expression); // "one" or "many"
|
|
211
|
+
const targetArg = init.arguments?.[0];
|
|
212
|
+
const target = targetArg?.kind === SK.Identifier ? getText(sf, targetArg) : "?";
|
|
213
|
+
const existing = model.relations.find((r) => r.startsWith(`${relName}:`));
|
|
214
|
+
if (!existing) {
|
|
215
|
+
model.relations.push(`${relName}: ${relType}(${target})`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
ts.forEachChild(node, visit);
|
|
222
|
+
}
|
|
223
|
+
visit(sf);
|
|
224
|
+
}
|
|
225
|
+
// ─── TypeORM ───
|
|
226
|
+
function extractTypeORMEntities(ts, sf) {
|
|
227
|
+
const models = [];
|
|
228
|
+
const SK = ts.SyntaxKind;
|
|
229
|
+
function visitNode(node) {
|
|
230
|
+
if (node.kind === SK.ClassDeclaration) {
|
|
231
|
+
const decorators = getDecorators(ts, node);
|
|
232
|
+
let isEntity = false;
|
|
233
|
+
let entityName = "";
|
|
234
|
+
for (const dec of decorators) {
|
|
235
|
+
const parsed = parseDecorator(ts, sf, dec);
|
|
236
|
+
if (parsed.name === "Entity") {
|
|
237
|
+
isEntity = true;
|
|
238
|
+
entityName = parsed.arg || "";
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!isEntity) {
|
|
243
|
+
ts.forEachChild(node, visitNode);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// Class name as fallback
|
|
247
|
+
const className = node.name ? getText(sf, node.name) : "Unknown";
|
|
248
|
+
const name = entityName || className;
|
|
249
|
+
const fields = [];
|
|
250
|
+
const relations = [];
|
|
251
|
+
for (const member of node.members || []) {
|
|
252
|
+
if (member.kind !== SK.PropertyDeclaration)
|
|
253
|
+
continue;
|
|
254
|
+
const memberDecs = getDecorators(ts, member);
|
|
255
|
+
const memberName = member.name ? getText(sf, member.name) : "";
|
|
256
|
+
if (!memberName || AUDIT_FIELDS.has(memberName))
|
|
257
|
+
continue;
|
|
258
|
+
// Get type annotation
|
|
259
|
+
const memberType = member.type ? getText(sf, member.type) : "unknown";
|
|
260
|
+
for (const dec of memberDecs) {
|
|
261
|
+
const parsed = parseDecorator(ts, sf, dec);
|
|
262
|
+
// Column decorators
|
|
263
|
+
if (parsed.name === "PrimaryGeneratedColumn" || parsed.name === "PrimaryColumn") {
|
|
264
|
+
const flags = ["pk"];
|
|
265
|
+
if (parsed.name === "PrimaryGeneratedColumn")
|
|
266
|
+
flags.push("default");
|
|
267
|
+
fields.push({ name: memberName, type: parsed.arg || memberType, flags });
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
if (parsed.name === "Column" || parsed.name === "CreateDateColumn" || parsed.name === "UpdateDateColumn") {
|
|
271
|
+
const flags = [];
|
|
272
|
+
// Parse column options from decorator argument
|
|
273
|
+
const decExpr = dec.expression;
|
|
274
|
+
if (decExpr?.kind === SK.CallExpression && decExpr.arguments?.length > 0) {
|
|
275
|
+
const optArg = decExpr.arguments[0];
|
|
276
|
+
const optText = getText(sf, optArg);
|
|
277
|
+
if (optText.includes("unique: true") || optText.includes("unique:true"))
|
|
278
|
+
flags.push("unique");
|
|
279
|
+
if (optText.includes("nullable: true") || optText.includes("nullable:true"))
|
|
280
|
+
flags.push("nullable");
|
|
281
|
+
if (optText.includes("default:"))
|
|
282
|
+
flags.push("default");
|
|
283
|
+
}
|
|
284
|
+
fields.push({ name: memberName, type: parsed.arg || memberType, flags });
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
// Relation decorators
|
|
288
|
+
if (["OneToMany", "ManyToOne", "OneToOne", "ManyToMany"].includes(parsed.name)) {
|
|
289
|
+
relations.push(`${memberName}: ${parsed.name}(${memberType})`);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (fields.length > 0) {
|
|
295
|
+
models.push({ name, fields, relations, orm: "typeorm", confidence: "ast" });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
ts.forEachChild(node, visitNode);
|
|
299
|
+
}
|
|
300
|
+
visitNode(sf);
|
|
301
|
+
return models;
|
|
302
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare function loadTypeScript(projectRoot: string): any | null;
|
|
2
|
+
export declare function resetCache(): void;
|
|
3
|
+
export declare function parseSourceFile(ts: any, fileName: string, content: string): any;
|
|
4
|
+
/**
|
|
5
|
+
* Get decorators from a node, handling both TS 4.x (node.decorators)
|
|
6
|
+
* and TS 5.x (node.modifiers with SyntaxKind.Decorator).
|
|
7
|
+
*/
|
|
8
|
+
export declare function getDecorators(ts: any, node: any): any[];
|
|
9
|
+
/**
|
|
10
|
+
* Extract the name and first string argument from a decorator.
|
|
11
|
+
* @returns { name: string, arg: string | null }
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseDecorator(ts: any, sf: any, decorator: any): {
|
|
14
|
+
name: string;
|
|
15
|
+
arg: string | null;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Get text from a node safely.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getText(sf: any, node: any): string;
|