@dreamtree-org/graphify 1.0.0 → 1.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,155 @@
1
+ import {
2
+ validateDsn
3
+ } from "./chunk-6JLEILYF.js";
4
+
5
+ // src/extractors/mysql.ts
6
+ var schemaId = (schema) => `db:${schema}`;
7
+ var tableId = (schema, table) => `db:${schema}::${table}`;
8
+ var columnId = (schema, table, column) => `db:${schema}::${table}.${column}`;
9
+ async function defaultQueryFn(dsn) {
10
+ const { connection } = validateDsn(dsn);
11
+ const mysql = await import("mysql2/promise");
12
+ const conn = await mysql.createConnection({
13
+ host: connection.host,
14
+ port: connection.port,
15
+ user: connection.user,
16
+ password: connection.password,
17
+ database: connection.database
18
+ });
19
+ return {
20
+ query: async (sql, params) => {
21
+ const [rows] = await conn.execute(sql, [...params]);
22
+ return rows;
23
+ },
24
+ close: () => conn.end()
25
+ };
26
+ }
27
+ async function extractMysql(dsn, queryFn) {
28
+ const { safeDisplay, connection } = validateDsn(dsn);
29
+ const schema = connection.database;
30
+ let query = queryFn;
31
+ let close;
32
+ if (!query) {
33
+ const live = await defaultQueryFn(dsn);
34
+ query = live.query;
35
+ close = live.close;
36
+ }
37
+ try {
38
+ const nodes = [];
39
+ const edges = [];
40
+ nodes.push({ id: schemaId(schema), label: schema, sourceFile: safeDisplay, sourceLocation: schema });
41
+ const tableRows = await query(
42
+ "SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
43
+ [schema]
44
+ );
45
+ const tables = tableRows.map((row) => ({
46
+ name: String(row.TABLE_NAME),
47
+ isView: String(row.TABLE_TYPE).toUpperCase() === "VIEW"
48
+ }));
49
+ const tableNames = new Set(tables.map((t) => t.name));
50
+ for (const table of tables) {
51
+ nodes.push({
52
+ id: tableId(schema, table.name),
53
+ label: `${table.isView ? "view" : "table"} ${table.name}`,
54
+ sourceFile: safeDisplay,
55
+ sourceLocation: table.name
56
+ });
57
+ edges.push({
58
+ source: schemaId(schema),
59
+ target: tableId(schema, table.name),
60
+ relation: "contains",
61
+ confidence: "EXTRACTED"
62
+ });
63
+ }
64
+ const columnRows = await query(
65
+ "SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME, ORDINAL_POSITION",
66
+ [schema]
67
+ );
68
+ for (const row of columnRows) {
69
+ const table = String(row.TABLE_NAME);
70
+ const column = String(row.COLUMN_NAME);
71
+ if (!tableNames.has(table)) continue;
72
+ nodes.push({
73
+ id: columnId(schema, table, column),
74
+ label: `${table}.${column}: ${String(row.COLUMN_TYPE)}`,
75
+ sourceFile: safeDisplay,
76
+ sourceLocation: `${table}.${column}`
77
+ });
78
+ edges.push({
79
+ source: tableId(schema, table),
80
+ target: columnId(schema, table, column),
81
+ relation: "contains",
82
+ confidence: "EXTRACTED"
83
+ });
84
+ }
85
+ const fkRows = await query(
86
+ "SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME IS NOT NULL ORDER BY TABLE_NAME, COLUMN_NAME",
87
+ [schema]
88
+ );
89
+ for (const row of fkRows) {
90
+ const table = String(row.TABLE_NAME);
91
+ const column = String(row.COLUMN_NAME);
92
+ const referenced = String(row.REFERENCED_TABLE_NAME);
93
+ if (!tableNames.has(table) || !tableNames.has(referenced)) continue;
94
+ edges.push({
95
+ source: columnId(schema, table, column),
96
+ target: tableId(schema, referenced),
97
+ relation: "references",
98
+ confidence: "EXTRACTED"
99
+ });
100
+ }
101
+ const viewNames = tables.filter((t) => t.isView).map((t) => t.name);
102
+ if (viewNames.length > 0) {
103
+ await addViewEdges(query, schema, viewNames, tableNames, edges);
104
+ }
105
+ return { nodes, edges };
106
+ } finally {
107
+ await close?.();
108
+ }
109
+ }
110
+ async function addViewEdges(query, schema, viewNames, tableNames, edges) {
111
+ try {
112
+ const usageRows = await query(
113
+ "SELECT VIEW_NAME, TABLE_NAME FROM information_schema.VIEW_TABLE_USAGE WHERE VIEW_SCHEMA = ? AND TABLE_SCHEMA = ? ORDER BY VIEW_NAME, TABLE_NAME",
114
+ [schema, schema]
115
+ );
116
+ for (const row of usageRows) {
117
+ const view = String(row.VIEW_NAME);
118
+ const table = String(row.TABLE_NAME);
119
+ if (!tableNames.has(view) || !tableNames.has(table) || view === table) continue;
120
+ edges.push({
121
+ source: tableId(schema, view),
122
+ target: tableId(schema, table),
123
+ relation: "references",
124
+ confidence: "EXTRACTED"
125
+ });
126
+ }
127
+ return;
128
+ } catch {
129
+ }
130
+ const definitionRows = await query(
131
+ "SELECT TABLE_NAME, VIEW_DEFINITION FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
132
+ [schema]
133
+ );
134
+ for (const row of definitionRows) {
135
+ const view = String(row.TABLE_NAME);
136
+ if (!viewNames.includes(view)) continue;
137
+ const definition = String(row.VIEW_DEFINITION ?? "").toLowerCase();
138
+ for (const table of [...tableNames].sort((a, b) => a.localeCompare(b))) {
139
+ if (table === view) continue;
140
+ if (new RegExp(`\\b${table.toLowerCase()}\\b`).test(definition)) {
141
+ edges.push({
142
+ source: tableId(schema, view),
143
+ target: tableId(schema, table),
144
+ relation: "references",
145
+ confidence: "INFERRED"
146
+ });
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ export {
153
+ extractMysql
154
+ };
155
+ //# sourceMappingURL=chunk-ZPB37LLQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/extractors/mysql.ts"],"sourcesContent":["import { validateDsn } from '../security.js';\nimport type { ExtractionResult, GraphEdge, GraphNode } from '../types.js';\n\n/**\n * MySQL schema extraction — introspect information_schema over a\n * user-supplied DSN and map the schema onto the same ExtractionResult shape\n * every code extractor produces, so tables/columns/foreign keys land in the\n * same graph as the code that talks to them.\n *\n * Only ever surfaces the credential-free DSN rendering (see\n * security.validateDsn()) in node attributes; the password never leaves the\n * connection config.\n */\n\n/** One `SELECT` against the live connection. Injectable so tests run on canned rows, mirroring security.ts's LookupFn pattern. */\nexport type DsnQueryFn = (sql: string, params: ReadonlyArray<string>) => Promise<Array<Record<string, unknown>>>;\n\ninterface TableRow {\n name: string;\n isView: boolean;\n}\n\nconst schemaId = (schema: string): string => `db:${schema}`;\nconst tableId = (schema: string, table: string): string => `db:${schema}::${table}`;\nconst columnId = (schema: string, table: string, column: string): string => `db:${schema}::${table}.${column}`;\n\nasync function defaultQueryFn(dsn: string): Promise<{ query: DsnQueryFn; close: () => Promise<void> }> {\n const { connection } = validateDsn(dsn);\n // Dynamic import so merely bundling/importing this module never loads the\n // mysql2 driver — it's only pulled in when a DSN is actually used.\n const mysql = await import('mysql2/promise');\n const conn = await mysql.createConnection({\n host: connection.host,\n port: connection.port,\n user: connection.user,\n password: connection.password,\n database: connection.database,\n });\n return {\n query: async (sql, params) => {\n const [rows] = await conn.execute(sql, [...params]);\n return rows as Array<Record<string, unknown>>;\n },\n close: () => conn.end(),\n };\n}\n\n/**\n * Extract the schema graph for the database named in `dsn`.\n *\n * Nodes: the schema, every table/view, every column.\n * Edges: schema `contains` table, table `contains` column, FK column\n * `references` the referenced table (EXTRACTED), view `references` its base\n * tables (EXTRACTED via VIEW_TABLE_USAGE on MySQL >= 8.0.13, INFERRED via a\n * VIEW_DEFINITION text match on older servers).\n */\nexport async function extractMysql(dsn: string, queryFn?: DsnQueryFn): Promise<ExtractionResult> {\n const { safeDisplay, connection } = validateDsn(dsn);\n const schema = connection.database;\n\n let query = queryFn;\n let close: (() => Promise<void>) | undefined;\n if (!query) {\n const live = await defaultQueryFn(dsn);\n query = live.query;\n close = live.close;\n }\n\n try {\n const nodes: GraphNode[] = [];\n const edges: GraphEdge[] = [];\n\n nodes.push({ id: schemaId(schema), label: schema, sourceFile: safeDisplay, sourceLocation: schema });\n\n const tableRows = await query(\n 'SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME',\n [schema],\n );\n const tables: TableRow[] = tableRows.map((row) => ({\n name: String(row.TABLE_NAME),\n isView: String(row.TABLE_TYPE).toUpperCase() === 'VIEW',\n }));\n const tableNames = new Set(tables.map((t) => t.name));\n\n for (const table of tables) {\n nodes.push({\n id: tableId(schema, table.name),\n label: `${table.isView ? 'view' : 'table'} ${table.name}`,\n sourceFile: safeDisplay,\n sourceLocation: table.name,\n });\n edges.push({\n source: schemaId(schema),\n target: tableId(schema, table.name),\n relation: 'contains',\n confidence: 'EXTRACTED',\n });\n }\n\n const columnRows = await query(\n 'SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE FROM information_schema.COLUMNS ' +\n 'WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME, ORDINAL_POSITION',\n [schema],\n );\n for (const row of columnRows) {\n const table = String(row.TABLE_NAME);\n const column = String(row.COLUMN_NAME);\n if (!tableNames.has(table)) continue;\n nodes.push({\n id: columnId(schema, table, column),\n label: `${table}.${column}: ${String(row.COLUMN_TYPE)}`,\n sourceFile: safeDisplay,\n sourceLocation: `${table}.${column}`,\n });\n edges.push({\n source: tableId(schema, table),\n target: columnId(schema, table, column),\n relation: 'contains',\n confidence: 'EXTRACTED',\n });\n }\n\n const fkRows = await query(\n 'SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME FROM information_schema.KEY_COLUMN_USAGE ' +\n 'WHERE TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME IS NOT NULL ORDER BY TABLE_NAME, COLUMN_NAME',\n [schema],\n );\n for (const row of fkRows) {\n const table = String(row.TABLE_NAME);\n const column = String(row.COLUMN_NAME);\n const referenced = String(row.REFERENCED_TABLE_NAME);\n if (!tableNames.has(table) || !tableNames.has(referenced)) continue;\n edges.push({\n source: columnId(schema, table, column),\n target: tableId(schema, referenced),\n relation: 'references',\n confidence: 'EXTRACTED',\n });\n }\n\n const viewNames = tables.filter((t) => t.isView).map((t) => t.name);\n if (viewNames.length > 0) {\n await addViewEdges(query, schema, viewNames, tableNames, edges);\n }\n\n return { nodes, edges };\n } finally {\n await close?.();\n }\n}\n\n/** View -> base-table edges: VIEW_TABLE_USAGE when the server has it (MySQL >= 8.0.13), else an INFERRED VIEW_DEFINITION text match. */\nasync function addViewEdges(\n query: DsnQueryFn,\n schema: string,\n viewNames: string[],\n tableNames: ReadonlySet<string>,\n edges: GraphEdge[],\n): Promise<void> {\n try {\n const usageRows = await query(\n 'SELECT VIEW_NAME, TABLE_NAME FROM information_schema.VIEW_TABLE_USAGE ' +\n 'WHERE VIEW_SCHEMA = ? AND TABLE_SCHEMA = ? ORDER BY VIEW_NAME, TABLE_NAME',\n [schema, schema],\n );\n for (const row of usageRows) {\n const view = String(row.VIEW_NAME);\n const table = String(row.TABLE_NAME);\n if (!tableNames.has(view) || !tableNames.has(table) || view === table) continue;\n edges.push({\n source: tableId(schema, view),\n target: tableId(schema, table),\n relation: 'references',\n confidence: 'EXTRACTED',\n });\n }\n return;\n } catch {\n // Server predates VIEW_TABLE_USAGE — fall back to matching table names\n // inside the view definition text, honestly downgraded to INFERRED.\n }\n\n const definitionRows = await query(\n 'SELECT TABLE_NAME, VIEW_DEFINITION FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME',\n [schema],\n );\n for (const row of definitionRows) {\n const view = String(row.TABLE_NAME);\n if (!viewNames.includes(view)) continue;\n const definition = String(row.VIEW_DEFINITION ?? '').toLowerCase();\n for (const table of [...tableNames].sort((a, b) => a.localeCompare(b))) {\n if (table === view) continue;\n if (new RegExp(`\\\\b${table.toLowerCase()}\\\\b`).test(definition)) {\n edges.push({\n source: tableId(schema, view),\n target: tableId(schema, table),\n relation: 'references',\n confidence: 'INFERRED',\n });\n }\n }\n }\n}\n"],"mappings":"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","names":[]}