@schemyx/mcp 0.1.1 → 0.1.2

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.
@@ -0,0 +1,1252 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractDatabaseModels = extractDatabaseModels;
4
+ exports.extractDatabaseEnums = extractDatabaseEnums;
5
+ exports.extractDatabaseMigrations = extractDatabaseMigrations;
6
+ exports.extractDatabaseAccesses = extractDatabaseAccesses;
7
+ exports.detectDatabaseProvider = detectDatabaseProvider;
8
+ exports.databaseExtractorForFile = databaseExtractorForFile;
9
+ exports.databaseOperationToAccessOperation = databaseOperationToAccessOperation;
10
+ const extractors_1 = require("./extractors");
11
+ const utils_1 = require("./utils");
12
+ const prismaScalarTypes = new Set([
13
+ 'String',
14
+ 'Boolean',
15
+ 'Int',
16
+ 'BigInt',
17
+ 'Float',
18
+ 'Decimal',
19
+ 'DateTime',
20
+ 'Json',
21
+ 'Bytes',
22
+ ]);
23
+ const readOperations = new Set([
24
+ 'findUnique',
25
+ 'findUniqueOrThrow',
26
+ 'findFirst',
27
+ 'findFirstOrThrow',
28
+ 'findMany',
29
+ 'count',
30
+ 'aggregate',
31
+ 'groupBy',
32
+ ]);
33
+ const createOperations = new Set(['create', 'createMany']);
34
+ const updateOperations = new Set(['update', 'updateMany']);
35
+ const deleteOperations = new Set(['delete', 'deleteMany']);
36
+ const upsertOperations = new Set(['upsert']);
37
+ function extractDatabaseModels(relPath, content) {
38
+ return (0, utils_1.uniqueBy)([
39
+ ...extractPrismaDatabaseModels(relPath, content),
40
+ ...extractSqlDatabaseModels(relPath, content),
41
+ ...extractDrizzleDatabaseModels(relPath, content),
42
+ ...extractTypeOrmDatabaseModels(relPath, content),
43
+ ...extractSequelizeDatabaseModels(relPath, content),
44
+ ...extractMongooseDatabaseModels(relPath, content),
45
+ ...extractDjangoDatabaseModels(relPath, content),
46
+ ...extractRailsMigrationModels(relPath, content),
47
+ ...extractLaravelMigrationModels(relPath, content),
48
+ ], (model) => `${model.provider}:${model.name}:${model.tableName ?? ''}:${model.line}`).slice(0, 160);
49
+ }
50
+ function extractDatabaseEnums(relPath, content) {
51
+ return (0, utils_1.uniqueBy)([...extractPrismaDatabaseEnums(relPath, content), ...extractSqlDatabaseEnums(relPath, content)], (item) => `${item.provider}:${item.name}:${item.values.join('|')}`).slice(0, 120);
52
+ }
53
+ function extractDatabaseMigrations(relPath, content) {
54
+ const provider = detectDatabaseProvider(relPath, content);
55
+ const isMigration = provider === 'sql' ||
56
+ /(?:^|\/)(?:migrations?|db\/migrate|database\/migrations)(?:\/|$)/i.test(relPath) ||
57
+ /Schema::(?:create|table|drop)|create_table\s+:|add_column\s+:|ALTER\s+TABLE|CREATE\s+TABLE/i.test(content);
58
+ if (!isMigration) {
59
+ return [];
60
+ }
61
+ const operations = (0, utils_1.uniqueBy)([
62
+ ...extractSqlMigrationOperations(content),
63
+ ...extractRailsMigrationOperations(content),
64
+ ...extractLaravelMigrationOperations(content),
65
+ ], (operation) => `${operation.kind}:${operation.target ?? ''}:${operation.name ?? ''}:${operation.line}`).slice(0, 240);
66
+ if (!operations.length) {
67
+ return [];
68
+ }
69
+ return [
70
+ {
71
+ name: migrationNameFromPath(relPath),
72
+ path: relPath,
73
+ provider,
74
+ operations,
75
+ creates: (0, utils_1.unique)(operations
76
+ .filter((operation) => operation.kind === 'create-table')
77
+ .map((operation) => operation.target ?? '')),
78
+ alters: (0, utils_1.unique)(operations
79
+ .filter((operation) => operation.kind === 'alter-table')
80
+ .map((operation) => operation.target ?? '')),
81
+ drops: (0, utils_1.unique)(operations
82
+ .filter((operation) => operation.kind === 'drop-table')
83
+ .map((operation) => operation.target ?? '')),
84
+ indexes: (0, utils_1.unique)(operations
85
+ .filter((operation) => operation.kind === 'create-index' || operation.kind === 'drop-index')
86
+ .map((operation) => operation.name ?? operation.target ?? '')),
87
+ enums: (0, utils_1.unique)(operations
88
+ .filter((operation) => operation.kind === 'create-enum' || operation.kind === 'drop-enum')
89
+ .map((operation) => operation.target ?? '')),
90
+ seedLike: operations.some((operation) => ['insert-seed', 'update-data', 'delete-data'].includes(operation.kind)),
91
+ destructive: operations.some((operation) => operation.destructive),
92
+ confidence: provider === 'sql' ? 'high' : 'medium',
93
+ },
94
+ ];
95
+ }
96
+ function extractDatabaseAccesses(relPath, content) {
97
+ if (/\.(md|mdx|css|scss|html|htm|json|yaml|yml|toml|xml|prisma)$/i.test(relPath) ||
98
+ isMigrationLikePath(relPath)) {
99
+ return [];
100
+ }
101
+ return (0, utils_1.uniqueBy)([
102
+ ...extractPrismaDatabaseAccesses(content),
103
+ ...extractSqlDatabaseAccesses(content),
104
+ ...extractKnexDatabaseAccesses(content),
105
+ ...extractRepositoryDatabaseAccesses(content),
106
+ ...extractMongooseDatabaseAccesses(content),
107
+ ...extractDjangoDatabaseAccesses(content),
108
+ ...extractActiveRecordDatabaseAccesses(content),
109
+ ...extractLaravelDatabaseAccesses(content),
110
+ ], (access) => `${access.framework}:${access.operation}:${access.model ?? ''}:${access.table ?? ''}:${access.receiver ?? ''}:${access.method ?? ''}:${access.line}`).slice(0, 200);
111
+ }
112
+ function detectDatabaseProvider(relPath, content) {
113
+ if (/schema\.prisma$/i.test(relPath) ||
114
+ /^\s*(?:model|enum|datasource|generator)\s+\w+/m.test(content)) {
115
+ return 'prisma';
116
+ }
117
+ if (/\b(?:pgTable|mysqlTable|sqliteTable)\s*\(/.test(content)) {
118
+ return 'drizzle';
119
+ }
120
+ if (/@Entity\(|@Column\(|@PrimaryGeneratedColumn\(/.test(content)) {
121
+ return 'typeorm';
122
+ }
123
+ if (/\b(?:sequelize|db)\.define\(/.test(content)) {
124
+ return 'sequelize';
125
+ }
126
+ if (/\bmongoose\.model\(|new\s+(?:mongoose\.)?Schema\(/.test(content)) {
127
+ return 'mongoose';
128
+ }
129
+ if (/class\s+\w+\s*\([^)]*models\.Model[^)]*\)/.test(content)) {
130
+ return 'django';
131
+ }
132
+ if (/class\s+\w+\s*<\s*(?:ApplicationRecord|ActiveRecord::Base|Model)\b/.test(content)) {
133
+ return 'rails';
134
+ }
135
+ if (/Schema::(?:create|table)\(|extends\s+Model\b|\$fillable|\$casts/.test(content)) {
136
+ return 'laravel';
137
+ }
138
+ if (/\b(?:CREATE|ALTER|DROP)\s+(?:TABLE|TYPE|INDEX)|\b(?:SELECT|INSERT|UPDATE|DELETE)\b/iu.test(content)) {
139
+ return 'sql';
140
+ }
141
+ return 'generic-orm';
142
+ }
143
+ function databaseExtractorForFile(relPath, content) {
144
+ return `database.${detectDatabaseProvider(relPath, content)}`;
145
+ }
146
+ function databaseOperationToAccessOperation(method) {
147
+ if (readOperations.has(method)) {
148
+ return method === 'count' || method === 'aggregate' || method === 'groupBy'
149
+ ? 'aggregate'
150
+ : 'read';
151
+ }
152
+ if (createOperations.has(method)) {
153
+ return 'create';
154
+ }
155
+ if (updateOperations.has(method)) {
156
+ return 'update';
157
+ }
158
+ if (deleteOperations.has(method)) {
159
+ return 'delete';
160
+ }
161
+ if (upsertOperations.has(method)) {
162
+ return 'upsert';
163
+ }
164
+ if (/save|insert|write/i.test(method)) {
165
+ return 'write';
166
+ }
167
+ return 'unknown';
168
+ }
169
+ function extractPrismaDatabaseModels(relPath, content) {
170
+ const enums = new Set(extractPrismaDatabaseEnums(relPath, content).map((item) => item.name));
171
+ const models = [];
172
+ for (const match of content.matchAll(/^model\s+([A-Za-z_$][\w$]*)\s*\{([\s\S]*?)^}/gm)) {
173
+ const name = match[1];
174
+ const block = match[2];
175
+ const line = (0, utils_1.lineNumberAt)(content, match.index);
176
+ const fields = [];
177
+ const indexes = [];
178
+ const relations = [];
179
+ const uniqueFields = [];
180
+ let tableName;
181
+ let primaryKey = [];
182
+ for (const row of block.split('\n')) {
183
+ const trimmed = row.trim();
184
+ const rowLine = line + block.slice(0, block.indexOf(row)).split('\n').length - 1;
185
+ if (!trimmed || trimmed.startsWith('//')) {
186
+ continue;
187
+ }
188
+ const mapMatch = trimmed.match(/^@@map\(\s*["']([^"']+)["']\s*\)/);
189
+ if (mapMatch) {
190
+ tableName = mapMatch[1];
191
+ continue;
192
+ }
193
+ const modelIndex = parsePrismaModelIndex(trimmed, rowLine);
194
+ if (modelIndex) {
195
+ indexes.push(modelIndex);
196
+ if (modelIndex.kind === 'primary') {
197
+ primaryKey = modelIndex.fields;
198
+ }
199
+ if (modelIndex.kind === 'unique') {
200
+ uniqueFields.push(modelIndex.fields);
201
+ }
202
+ continue;
203
+ }
204
+ const field = parsePrismaField(name, trimmed, rowLine, enums);
205
+ if (!field) {
206
+ continue;
207
+ }
208
+ fields.push(field);
209
+ if (field.id) {
210
+ primaryKey = [field.name];
211
+ }
212
+ if (field.unique) {
213
+ uniqueFields.push([field.name]);
214
+ }
215
+ if (field.relation?.model) {
216
+ relations.push({
217
+ fromModel: name,
218
+ fromFields: field.relation.fields,
219
+ toModel: field.relation.model,
220
+ toFields: field.relation.references,
221
+ name: field.relation.name,
222
+ onDelete: field.relation.onDelete,
223
+ onUpdate: field.relation.onUpdate,
224
+ line: rowLine,
225
+ confidence: field.relation.fields.length ? 'high' : 'medium',
226
+ });
227
+ }
228
+ }
229
+ models.push({
230
+ name,
231
+ tableName,
232
+ kind: 'model',
233
+ provider: 'prisma',
234
+ framework: 'prisma',
235
+ fields,
236
+ fieldNames: fields.map((field) => field.name),
237
+ primaryKey,
238
+ uniqueFields: (0, utils_1.uniqueBy)(uniqueFields, (fieldsValue) => fieldsValue.join('|')),
239
+ indexes,
240
+ relations,
241
+ enumRefs: (0, utils_1.unique)(fields.map((field) => field.enumName ?? '')),
242
+ map: tableName,
243
+ line,
244
+ confidence: 'high',
245
+ sourcePath: relPath,
246
+ });
247
+ }
248
+ return models;
249
+ }
250
+ function extractPrismaDatabaseEnums(relPath, content) {
251
+ const enums = [];
252
+ for (const match of content.matchAll(/^enum\s+([A-Za-z_$][\w$]*)\s*\{([\s\S]*?)^}/gm)) {
253
+ const line = (0, utils_1.lineNumberAt)(content, match.index);
254
+ let mappedName;
255
+ const values = match[2]
256
+ .split('\n')
257
+ .map((row) => row.trim())
258
+ .filter((row) => row && !row.startsWith('//'))
259
+ .filter((row) => {
260
+ const mapMatch = row.match(/^@@map\(\s*["']([^"']+)["']\s*\)/);
261
+ if (mapMatch) {
262
+ mappedName = mapMatch[1];
263
+ return false;
264
+ }
265
+ return !row.startsWith('@@');
266
+ })
267
+ .map((row) => row.split(/\s+/)[0])
268
+ .filter(Boolean);
269
+ enums.push({
270
+ name: match[1],
271
+ values,
272
+ provider: 'prisma',
273
+ map: mappedName,
274
+ line,
275
+ confidence: 'high',
276
+ sourcePath: relPath,
277
+ });
278
+ }
279
+ return enums;
280
+ }
281
+ function parsePrismaField(modelName, trimmed, line, enums) {
282
+ if (trimmed.startsWith('@@')) {
283
+ return undefined;
284
+ }
285
+ const match = trimmed.match(/^([A-Za-z_$][\w$]*)\s+([A-Za-z_$][\w$]*)(\[\])?(\?)?(.*)$/);
286
+ if (!match) {
287
+ return undefined;
288
+ }
289
+ const name = match[1];
290
+ const type = match[2];
291
+ const attrs = match[5] ?? '';
292
+ const relation = enums.has(type) ? undefined : parsePrismaRelation(modelName, type, attrs);
293
+ const mapMatch = attrs.match(/@map\(\s*["']([^"']+)["']\s*\)/);
294
+ const nativeTypeMatch = attrs.match(/@db\.([A-Za-z_$][\w$]*(?:\([^)]*\))?)/);
295
+ return {
296
+ name,
297
+ type,
298
+ databaseName: mapMatch?.[1],
299
+ required: !match[4] && !match[3],
300
+ optional: Boolean(match[4]),
301
+ list: Boolean(match[3]),
302
+ id: /@id\b/.test(attrs),
303
+ unique: /@unique\b/.test(attrs),
304
+ indexed: false,
305
+ generated: /@default\((?:autoincrement|cuid|uuid|dbgenerated)\b/.test(attrs),
306
+ updatedAt: /@updatedAt\b/.test(attrs),
307
+ default: attrs.match(/@default\(([^)]*(?:\)[^)]*)?)\)/)?.[1],
308
+ enumName: enums.has(type) ? type : undefined,
309
+ nativeType: nativeTypeMatch?.[1],
310
+ relation,
311
+ line,
312
+ source: 'prisma',
313
+ raw: trimmed,
314
+ };
315
+ }
316
+ function parsePrismaRelation(modelName, type, attrs) {
317
+ const relationMatch = attrs.match(/@relation\(([^)]*)\)/);
318
+ const likelyRelation = !prismaScalarTypes.has(type) && /^[A-Z]/.test(type);
319
+ if (!relationMatch && !likelyRelation) {
320
+ return undefined;
321
+ }
322
+ const args = relationMatch?.[1] ?? '';
323
+ const name = args.match(/["']([^"']+)["']/)?.[1];
324
+ const fields = parseBracketList(args.match(/fields\s*:\s*\[([^\]]*)\]/)?.[1]);
325
+ const references = parseBracketList(args.match(/references\s*:\s*\[([^\]]*)\]/)?.[1]);
326
+ const onDelete = args.match(/onDelete\s*:\s*([A-Za-z_$][\w$]*)/)?.[1];
327
+ const onUpdate = args.match(/onUpdate\s*:\s*([A-Za-z_$][\w$]*)/)?.[1];
328
+ return {
329
+ model: type === modelName ? undefined : type,
330
+ name,
331
+ fields,
332
+ references,
333
+ onDelete,
334
+ onUpdate,
335
+ };
336
+ }
337
+ function parsePrismaModelIndex(trimmed, line) {
338
+ const match = trimmed.match(/^@@(id|unique|index)\s*\(\s*\[([^\]]*)\]([^)]*)\)/);
339
+ if (!match) {
340
+ return undefined;
341
+ }
342
+ const kind = match[1] === 'id' ? 'primary' : match[1] === 'unique' ? 'unique' : 'index';
343
+ const name = match[3]?.match(/name\s*:\s*["']([^"']+)["']/)?.[1];
344
+ return {
345
+ kind,
346
+ name,
347
+ fields: parseBracketList(match[2]),
348
+ line,
349
+ };
350
+ }
351
+ function extractSqlDatabaseModels(relPath, content) {
352
+ const models = [];
353
+ for (const match of content.matchAll(/\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"[]?([A-Za-z_][\w.]*)[`"\]]?\s*\(([\s\S]*?)\);/gi)) {
354
+ const tableName = cleanSqlIdentifier(match[1].split('.').pop() ?? match[1]);
355
+ const body = match[2];
356
+ const line = (0, utils_1.lineNumberAt)(content, match.index);
357
+ const fields = [];
358
+ const indexes = [];
359
+ const relations = [];
360
+ let primaryKey = [];
361
+ const uniqueFields = [];
362
+ for (const part of splitSqlDefinitions(body)) {
363
+ const trimmed = part.trim();
364
+ const partLine = line + body.slice(0, body.indexOf(part)).split('\n').length - 1;
365
+ const tableIndex = parseSqlTableConstraint(tableName, trimmed, partLine);
366
+ if (tableIndex) {
367
+ indexes.push(tableIndex);
368
+ if (tableIndex.kind === 'primary') {
369
+ primaryKey = tableIndex.fields;
370
+ }
371
+ if (tableIndex.kind === 'unique') {
372
+ uniqueFields.push(tableIndex.fields);
373
+ }
374
+ if (tableIndex.kind === 'foreign' && tableIndex.references) {
375
+ relations.push({
376
+ fromModel: tableName,
377
+ fromFields: tableIndex.fields,
378
+ toModel: tableIndex.references.table,
379
+ toFields: tableIndex.references.fields,
380
+ line: partLine,
381
+ confidence: 'high',
382
+ });
383
+ }
384
+ continue;
385
+ }
386
+ const field = parseSqlColumn(tableName, trimmed, partLine);
387
+ if (!field) {
388
+ continue;
389
+ }
390
+ fields.push(field);
391
+ if (field.id) {
392
+ primaryKey = [field.name];
393
+ }
394
+ if (field.unique) {
395
+ uniqueFields.push([field.name]);
396
+ }
397
+ if (field.relation?.model) {
398
+ relations.push({
399
+ fromModel: tableName,
400
+ fromFields: [field.name],
401
+ toModel: field.relation.model,
402
+ toFields: field.relation.references,
403
+ onDelete: field.relation.onDelete,
404
+ onUpdate: field.relation.onUpdate,
405
+ line: partLine,
406
+ confidence: 'high',
407
+ });
408
+ }
409
+ }
410
+ models.push({
411
+ name: tableName,
412
+ tableName,
413
+ kind: 'table',
414
+ provider: 'sql',
415
+ framework: 'sql',
416
+ fields,
417
+ fieldNames: fields.map((field) => field.name),
418
+ primaryKey,
419
+ uniqueFields: (0, utils_1.uniqueBy)(uniqueFields, (fieldsValue) => fieldsValue.join('|')),
420
+ indexes,
421
+ relations,
422
+ enumRefs: (0, utils_1.unique)(fields.map((field) => field.enumName ?? '')),
423
+ line,
424
+ confidence: 'high',
425
+ sourcePath: relPath,
426
+ });
427
+ }
428
+ return models;
429
+ }
430
+ function extractSqlDatabaseEnums(relPath, content) {
431
+ const enums = [];
432
+ for (const match of content.matchAll(/\bCREATE\s+TYPE\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?\s+AS\s+ENUM\s*\(([^;]+)\);/gi)) {
433
+ enums.push({
434
+ name: cleanSqlIdentifier(match[1].split('.').pop() ?? match[1]),
435
+ values: Array.from(match[2].matchAll(/'([^']+)'/g)).map((item) => item[1]),
436
+ provider: 'sql',
437
+ line: (0, utils_1.lineNumberAt)(content, match.index),
438
+ confidence: 'high',
439
+ sourcePath: relPath,
440
+ });
441
+ }
442
+ return enums;
443
+ }
444
+ function extractDrizzleDatabaseModels(relPath, content) {
445
+ const models = [];
446
+ const pattern = /(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:pgTable|mysqlTable|sqliteTable)\(\s*['"]([^'"]+)['"]\s*,\s*\{([\s\S]*?)\}\s*(?:,|\))/g;
447
+ for (const match of content.matchAll(pattern)) {
448
+ const name = (0, utils_1.toPascalCase)(match[1]);
449
+ const tableName = match[2];
450
+ const body = match[3];
451
+ const line = (0, utils_1.lineNumberAt)(content, match.index);
452
+ const fields = [];
453
+ for (const fieldMatch of body.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)\(\s*['"]([^'"]+)['"][^)]*\)([^,\n}]*)/g)) {
454
+ const fieldLine = (0, utils_1.lineNumberAt)(content, (match.index ?? 0) + fieldMatch.index);
455
+ const chain = fieldMatch[4] ?? '';
456
+ fields.push({
457
+ name: fieldMatch[1],
458
+ databaseName: fieldMatch[3],
459
+ type: fieldMatch[2],
460
+ required: /\.notNull\(\)/.test(chain),
461
+ optional: !/\.notNull\(\)/.test(chain),
462
+ id: /\.primaryKey\(\)/.test(chain),
463
+ unique: /\.unique\(\)/.test(chain),
464
+ default: chain.match(/\.default\(([^)]*)\)/)?.[1],
465
+ line: fieldLine,
466
+ source: 'drizzle',
467
+ raw: fieldMatch[0],
468
+ });
469
+ }
470
+ models.push(createModelFromFields(name, tableName, 'table', 'drizzle', fields, line, relPath));
471
+ }
472
+ return models;
473
+ }
474
+ function extractTypeOrmDatabaseModels(relPath, content) {
475
+ const models = [];
476
+ const pattern = /@Entity\(([^)]*)\)\s*(?:export\s+)?class\s+([A-Za-z_$][\w$]*)\s*\{([\s\S]*?)^}/gm;
477
+ for (const match of content.matchAll(pattern)) {
478
+ const entityArgs = match[1] ?? '';
479
+ const tableName = entityArgs.match(/['"]([^'"]+)['"]/)?.[1];
480
+ const name = match[2];
481
+ const body = match[3];
482
+ const line = (0, utils_1.lineNumberAt)(content, match.index);
483
+ const fields = [];
484
+ const relations = [];
485
+ for (const fieldMatch of body.matchAll(/((?:\s*@[A-Za-z_$][\w$]*(?:\([^)]*\))?\s*)+)\s*(?:public\s+|private\s+|protected\s+)?([A-Za-z_$][\w$]*)[?!]?\s*:\s*([^;=]+)[;=]/g)) {
486
+ const decorators = fieldMatch[1];
487
+ const fieldName = fieldMatch[2];
488
+ const fieldType = fieldMatch[3].trim();
489
+ const fieldLine = (0, utils_1.lineNumberAt)(content, (match.index ?? 0) + fieldMatch.index);
490
+ const relationDecorator = decorators.match(/@(ManyToOne|OneToOne|OneToMany|ManyToMany)\(\s*\(\)\s*=>\s*([A-Za-z_$][\w$]*)/);
491
+ const field = {
492
+ name: fieldName,
493
+ type: fieldType,
494
+ required: !/nullable\s*:\s*true/.test(decorators) && !/[?]|null/.test(fieldType),
495
+ optional: /nullable\s*:\s*true/.test(decorators) || /[?]|null/.test(fieldType),
496
+ id: /@Primary/.test(decorators),
497
+ unique: /unique\s*:\s*true|@Index\([^)]*unique\s*:\s*true/.test(decorators),
498
+ generated: /@PrimaryGeneratedColumn/.test(decorators),
499
+ default: decorators.match(/default\s*:\s*([^,}]+)/)?.[1]?.trim(),
500
+ relation: relationDecorator
501
+ ? {
502
+ model: relationDecorator[2],
503
+ fields: [],
504
+ references: [],
505
+ name: relationDecorator[1],
506
+ }
507
+ : undefined,
508
+ line: fieldLine,
509
+ source: 'typeorm',
510
+ raw: fieldMatch[0].trim(),
511
+ };
512
+ fields.push(field);
513
+ if (field.relation?.model) {
514
+ relations.push({
515
+ fromModel: name,
516
+ fromFields: [],
517
+ toModel: field.relation.model,
518
+ toFields: [],
519
+ name: field.relation.name,
520
+ line: fieldLine,
521
+ confidence: 'medium',
522
+ });
523
+ }
524
+ }
525
+ const model = createModelFromFields(name, tableName, 'table', 'typeorm', fields, line, relPath);
526
+ model.relations = relations;
527
+ models.push(model);
528
+ }
529
+ return models;
530
+ }
531
+ function extractSequelizeDatabaseModels(relPath, content) {
532
+ const models = [];
533
+ for (const match of content.matchAll(/\b(?:sequelize|db)\.define\(\s*['"]([^'"]+)['"]\s*,\s*\{([\s\S]*?)\}\s*(?:,|\))/g)) {
534
+ const name = (0, utils_1.toPascalCase)(match[1]);
535
+ const body = match[2];
536
+ const line = (0, utils_1.lineNumberAt)(content, match.index);
537
+ const fields = parseObjectOrmFields(body, 'sequelize', content, match.index ?? 0);
538
+ models.push(createModelFromFields(name, match[1], 'table', 'sequelize', fields, line, relPath));
539
+ }
540
+ return models;
541
+ }
542
+ function extractMongooseDatabaseModels(relPath, content) {
543
+ const schemas = new Map();
544
+ const models = [];
545
+ for (const match of content.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+(?:mongoose\.)?Schema\(\s*\{([\s\S]*?)\}\s*(?:,|\))/g)) {
546
+ schemas.set(match[1], match[2]);
547
+ }
548
+ for (const match of content.matchAll(/mongoose\.model\(\s*['"]([^'"]+)['"]\s*,\s*([A-Za-z_$][\w$]*)/g)) {
549
+ const name = match[1];
550
+ const schemaBody = schemas.get(match[2]) ?? '';
551
+ const line = (0, utils_1.lineNumberAt)(content, match.index);
552
+ const fields = parseObjectOrmFields(schemaBody, 'mongoose', content, match.index ?? 0);
553
+ models.push(createModelFromFields(name, name, 'collection', 'mongoose', fields, line, relPath));
554
+ }
555
+ return models;
556
+ }
557
+ function extractDjangoDatabaseModels(relPath, content) {
558
+ const models = [];
559
+ for (const match of content.matchAll(/class\s+([A-Z][A-Za-z0-9_]*)\s*\([^)]*models\.Model[^)]*\):([\s\S]*?)(?=\nclass\s|\n\S|$)/g)) {
560
+ const name = match[1];
561
+ const body = match[2];
562
+ const line = (0, utils_1.lineNumberAt)(content, match.index);
563
+ const fields = [];
564
+ const relations = [];
565
+ const tableName = body.match(/db_table\s*=\s*['"]([^'"]+)['"]/)?.[1];
566
+ for (const fieldMatch of body.matchAll(/^\s+([A-Za-z_][\w]*)\s*=\s*models\.([A-Za-z_][\w]*)\(([^)]*)\)/gm)) {
567
+ const fieldLine = (0, utils_1.lineNumberAt)(content, (match.index ?? 0) + fieldMatch.index);
568
+ const fieldType = fieldMatch[2];
569
+ const args = fieldMatch[3] ?? '';
570
+ const relationTarget = fieldType.match(/^(?:ForeignKey|OneToOneField|ManyToManyField)$/)
571
+ ? args.match(/['"]?([A-Z][A-Za-z0-9_]*)['"]?/)?.[1]
572
+ : undefined;
573
+ const field = {
574
+ name: fieldMatch[1],
575
+ type: fieldType,
576
+ required: !/null\s*=\s*True|blank\s*=\s*True/.test(args),
577
+ optional: /null\s*=\s*True|blank\s*=\s*True/.test(args),
578
+ unique: /unique\s*=\s*True/.test(args),
579
+ default: args.match(/default\s*=\s*([^,)]+)/)?.[1]?.trim(),
580
+ relation: relationTarget
581
+ ? { model: relationTarget, fields: [fieldMatch[1]], references: ['id'] }
582
+ : undefined,
583
+ line: fieldLine,
584
+ source: 'django',
585
+ raw: fieldMatch[0].trim(),
586
+ };
587
+ fields.push(field);
588
+ if (relationTarget) {
589
+ relations.push({
590
+ fromModel: name,
591
+ fromFields: [field.name],
592
+ toModel: relationTarget,
593
+ toFields: ['id'],
594
+ line: fieldLine,
595
+ confidence: 'medium',
596
+ });
597
+ }
598
+ }
599
+ const model = createModelFromFields(name, tableName, 'table', 'django', fields, line, relPath);
600
+ model.relations = relations;
601
+ models.push(model);
602
+ }
603
+ return models;
604
+ }
605
+ function extractRailsMigrationModels(relPath, content) {
606
+ const models = [];
607
+ for (const match of content.matchAll(/create_table\s+:([A-Za-z_][\w]*)\s+do\s+\|t\|([\s\S]*?)\n\s*end/g)) {
608
+ const tableName = match[1];
609
+ const name = (0, utils_1.toPascalCase)(tableName.replace(/s$/, ''));
610
+ const body = match[2];
611
+ const line = (0, utils_1.lineNumberAt)(content, match.index);
612
+ const fields = parseRailsTableFields(body, content, match.index ?? 0);
613
+ models.push(createModelFromFields(name, tableName, 'table', 'rails', fields, line, relPath));
614
+ }
615
+ return models;
616
+ }
617
+ function extractLaravelMigrationModels(relPath, content) {
618
+ const models = [];
619
+ for (const match of content.matchAll(/Schema::create\(\s*['"]([^'"]+)['"]\s*,\s*function\s*\([^)]*\)\s*\{([\s\S]*?)\}\s*\);/g)) {
620
+ const tableName = match[1];
621
+ const name = (0, utils_1.toPascalCase)(tableName.replace(/s$/, ''));
622
+ const body = match[2];
623
+ const line = (0, utils_1.lineNumberAt)(content, match.index);
624
+ const fields = parseLaravelTableFields(body, content, match.index ?? 0);
625
+ models.push(createModelFromFields(name, tableName, 'table', 'laravel', fields, line, relPath));
626
+ }
627
+ return models;
628
+ }
629
+ function extractSqlMigrationOperations(content) {
630
+ const operations = [];
631
+ addSqlOperationMatches(operations, content, /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"[]?([A-Za-z_][\w.]*)[`"\]]?\s*\(([\s\S]*?)\);/gi, 'create-table', false);
632
+ addSqlOperationMatches(operations, content, /\bALTER\s+TABLE\s+(?:ONLY\s+)?[`"[]?([A-Za-z_][\w.]*)[`"\]]?\s+([\s\S]*?);/gi, 'alter-table', false);
633
+ addSqlOperationMatches(operations, content, /\bDROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?[`"[]?([A-Za-z_][\w.]*)[`"\]]?[^;]*;/gi, 'drop-table', true);
634
+ addSqlOperationMatches(operations, content, /\bCREATE\s+(?:UNIQUE\s+)?INDEX\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?\s+ON\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?\s*\(([^)]*)\)/gi, 'create-index', false);
635
+ addSqlOperationMatches(operations, content, /\bDROP\s+INDEX\s+(?:IF\s+EXISTS\s+)?[`"[]?([A-Za-z_][\w.]*)[`"\]]?[^;]*;/gi, 'drop-index', true);
636
+ addSqlOperationMatches(operations, content, /\bCREATE\s+TYPE\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?\s+AS\s+ENUM\s*\(([^;]+)\);/gi, 'create-enum', false);
637
+ addSqlOperationMatches(operations, content, /\bDROP\s+TYPE\s+(?:IF\s+EXISTS\s+)?[`"[]?([A-Za-z_][\w.]*)[`"\]]?[^;]*;/gi, 'drop-enum', true);
638
+ addSqlOperationMatches(operations, content, /\bINSERT\s+INTO\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?[\s\S]*?;/gi, 'insert-seed', false);
639
+ addSqlOperationMatches(operations, content, /\bUPDATE\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?[\s\S]*?;/gi, 'update-data', false);
640
+ addSqlOperationMatches(operations, content, /\bDELETE\s+FROM\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?[\s\S]*?;/gi, 'delete-data', true);
641
+ return operations;
642
+ }
643
+ function extractRailsMigrationOperations(content) {
644
+ const operations = [];
645
+ for (const match of content.matchAll(/create_table\s+:([A-Za-z_][\w]*)/g)) {
646
+ operations.push({
647
+ kind: 'create-table',
648
+ target: match[1],
649
+ line: (0, utils_1.lineNumberAt)(content, match.index),
650
+ statement: match[0],
651
+ });
652
+ }
653
+ for (const match of content.matchAll(/add_(column|reference|index)\s+:([A-Za-z_][\w]*)/g)) {
654
+ operations.push({
655
+ kind: match[1] === 'index' ? 'create-index' : 'alter-table',
656
+ target: match[2],
657
+ line: (0, utils_1.lineNumberAt)(content, match.index),
658
+ statement: match[0],
659
+ });
660
+ }
661
+ for (const match of content.matchAll(/(?:drop_table|remove_column|remove_index)\s+:([A-Za-z_][\w]*)/g)) {
662
+ operations.push({
663
+ kind: match[0].startsWith('drop_table') ? 'drop-table' : 'alter-table',
664
+ target: match[1],
665
+ destructive: true,
666
+ line: (0, utils_1.lineNumberAt)(content, match.index),
667
+ statement: match[0],
668
+ });
669
+ }
670
+ return operations;
671
+ }
672
+ function extractLaravelMigrationOperations(content) {
673
+ const operations = [];
674
+ for (const match of content.matchAll(/Schema::(create|table|drop|dropIfExists)\(\s*['"]([^'"]+)['"]/g)) {
675
+ operations.push({
676
+ kind: match[1] === 'create'
677
+ ? 'create-table'
678
+ : match[1] === 'table'
679
+ ? 'alter-table'
680
+ : 'drop-table',
681
+ target: match[2],
682
+ destructive: match[1] === 'drop' || match[1] === 'dropIfExists',
683
+ line: (0, utils_1.lineNumberAt)(content, match.index),
684
+ statement: match[0],
685
+ });
686
+ }
687
+ return operations;
688
+ }
689
+ function addSqlOperationMatches(operations, content, pattern, kind, destructive) {
690
+ for (const match of content.matchAll(pattern)) {
691
+ const statement = match[0].replace(/\s+/g, ' ').trim();
692
+ const target = cleanSqlIdentifier((kind === 'create-index' ? match[2] : match[1]) ?? '');
693
+ const name = kind === 'create-index' || kind === 'drop-index' ? cleanSqlIdentifier(match[1]) : undefined;
694
+ const fields = kind === 'create-table'
695
+ ? extractSqlCreateTableFields(match[2] ?? '')
696
+ : kind === 'create-index'
697
+ ? parseSqlFieldList(match[3] ?? '')
698
+ : [];
699
+ const references = statement.match(/REFERENCES\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?\s*\(([^)]*)\)/i);
700
+ operations.push({
701
+ kind,
702
+ target,
703
+ name,
704
+ fields,
705
+ columns: fields,
706
+ references: references
707
+ ? {
708
+ table: cleanSqlIdentifier(references[1].split('.').pop() ?? references[1]),
709
+ columns: parseSqlFieldList(references[2]),
710
+ }
711
+ : undefined,
712
+ destructive,
713
+ line: (0, utils_1.lineNumberAt)(content, match.index),
714
+ statement: statement.slice(0, 260),
715
+ });
716
+ }
717
+ }
718
+ function extractPrismaDatabaseAccesses(content) {
719
+ const accesses = [];
720
+ const prismaAccessPattern = /\b((?:this\.)?prisma|tx|db)\.([a-z][A-Za-z0-9_]*)\.(findUniqueOrThrow|findUnique|findFirstOrThrow|findFirst|findMany|createMany|create|updateMany|update|deleteMany|delete|upsert|count|aggregate|groupBy)\s*\(/g;
721
+ for (const match of content.matchAll(prismaAccessPattern)) {
722
+ const model = (0, utils_1.toPascalCase)(match[2]);
723
+ const method = match[3];
724
+ const openIndex = (match.index ?? 0) + match[0].lastIndexOf('(');
725
+ const callBody = (0, extractors_1.readBalancedRange)(content, openIndex, '(', ')') ?? '';
726
+ const operation = databaseOperationToAccessOperation(method);
727
+ accesses.push({
728
+ operation,
729
+ model,
730
+ receiver: match[2],
731
+ method,
732
+ framework: 'prisma',
733
+ line: (0, utils_1.lineNumberAt)(content, match.index),
734
+ fields: extractObjectKeysFromProperty(callBody, 'data'),
735
+ whereFields: extractObjectKeysFromProperty(callBody, 'where'),
736
+ relationHints: extractRelationHints(callBody),
737
+ transactional: match[1] === 'tx',
738
+ confidence: 'high',
739
+ risk: databaseAccessRisk(operation, model, callBody),
740
+ });
741
+ }
742
+ for (const match of content.matchAll(/\b(?:this\.)?(?:prisma|db)\.\$(transaction|queryRaw|executeRaw)\b/g)) {
743
+ const method = match[1];
744
+ accesses.push({
745
+ operation: method === 'transaction' ? 'transaction' : 'raw-sql',
746
+ method,
747
+ framework: 'prisma',
748
+ line: (0, utils_1.lineNumberAt)(content, match.index),
749
+ fields: [],
750
+ whereFields: [],
751
+ relationHints: [],
752
+ transactional: method === 'transaction',
753
+ raw: method !== 'transaction',
754
+ confidence: 'medium',
755
+ risk: method === 'transaction' ? [] : ['raw-sql-review'],
756
+ });
757
+ }
758
+ return accesses;
759
+ }
760
+ function extractSqlDatabaseAccesses(content) {
761
+ const accesses = [];
762
+ const patterns = [
763
+ [/\bSELECT\b[\s\S]*?\bFROM\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?/gi, 'read'],
764
+ [/\bINSERT\s+INTO\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?\s*(?:\(([^)]*)\))?/gi, 'create'],
765
+ [
766
+ /\bUPDATE\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?[\s\S]*?\bSET\s+([\s\S]*?)(?:\bWHERE\b|;|\n)/gi,
767
+ 'update',
768
+ ],
769
+ [/\bDELETE\s+FROM\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?/gi, 'delete'],
770
+ ];
771
+ for (const [pattern, operation] of patterns) {
772
+ for (const match of content.matchAll(pattern)) {
773
+ const table = cleanSqlIdentifier(match[1].split('.').pop() ?? match[1]);
774
+ accesses.push({
775
+ operation,
776
+ table,
777
+ framework: 'sql',
778
+ line: (0, utils_1.lineNumberAt)(content, match.index),
779
+ fields: operation === 'create'
780
+ ? parseSqlFieldList(match[2] ?? '')
781
+ : operation === 'update'
782
+ ? extractAssignmentFields(match[2] ?? '')
783
+ : [],
784
+ whereFields: extractSqlWhereFields(match[0]),
785
+ relationHints: [],
786
+ raw: true,
787
+ confidence: /[`'"]/.test(match[0]) ? 'medium' : 'high',
788
+ risk: databaseAccessRisk(operation, table, match[0]),
789
+ });
790
+ }
791
+ }
792
+ return accesses;
793
+ }
794
+ function extractKnexDatabaseAccesses(content) {
795
+ const accesses = [];
796
+ const pattern = /\b(?:knex|db|database)\(\s*['"]([^'"]+)['"]\s*\)(?:\.[A-Za-z_$][\w$]+\([^)]*\))*\.(select|first|where|insert|update|del|delete|count)\s*\(([^)]*)\)/g;
797
+ for (const match of content.matchAll(pattern)) {
798
+ const method = match[2];
799
+ const operation = method === 'insert'
800
+ ? 'create'
801
+ : method === 'update'
802
+ ? 'update'
803
+ : method === 'del' || method === 'delete'
804
+ ? 'delete'
805
+ : method === 'count'
806
+ ? 'aggregate'
807
+ : 'read';
808
+ accesses.push({
809
+ operation,
810
+ table: match[1],
811
+ receiver: 'knex',
812
+ method,
813
+ framework: 'sql',
814
+ line: (0, utils_1.lineNumberAt)(content, match.index),
815
+ fields: extractStringArgs(match[3] ?? ''),
816
+ whereFields: [],
817
+ relationHints: [],
818
+ confidence: 'medium',
819
+ risk: databaseAccessRisk(operation, match[1], match[0]),
820
+ });
821
+ }
822
+ return accesses;
823
+ }
824
+ function extractRepositoryDatabaseAccesses(content) {
825
+ const accesses = [];
826
+ const pattern = /\b(?:this\.)?([A-Za-z_$][\w$]*(?:Repository|Repo|Store))\.(find|findOne|findBy|findAndCount|save|insert|create|update|upsert|delete|remove|count)\s*\(([\s\S]*?)\)/g;
827
+ for (const match of content.matchAll(pattern)) {
828
+ const receiver = match[1];
829
+ const method = match[2];
830
+ const operation = method === 'find' || method === 'findOne' || method === 'findBy'
831
+ ? 'read'
832
+ : method === 'count' || method === 'findAndCount'
833
+ ? 'aggregate'
834
+ : databaseOperationToAccessOperation(method);
835
+ const model = repositoryReceiverToModel(receiver);
836
+ accesses.push({
837
+ operation,
838
+ model,
839
+ receiver,
840
+ method,
841
+ framework: 'typeorm',
842
+ line: (0, utils_1.lineNumberAt)(content, match.index),
843
+ fields: extractObjectKeysFromProperty(match[3] ?? '', 'data'),
844
+ whereFields: extractObjectKeysFromProperty(match[3] ?? '', 'where'),
845
+ relationHints: extractRelationHints(match[3] ?? ''),
846
+ confidence: 'medium',
847
+ risk: databaseAccessRisk(operation, model, match[0]),
848
+ });
849
+ }
850
+ return accesses;
851
+ }
852
+ function extractMongooseDatabaseAccesses(content) {
853
+ const accesses = [];
854
+ const pattern = /\b([A-Z][A-Za-z0-9_]*)\.(find|findOne|findById|create|insertMany|updateOne|updateMany|findByIdAndUpdate|deleteOne|deleteMany|findByIdAndDelete|countDocuments|aggregate)\s*\(([\s\S]*?)\)/g;
855
+ for (const match of content.matchAll(pattern)) {
856
+ const method = match[2];
857
+ const operation = method.startsWith('find') || method === 'findOne'
858
+ ? 'read'
859
+ : method === 'create' || method === 'insertMany'
860
+ ? 'create'
861
+ : method.includes('Update')
862
+ ? 'update'
863
+ : method.includes('delete') || method.includes('Delete')
864
+ ? 'delete'
865
+ : method === 'aggregate' || method === 'countDocuments'
866
+ ? 'aggregate'
867
+ : 'unknown';
868
+ accesses.push({
869
+ operation,
870
+ model: match[1],
871
+ receiver: match[1],
872
+ method,
873
+ framework: 'mongoose',
874
+ line: (0, utils_1.lineNumberAt)(content, match.index),
875
+ fields: [],
876
+ whereFields: extractObjectKeys(match[3] ?? ''),
877
+ relationHints: [],
878
+ confidence: 'medium',
879
+ risk: databaseAccessRisk(operation, match[1], match[0]),
880
+ });
881
+ }
882
+ return accesses;
883
+ }
884
+ function extractDjangoDatabaseAccesses(content) {
885
+ const accesses = [];
886
+ const pattern = /\b([A-Z][A-Za-z0-9_]*)\.objects\.(filter|get|all|create|update|get_or_create|update_or_create|delete|count)\s*\(([^)]*)\)/g;
887
+ for (const match of content.matchAll(pattern)) {
888
+ const method = match[2];
889
+ const operation = method === 'filter' || method === 'get' || method === 'all'
890
+ ? 'read'
891
+ : method === 'count'
892
+ ? 'aggregate'
893
+ : method.includes('create')
894
+ ? 'create'
895
+ : method === 'update'
896
+ ? 'update'
897
+ : method === 'delete'
898
+ ? 'delete'
899
+ : 'unknown';
900
+ accesses.push({
901
+ operation,
902
+ model: match[1],
903
+ receiver: `${match[1]}.objects`,
904
+ method,
905
+ framework: 'django',
906
+ line: (0, utils_1.lineNumberAt)(content, match.index),
907
+ fields: extractKeywordArgs(match[3] ?? ''),
908
+ whereFields: extractKeywordArgs(match[3] ?? ''),
909
+ relationHints: [],
910
+ confidence: 'medium',
911
+ risk: databaseAccessRisk(operation, match[1], match[0]),
912
+ });
913
+ }
914
+ return accesses;
915
+ }
916
+ function extractActiveRecordDatabaseAccesses(content) {
917
+ const accesses = [];
918
+ const pattern = /\b([A-Z][A-Za-z0-9_]*)\.(where|find|find_by|all|create|create!|update|update!|destroy|delete|count)\s*(?:\(([^)]*)\))?/g;
919
+ for (const match of content.matchAll(pattern)) {
920
+ const method = match[2];
921
+ const operation = method === 'where' || method === 'find' || method === 'find_by' || method === 'all'
922
+ ? 'read'
923
+ : method === 'count'
924
+ ? 'aggregate'
925
+ : method.startsWith('create')
926
+ ? 'create'
927
+ : method.startsWith('update')
928
+ ? 'update'
929
+ : method === 'destroy' || method === 'delete'
930
+ ? 'delete'
931
+ : 'unknown';
932
+ accesses.push({
933
+ operation,
934
+ model: match[1],
935
+ receiver: match[1],
936
+ method,
937
+ framework: 'rails',
938
+ line: (0, utils_1.lineNumberAt)(content, match.index),
939
+ fields: extractRubyHashKeys(match[3] ?? ''),
940
+ whereFields: extractRubyHashKeys(match[3] ?? ''),
941
+ relationHints: [],
942
+ confidence: 'medium',
943
+ risk: databaseAccessRisk(operation, match[1], match[0]),
944
+ });
945
+ }
946
+ return accesses;
947
+ }
948
+ function extractLaravelDatabaseAccesses(content) {
949
+ const accesses = [];
950
+ const pattern = /\b([A-Z][A-Za-z0-9_]*)::(?:query\(\)->)?(where|find|all|create|update|upsert|delete|count)\s*\(([^)]*)\)/g;
951
+ for (const match of content.matchAll(pattern)) {
952
+ const method = match[2];
953
+ const operation = method === 'where' || method === 'find' || method === 'all'
954
+ ? 'read'
955
+ : method === 'count'
956
+ ? 'aggregate'
957
+ : databaseOperationToAccessOperation(method);
958
+ accesses.push({
959
+ operation,
960
+ model: match[1],
961
+ receiver: match[1],
962
+ method,
963
+ framework: 'laravel',
964
+ line: (0, utils_1.lineNumberAt)(content, match.index),
965
+ fields: extractStringArgs(match[3] ?? ''),
966
+ whereFields: extractStringArgs(match[3] ?? ''),
967
+ relationHints: [],
968
+ confidence: 'medium',
969
+ risk: databaseAccessRisk(operation, match[1], match[0]),
970
+ });
971
+ }
972
+ return accesses;
973
+ }
974
+ function createModelFromFields(name, tableName, kind, provider, fields, line, relPath) {
975
+ return {
976
+ name,
977
+ tableName,
978
+ kind,
979
+ provider,
980
+ framework: provider,
981
+ fields,
982
+ fieldNames: fields.map((field) => field.name),
983
+ primaryKey: fields.filter((field) => field.id).map((field) => field.name),
984
+ uniqueFields: fields.filter((field) => field.unique).map((field) => [field.name]),
985
+ indexes: fields
986
+ .filter((field) => field.id || field.unique || field.indexed)
987
+ .map((field) => ({
988
+ kind: field.id ? 'primary' : field.unique ? 'unique' : 'index',
989
+ fields: [field.name],
990
+ line: field.line,
991
+ })),
992
+ relations: fields
993
+ .filter((field) => field.relation?.model)
994
+ .map((field) => ({
995
+ fromModel: name,
996
+ fromFields: field.relation?.fields.length ? field.relation.fields : [field.name],
997
+ toModel: field.relation?.model ?? '',
998
+ toFields: field.relation?.references.length ? field.relation.references : ['id'],
999
+ name: field.relation?.name,
1000
+ onDelete: field.relation?.onDelete,
1001
+ onUpdate: field.relation?.onUpdate,
1002
+ line: field.line,
1003
+ confidence: 'medium',
1004
+ })),
1005
+ enumRefs: (0, utils_1.unique)(fields.map((field) => field.enumName ?? '')),
1006
+ line,
1007
+ confidence: provider === 'generic-orm' ? 'low' : 'medium',
1008
+ sourcePath: relPath,
1009
+ };
1010
+ }
1011
+ function parseSqlColumn(tableName, trimmed, line) {
1012
+ const match = trimmed.match(/^[`"[]?([A-Za-z_][\w]*)[`"\]]?\s+(.+)$/);
1013
+ if (!match || /^(?:CONSTRAINT|PRIMARY|FOREIGN|UNIQUE|KEY|INDEX|CHECK)\b/i.test(match[1])) {
1014
+ return undefined;
1015
+ }
1016
+ const name = cleanSqlIdentifier(match[1]);
1017
+ const rest = match[2];
1018
+ const type = rest
1019
+ .split(/\s+(?:NOT\s+NULL|NULL|DEFAULT|PRIMARY\s+KEY|UNIQUE|REFERENCES|CONSTRAINT|CHECK)\b/i)[0]
1020
+ .trim();
1021
+ const references = rest.match(/REFERENCES\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?\s*\(([^)]*)\)/i);
1022
+ return {
1023
+ name,
1024
+ type,
1025
+ required: /\bNOT\s+NULL\b/i.test(rest) || /\bPRIMARY\s+KEY\b/i.test(rest),
1026
+ optional: !/\bNOT\s+NULL\b/i.test(rest) && !/\bPRIMARY\s+KEY\b/i.test(rest),
1027
+ id: /\bPRIMARY\s+KEY\b/i.test(rest),
1028
+ unique: /\bUNIQUE\b/i.test(rest),
1029
+ default: rest.match(/\bDEFAULT\s+([^,\n]+)/i)?.[1]?.trim(),
1030
+ relation: references
1031
+ ? {
1032
+ model: cleanSqlIdentifier(references[1].split('.').pop() ?? references[1]),
1033
+ fields: [name],
1034
+ references: parseSqlFieldList(references[2]),
1035
+ onDelete: rest.match(/\bON\s+DELETE\s+([A-Z]+)/i)?.[1],
1036
+ onUpdate: rest.match(/\bON\s+UPDATE\s+([A-Z]+)/i)?.[1],
1037
+ }
1038
+ : undefined,
1039
+ line,
1040
+ source: 'sql',
1041
+ raw: trimmed,
1042
+ };
1043
+ }
1044
+ function parseSqlTableConstraint(tableName, trimmed, line) {
1045
+ const normalized = trimmed.replace(/^CONSTRAINT\s+[`"[]?[A-Za-z_][\w.]*[`"\]]?\s+/i, '');
1046
+ const primary = normalized.match(/^PRIMARY\s+KEY\s*\(([^)]*)\)/i);
1047
+ const uniqueMatch = normalized.match(/^UNIQUE\s*\(([^)]*)\)/i);
1048
+ const foreign = normalized.match(/^FOREIGN\s+KEY\s*\(([^)]*)\)\s+REFERENCES\s+[`"[]?([A-Za-z_][\w.]*)[`"\]]?\s*\(([^)]*)\)/i);
1049
+ if (primary) {
1050
+ return { kind: 'primary', fields: parseSqlFieldList(primary[1]), line };
1051
+ }
1052
+ if (uniqueMatch) {
1053
+ return { kind: 'unique', fields: parseSqlFieldList(uniqueMatch[1]), line };
1054
+ }
1055
+ if (foreign) {
1056
+ return {
1057
+ kind: 'foreign',
1058
+ fields: parseSqlFieldList(foreign[1]),
1059
+ references: {
1060
+ table: cleanSqlIdentifier(foreign[2].split('.').pop() ?? foreign[2]),
1061
+ fields: parseSqlFieldList(foreign[3]),
1062
+ },
1063
+ line,
1064
+ };
1065
+ }
1066
+ void tableName;
1067
+ return undefined;
1068
+ }
1069
+ function parseObjectOrmFields(body, source, content, offset) {
1070
+ const fields = [];
1071
+ for (const match of body.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*(?:\{([^{}]*)\}|([A-Za-z_$][\w$.]*))/g)) {
1072
+ const raw = match[0];
1073
+ const config = match[2] ?? match[3] ?? '';
1074
+ const type = config.match(/type\s*:\s*([A-Za-z_$][\w$.]*)/)?.[1] ?? match[3];
1075
+ fields.push({
1076
+ name: match[1],
1077
+ type,
1078
+ required: /required\s*:\s*true|allowNull\s*:\s*false/.test(config),
1079
+ optional: /required\s*:\s*false|allowNull\s*:\s*true/.test(config),
1080
+ id: /primaryKey\s*:\s*true/.test(config),
1081
+ unique: /unique\s*:\s*true/.test(config),
1082
+ default: config.match(/default(?:Value)?\s*:\s*([^,}]+)/)?.[1]?.trim(),
1083
+ line: (0, utils_1.lineNumberAt)(content, offset + (match.index ?? 0)),
1084
+ source,
1085
+ raw,
1086
+ });
1087
+ }
1088
+ return fields;
1089
+ }
1090
+ function parseRailsTableFields(body, content, offset) {
1091
+ const fields = [];
1092
+ for (const match of body.matchAll(/t\.([a-z_]+)\s+:([A-Za-z_][\w]*)([^,\n]*)/g)) {
1093
+ fields.push({
1094
+ name: match[2],
1095
+ type: match[1],
1096
+ required: /null:\s*false/.test(match[3] ?? ''),
1097
+ optional: !/null:\s*false/.test(match[3] ?? ''),
1098
+ unique: /unique:\s*true/.test(match[3] ?? ''),
1099
+ indexed: /index:\s*true/.test(match[3] ?? ''),
1100
+ relation: match[1] === 'references'
1101
+ ? { model: (0, utils_1.toPascalCase)(match[2]), fields: [match[2]], references: ['id'] }
1102
+ : undefined,
1103
+ line: (0, utils_1.lineNumberAt)(content, offset + (match.index ?? 0)),
1104
+ source: 'rails',
1105
+ raw: match[0],
1106
+ });
1107
+ }
1108
+ return fields;
1109
+ }
1110
+ function parseLaravelTableFields(body, content, offset) {
1111
+ const fields = [];
1112
+ for (const match of body.matchAll(/\$table->([A-Za-z_][\w]*)\(([^)]*)\)([^;\n]*);/g)) {
1113
+ const method = match[1];
1114
+ const args = match[2] ?? '';
1115
+ const name = extractStringArgs(args)[0] ?? (method === 'id' ? 'id' : method);
1116
+ const chain = match[3] ?? '';
1117
+ fields.push({
1118
+ name,
1119
+ type: method,
1120
+ required: !/nullable\(\)/.test(chain),
1121
+ optional: /nullable\(\)/.test(chain),
1122
+ id: method === 'id' || /primary\(\)/.test(chain),
1123
+ unique: /unique\(\)/.test(chain),
1124
+ indexed: /index\(\)/.test(chain) || /foreignId|constrained/.test(method + chain),
1125
+ default: chain.match(/default\(([^)]*)\)/)?.[1],
1126
+ relation: method === 'foreignId' || /constrained\(\)/.test(chain)
1127
+ ? {
1128
+ model: (0, utils_1.toPascalCase)(name.replace(/_id$|Id$/, '')),
1129
+ fields: [name],
1130
+ references: ['id'],
1131
+ }
1132
+ : undefined,
1133
+ line: (0, utils_1.lineNumberAt)(content, offset + (match.index ?? 0)),
1134
+ source: 'laravel',
1135
+ raw: match[0],
1136
+ });
1137
+ }
1138
+ return fields;
1139
+ }
1140
+ function splitSqlDefinitions(value) {
1141
+ const parts = [];
1142
+ let depth = 0;
1143
+ let quote;
1144
+ let start = 0;
1145
+ for (let index = 0; index < value.length; index += 1) {
1146
+ const char = value[index];
1147
+ if (quote) {
1148
+ if (char === quote && value[index - 1] !== '\\') {
1149
+ quote = undefined;
1150
+ }
1151
+ continue;
1152
+ }
1153
+ if (char === '"' || char === "'" || char === '`') {
1154
+ quote = char;
1155
+ continue;
1156
+ }
1157
+ if (char === '(')
1158
+ depth += 1;
1159
+ if (char === ')')
1160
+ depth = Math.max(0, depth - 1);
1161
+ if (char === ',' && depth === 0) {
1162
+ parts.push(value.slice(start, index));
1163
+ start = index + 1;
1164
+ }
1165
+ }
1166
+ parts.push(value.slice(start));
1167
+ return parts;
1168
+ }
1169
+ function extractSqlCreateTableFields(body) {
1170
+ return splitSqlDefinitions(body)
1171
+ .map((part) => part.trim().match(/^[`"[]?([A-Za-z_][\w]*)[`"\]]?\s+/)?.[1] ?? '')
1172
+ .filter((field) => field && !/^(?:CONSTRAINT|PRIMARY|FOREIGN|UNIQUE|KEY|INDEX|CHECK)$/i.test(field));
1173
+ }
1174
+ function parseBracketList(value) {
1175
+ if (!value) {
1176
+ return [];
1177
+ }
1178
+ return (0, utils_1.unique)(value
1179
+ .split(',')
1180
+ .map((item) => item.trim().replace(/[`"'[\]]/g, ''))
1181
+ .filter(Boolean));
1182
+ }
1183
+ function parseSqlFieldList(value) {
1184
+ return parseBracketList(value).map(cleanSqlIdentifier);
1185
+ }
1186
+ function cleanSqlIdentifier(value) {
1187
+ return value.trim().replace(/^[`"[]+|[`"\]]+$/g, '');
1188
+ }
1189
+ function extractObjectKeysFromProperty(body, property) {
1190
+ const match = body.match(new RegExp(`\\b${(0, utils_1.escapeRegExp)(property)}\\s*:\\s*\\{([\\s\\S]*?)\\}`, 'm'));
1191
+ return match ? extractObjectKeys(match[1]) : [];
1192
+ }
1193
+ function extractObjectKeys(body) {
1194
+ return (0, utils_1.unique)(Array.from(body.matchAll(/\b([A-Za-z_$][\w$]*)\s*:/g)).map((match) => match[1]))
1195
+ .filter((item) => !['where', 'data', 'select', 'include', 'orderBy'].includes(item))
1196
+ .slice(0, 60);
1197
+ }
1198
+ function extractRelationHints(body) {
1199
+ const hints = new Set();
1200
+ for (const match of body.matchAll(/\binclude\s*:\s*\{([\s\S]*?)\}/g)) {
1201
+ for (const key of extractObjectKeys(match[1])) {
1202
+ hints.add(key);
1203
+ }
1204
+ }
1205
+ return Array.from(hints).slice(0, 40);
1206
+ }
1207
+ function extractAssignmentFields(body) {
1208
+ return (0, utils_1.unique)(Array.from(body.matchAll(/\b([A-Za-z_][\w]*)\s*=/g)).map((match) => match[1])).slice(0, 60);
1209
+ }
1210
+ function extractSqlWhereFields(body) {
1211
+ const where = body.match(/\bWHERE\b([\s\S]*)/i)?.[1] ?? '';
1212
+ return (0, utils_1.unique)(Array.from(where.matchAll(/[`"[]?([A-Za-z_][\w]*)[`"\]]?\s*(?:=|<>|!=|<|>|LIKE|IN\b)/gi)).map((match) => cleanSqlIdentifier(match[1]))).slice(0, 60);
1213
+ }
1214
+ function extractStringArgs(body) {
1215
+ return (0, utils_1.unique)(Array.from(body.matchAll(/['"]([^'"]+)['"]/g)).map((match) => match[1])).slice(0, 60);
1216
+ }
1217
+ function extractKeywordArgs(body) {
1218
+ return (0, utils_1.unique)(Array.from(body.matchAll(/\b([A-Za-z_][\w]*)\s*=/g)).map((match) => match[1])).slice(0, 60);
1219
+ }
1220
+ function extractRubyHashKeys(body) {
1221
+ return (0, utils_1.unique)(Array.from(body.matchAll(/\b([A-Za-z_][\w]*):/g)).map((match) => match[1])).slice(0, 60);
1222
+ }
1223
+ function repositoryReceiverToModel(receiver) {
1224
+ return (0, utils_1.toPascalCase)(receiver.replace(/(?:Repository|Repo|Store)$/i, ''));
1225
+ }
1226
+ function databaseAccessRisk(operation, modelOrTable, context) {
1227
+ const risk = [];
1228
+ const lower = `${modelOrTable ?? ''} ${context}`.toLowerCase();
1229
+ if (['create', 'update', 'delete', 'upsert', 'write', 'raw-sql'].includes(operation)) {
1230
+ risk.push('mutates-data');
1231
+ }
1232
+ if (operation === 'delete') {
1233
+ risk.push('destructive-write');
1234
+ }
1235
+ if (/user|session|token|password|auth|billing|payment|stripe|permission|role|api.?key/.test(lower)) {
1236
+ risk.push('sensitive-model');
1237
+ }
1238
+ if (/raw|queryRaw|executeRaw|\bDELETE\b|\bDROP\b|\bTRUNCATE\b/.test(context)) {
1239
+ risk.push('manual-review');
1240
+ }
1241
+ return (0, utils_1.unique)(risk);
1242
+ }
1243
+ function migrationNameFromPath(relPath) {
1244
+ const parts = relPath.split('/');
1245
+ const migrationDir = parts.length > 1 ? parts[parts.length - 2] : undefined;
1246
+ const fileName = parts[parts.length - 1]?.replace(/\.[^.]+$/, '');
1247
+ return migrationDir && !/^migrations?$/i.test(migrationDir) ? migrationDir : fileName || relPath;
1248
+ }
1249
+ function isMigrationLikePath(relPath) {
1250
+ return /(?:^|\/)(?:migrations?|db\/migrate|database\/migrations)(?:\/|$)/i.test(relPath);
1251
+ }
1252
+ //# sourceMappingURL=database.js.map