sql-kite 1.0.7 → 1.0.8

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.
Files changed (88) hide show
  1. package/package.json +11 -3
  2. package/server/db/connections.js +114 -0
  3. package/server/db/meta-db.js +0 -0
  4. package/server/db/user-db.js +0 -0
  5. package/server/index.js +214 -0
  6. package/server/routes/branches.js +484 -0
  7. package/server/routes/compare.js +109 -0
  8. package/server/routes/export.js +201 -0
  9. package/server/routes/import.js +375 -0
  10. package/server/routes/migrations.js +332 -0
  11. package/server/routes/query.js +67 -0
  12. package/server/routes/schema.js +206 -0
  13. package/server/routes/snapshots.js +322 -0
  14. package/server/routes/tables.js +121 -0
  15. package/server/routes/timeline.js +108 -0
  16. package/server/server.js +0 -0
  17. package/src/commands/import-server.js +2 -5
  18. package/src/commands/import.js +5 -9
  19. package/src/commands/start.js +6 -7
  20. package/src/commands/stop.js +7 -2
  21. package/src/utils/paths.js +61 -1
  22. package/studio-out/404/index.html +1 -0
  23. package/studio-out/404.html +1 -0
  24. package/studio-out/__next.__PAGE__.txt +10 -0
  25. package/studio-out/__next._full.txt +20 -0
  26. package/studio-out/__next._head.txt +6 -0
  27. package/studio-out/__next._index.txt +6 -0
  28. package/studio-out/__next._tree.txt +3 -0
  29. package/studio-out/_next/static/LhecVBdPttfi1VZfXA-dL/_buildManifest.js +11 -0
  30. package/studio-out/_next/static/LhecVBdPttfi1VZfXA-dL/_clientMiddlewareManifest.json +1 -0
  31. package/studio-out/_next/static/LhecVBdPttfi1VZfXA-dL/_ssgManifest.js +1 -0
  32. package/studio-out/_next/static/chunks/118fc599da2f27aa.css +2 -0
  33. package/studio-out/_next/static/chunks/240f2fa81d4fb687.js +1 -0
  34. package/studio-out/_next/static/chunks/42c33ca704af9b68.js +1 -0
  35. package/studio-out/_next/static/chunks/99b69e65b599be96.js +5 -0
  36. package/studio-out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  37. package/studio-out/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
  38. package/studio-out/_next/static/chunks/b20313408e970968.css +1 -0
  39. package/studio-out/_next/static/chunks/d104f42a7b0c57b2.js +2 -0
  40. package/studio-out/_next/static/chunks/d4aa9be9c80c98d6.js +1 -0
  41. package/studio-out/_next/static/chunks/f2f58a7e93290fbb.js +1 -0
  42. package/studio-out/_next/static/chunks/f547e106c8e2aa8e.js +1 -0
  43. package/studio-out/_next/static/chunks/f5cb054219e2eeb8.js +109 -0
  44. package/studio-out/_next/static/chunks/turbopack-1577480078e795df.js +4 -0
  45. package/studio-out/_not-found/__next._full.txt +15 -0
  46. package/studio-out/_not-found/__next._head.txt +6 -0
  47. package/studio-out/_not-found/__next._index.txt +6 -0
  48. package/studio-out/_not-found/__next._not-found/__PAGE__.txt +5 -0
  49. package/studio-out/_not-found/__next._not-found.txt +4 -0
  50. package/studio-out/_not-found/__next._tree.txt +2 -0
  51. package/studio-out/_not-found/index.html +1 -0
  52. package/studio-out/_not-found/index.txt +15 -0
  53. package/studio-out/favicon.ico +10 -0
  54. package/studio-out/index.html +37 -0
  55. package/studio-out/index.txt +20 -0
  56. package/studio-out/logo.svg +5 -0
  57. package/studio-out/snapshots/__next._full.txt +15 -0
  58. package/studio-out/snapshots/__next._head.txt +6 -0
  59. package/studio-out/snapshots/__next._index.txt +6 -0
  60. package/studio-out/snapshots/__next._tree.txt +2 -0
  61. package/studio-out/snapshots/__next.snapshots/__PAGE__.txt +5 -0
  62. package/studio-out/snapshots/__next.snapshots.txt +4 -0
  63. package/studio-out/snapshots/index.html +1 -0
  64. package/studio-out/snapshots/index.txt +15 -0
  65. package/studio-out/sql/__next._full.txt +15 -0
  66. package/studio-out/sql/__next._head.txt +6 -0
  67. package/studio-out/sql/__next._index.txt +6 -0
  68. package/studio-out/sql/__next._tree.txt +2 -0
  69. package/studio-out/sql/__next.sql/__PAGE__.txt +5 -0
  70. package/studio-out/sql/__next.sql.txt +4 -0
  71. package/studio-out/sql/index.html +1 -0
  72. package/studio-out/sql/index.txt +15 -0
  73. package/studio-out/tables/__next._full.txt +15 -0
  74. package/studio-out/tables/__next._head.txt +6 -0
  75. package/studio-out/tables/__next._index.txt +6 -0
  76. package/studio-out/tables/__next._tree.txt +2 -0
  77. package/studio-out/tables/__next.tables/__PAGE__.txt +5 -0
  78. package/studio-out/tables/__next.tables.txt +4 -0
  79. package/studio-out/tables/index.html +1 -0
  80. package/studio-out/tables/index.txt +15 -0
  81. package/studio-out/timeline/__next._full.txt +15 -0
  82. package/studio-out/timeline/__next._head.txt +6 -0
  83. package/studio-out/timeline/__next._index.txt +6 -0
  84. package/studio-out/timeline/__next._tree.txt +2 -0
  85. package/studio-out/timeline/__next.timeline/__PAGE__.txt +5 -0
  86. package/studio-out/timeline/__next.timeline.txt +4 -0
  87. package/studio-out/timeline/index.html +1 -0
  88. package/studio-out/timeline/index.txt +15 -0
@@ -0,0 +1,332 @@
1
+ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs';
2
+ import { join } from 'path';
3
+ import crypto from 'crypto';
4
+
5
+ export default async function migrationsRoutes(fastify, options) {
6
+ // List all migrations
7
+ fastify.get('/', async (request, reply) => {
8
+ const migrationsPath = join(fastify.projectPath, 'migrations');
9
+ const metaDb = fastify.getMetaDb();
10
+ const currentBranch = fastify.getCurrentBranch();
11
+
12
+ try {
13
+ // Ensure migrations directory exists
14
+ if (!existsSync(migrationsPath)) {
15
+ return [];
16
+ }
17
+
18
+ const files = readdirSync(migrationsPath).filter(f => f.endsWith('.sql')).sort();
19
+
20
+ const applied = metaDb.prepare(`
21
+ SELECT filename, applied_at FROM migrations WHERE branch = ? ORDER BY id
22
+ `).all(currentBranch);
23
+
24
+ const appliedMap = new Map(applied.map(m => [m.filename, m.applied_at]));
25
+
26
+ return files.map(filename => {
27
+ const filePath = join(migrationsPath, filename);
28
+ const content = readFileSync(filePath, 'utf-8');
29
+ const stats = statSync(filePath);
30
+ const checksum = crypto.createHash('sha256').update(content).digest('hex');
31
+ const appliedAt = appliedMap.get(filename);
32
+
33
+ return {
34
+ filename,
35
+ applied: appliedMap.has(filename),
36
+ content,
37
+ branch: currentBranch,
38
+ created_at: stats.birthtime.toISOString(),
39
+ applied_at: appliedAt || null,
40
+ checksum,
41
+ author: 'SQL Kite'
42
+ };
43
+ });
44
+ } catch (error) {
45
+ reply.code(500).send({ error: error.message });
46
+ }
47
+ });
48
+
49
+ // Create a new migration file
50
+ fastify.post('/create', async (request, reply) => {
51
+ const { name, sql } = request.body;
52
+ const migrationsPath = join(fastify.projectPath, 'migrations');
53
+ const metaDb = fastify.getMetaDb();
54
+ const currentBranch = fastify.getCurrentBranch();
55
+
56
+ if (!name || !sql) {
57
+ return reply.code(400).send({ error: 'Name and SQL are required' });
58
+ }
59
+
60
+ try {
61
+ // Ensure migrations directory exists
62
+ if (!existsSync(migrationsPath)) {
63
+ mkdirSync(migrationsPath, { recursive: true });
64
+ }
65
+
66
+ // Get next migration number
67
+ const existingFiles = readdirSync(migrationsPath).filter(f => f.endsWith('.sql'));
68
+ const numbers = existingFiles
69
+ .map(f => parseInt(f.split('_')[0]))
70
+ .filter(n => !isNaN(n));
71
+ const nextNum = numbers.length > 0 ? Math.max(...numbers) + 1 : 1;
72
+
73
+ // Create filename
74
+ const filename = `${String(nextNum).padStart(3, '0')}_${name}.sql`;
75
+ const filePath = join(migrationsPath, filename);
76
+
77
+ // Write migration file
78
+ writeFileSync(filePath, sql, 'utf-8');
79
+
80
+ // Log event in current branch
81
+ metaDb.prepare(`
82
+ INSERT INTO events (branch, type, data)
83
+ VALUES (?, 'migration_created', ?)
84
+ `).run(currentBranch, JSON.stringify({ filename, name }));
85
+
86
+ return {
87
+ success: true,
88
+ filename,
89
+ path: filePath
90
+ };
91
+ } catch (error) {
92
+ reply.code(500).send({ error: error.message });
93
+ }
94
+ });
95
+
96
+ // Apply migration
97
+ fastify.post('/apply', async (request, reply) => {
98
+ const { filename } = request.body;
99
+ const migrationsPath = join(fastify.projectPath, 'migrations');
100
+ const db = fastify.getUserDb();
101
+ const metaDb = fastify.getMetaDb();
102
+ const currentBranch = fastify.getCurrentBranch();
103
+
104
+ try {
105
+ // Check if already applied in this branch
106
+ const existing = metaDb.prepare(`
107
+ SELECT id FROM migrations WHERE branch = ? AND filename = ?
108
+ `).get(currentBranch, filename);
109
+
110
+ if (existing) {
111
+ return reply.code(400).send({ error: 'Migration already applied in this branch' });
112
+ }
113
+
114
+ // Read and execute migration
115
+ const sql = readFileSync(join(migrationsPath, filename), 'utf-8');
116
+ db.exec(sql);
117
+
118
+ // Mark as applied in this branch
119
+ metaDb.prepare(`
120
+ INSERT INTO migrations (branch, filename) VALUES (?, ?)
121
+ `).run(currentBranch, filename);
122
+
123
+ // Log event in this branch
124
+ metaDb.prepare(`
125
+ INSERT INTO events (branch, type, data)
126
+ VALUES (?, 'migration_applied', ?)
127
+ `).run(currentBranch, JSON.stringify({ filename }));
128
+
129
+ return { success: true, branch: currentBranch };
130
+ } catch (error) {
131
+ reply.code(500).send({ error: error.message });
132
+ }
133
+ });
134
+
135
+ // Apply all pending migrations
136
+ fastify.post('/apply-all', async (request, reply) => {
137
+ const migrationsPath = join(fastify.projectPath, 'migrations');
138
+ const db = fastify.getUserDb();
139
+ const metaDb = fastify.getMetaDb();
140
+ const currentBranch = fastify.getCurrentBranch();
141
+
142
+ try {
143
+ const allFiles = readdirSync(migrationsPath).filter(f => f.endsWith('.sql')).sort();
144
+
145
+ const applied = metaDb.prepare(`
146
+ SELECT filename FROM migrations WHERE branch = ?
147
+ `).all(currentBranch);
148
+
149
+ const appliedSet = new Set(applied.map(m => m.filename));
150
+ const pendingFiles = allFiles.filter(f => !appliedSet.has(f));
151
+
152
+ const results = [];
153
+
154
+ for (const filename of pendingFiles) {
155
+ try {
156
+ const sql = readFileSync(join(migrationsPath, filename), 'utf-8');
157
+ db.exec(sql);
158
+
159
+ metaDb.prepare(`
160
+ INSERT INTO migrations (branch, filename) VALUES (?, ?)
161
+ `).run(currentBranch, filename);
162
+
163
+ metaDb.prepare(`
164
+ INSERT INTO events (branch, type, data)
165
+ VALUES (?, 'migration_applied', ?)
166
+ `).run(currentBranch, JSON.stringify({ filename }));
167
+
168
+ results.push({ filename, success: true });
169
+ } catch (error) {
170
+ results.push({ filename, success: false, error: error.message });
171
+ // Stop on first error
172
+ break;
173
+ }
174
+ }
175
+
176
+ return {
177
+ success: true,
178
+ branch: currentBranch,
179
+ applied: results
180
+ };
181
+ } catch (error) {
182
+ reply.code(500).send({ error: error.message });
183
+ }
184
+ });
185
+
186
+ // Delete migration (only if NEVER applied in ANY branch)
187
+ fastify.delete('/:filename', async (request, reply) => {
188
+ const { filename } = request.params;
189
+ const migrationsPath = join(fastify.projectPath, 'migrations');
190
+ const metaDb = fastify.getMetaDb();
191
+ const currentBranch = fastify.getCurrentBranch();
192
+
193
+ try {
194
+ // CRITICAL: Check if migration was applied in ANY branch (not just current)
195
+ const appliedInAnyBranch = metaDb.prepare(`
196
+ SELECT branch FROM migrations WHERE filename = ? LIMIT 1
197
+ `).get(filename);
198
+
199
+ if (appliedInAnyBranch) {
200
+ return reply.code(400).send({
201
+ error: `Migration cannot be deleted. It has been applied in branch "${appliedInAnyBranch.branch}".`,
202
+ applied_in_branch: appliedInAnyBranch.branch,
203
+ reason: 'APPLIED_IN_BRANCH'
204
+ });
205
+ }
206
+
207
+ // Migration has never been applied - safe to delete
208
+ const filePath = join(migrationsPath, filename);
209
+
210
+ if (!existsSync(filePath)) {
211
+ return reply.code(404).send({ error: 'Migration file not found' });
212
+ }
213
+
214
+ // Delete the file
215
+ const { unlinkSync } = await import('fs');
216
+ unlinkSync(filePath);
217
+
218
+ // Log deletion event
219
+ metaDb.prepare(`
220
+ INSERT INTO events (branch, type, data)
221
+ VALUES (?, 'migration_deleted', ?)
222
+ `).run(currentBranch, JSON.stringify({ filename }));
223
+
224
+ return {
225
+ success: true,
226
+ filename,
227
+ message: 'Migration deleted successfully'
228
+ };
229
+ } catch (error) {
230
+ reply.code(500).send({ error: error.message });
231
+ }
232
+ });
233
+
234
+ // Get migration application status across ALL branches
235
+ fastify.get('/:filename/status', async (request, reply) => {
236
+ const { filename } = request.params;
237
+ const metaDb = fastify.getMetaDb();
238
+
239
+ try {
240
+ const applications = metaDb.prepare(`
241
+ SELECT branch, applied_at
242
+ FROM migrations
243
+ WHERE filename = ?
244
+ ORDER BY applied_at DESC
245
+ `).all(filename);
246
+
247
+ return {
248
+ filename,
249
+ applied_in_branches: applications.map(a => a.branch),
250
+ can_delete: applications.length === 0,
251
+ applications
252
+ };
253
+ } catch (error) {
254
+ reply.code(500).send({ error: error.message });
255
+ }
256
+ });
257
+
258
+ // Export single migration file
259
+ fastify.get('/:filename/export', async (request, reply) => {
260
+ const { filename } = request.params;
261
+ const migrationsPath = join(fastify.projectPath, 'migrations');
262
+
263
+ try {
264
+ const filePath = join(migrationsPath, filename);
265
+
266
+ if (!existsSync(filePath)) {
267
+ return reply.code(404).send({ error: 'Migration file not found' });
268
+ }
269
+
270
+ const content = readFileSync(filePath, 'utf-8');
271
+
272
+ // Set headers for file download
273
+ reply.header('Content-Type', 'text/plain');
274
+ reply.header('Content-Disposition', `attachment; filename="${filename}"`);
275
+
276
+ return content;
277
+ } catch (error) {
278
+ reply.code(500).send({ error: error.message });
279
+ }
280
+ });
281
+
282
+ // Export all applied migrations for current branch (ordered)
283
+ fastify.get('/export/applied', async (request, reply) => {
284
+ const metaDb = fastify.getMetaDb();
285
+ const currentBranch = fastify.getCurrentBranch();
286
+ const migrationsPath = join(fastify.projectPath, 'migrations');
287
+
288
+ try {
289
+ // Get applied migrations in order
290
+ const applied = metaDb.prepare(`
291
+ SELECT filename, applied_at
292
+ FROM migrations
293
+ WHERE branch = ?
294
+ ORDER BY id
295
+ `).all(currentBranch);
296
+
297
+ if (applied.length === 0) {
298
+ return reply.code(400).send({
299
+ error: 'No migrations have been applied in this branch'
300
+ });
301
+ }
302
+
303
+ // Build combined SQL file
304
+ let combinedSql = `-- Applied migrations for branch: ${currentBranch}\n`;
305
+ combinedSql += `-- Generated: ${new Date().toISOString()}\n`;
306
+ combinedSql += `-- Total migrations: ${applied.length}\n\n`;
307
+
308
+ for (const migration of applied) {
309
+ const filePath = join(migrationsPath, migration.filename);
310
+
311
+ if (existsSync(filePath)) {
312
+ const content = readFileSync(filePath, 'utf-8');
313
+ combinedSql += `-- ========================================\n`;
314
+ combinedSql += `-- Migration: ${migration.filename}\n`;
315
+ combinedSql += `-- Applied: ${migration.applied_at}\n`;
316
+ combinedSql += `-- ========================================\n\n`;
317
+ combinedSql += content;
318
+ combinedSql += `\n\n`;
319
+ }
320
+ }
321
+
322
+ const filename = `${fastify.projectPath.split(/[\\/]/).pop()}_${currentBranch}_migrations.sql`;
323
+
324
+ reply.header('Content-Type', 'text/plain');
325
+ reply.header('Content-Disposition', `attachment; filename="${filename}"`);
326
+
327
+ return combinedSql;
328
+ } catch (error) {
329
+ reply.code(500).send({ error: error.message });
330
+ }
331
+ });
332
+ }
@@ -0,0 +1,67 @@
1
+ export default async function queryRoutes(fastify, options) {
2
+ // Execute SQL query
3
+ fastify.post('/', async (request, reply) => {
4
+ const { sql } = request.body;
5
+ const db = fastify.getUserDb();
6
+ const metaDb = fastify.getMetaDb();
7
+
8
+ if (!sql || sql.trim() === '') {
9
+ return reply.code(400).send({ error: 'SQL query is required' });
10
+ }
11
+
12
+ const startTime = Date.now();
13
+
14
+ try {
15
+ const trimmedSql = sql.trim().toUpperCase();
16
+ let result;
17
+
18
+ // Determine query type
19
+ if (trimmedSql.startsWith('SELECT') || trimmedSql.startsWith('PRAGMA')) {
20
+ // Read query
21
+ result = {
22
+ type: 'select',
23
+ rows: db.prepare(sql).all(),
24
+ executionTime: Date.now() - startTime
25
+ };
26
+ } else {
27
+ // Write query (INSERT, UPDATE, DELETE, CREATE, etc.)
28
+ const info = db.prepare(sql).run();
29
+ result = {
30
+ type: 'write',
31
+ changes: info.changes,
32
+ lastInsertRowid: info.lastInsertRowid,
33
+ executionTime: Date.now() - startTime
34
+ };
35
+ }
36
+
37
+ // Log to timeline
38
+ metaDb.prepare(`
39
+ INSERT INTO events (type, data)
40
+ VALUES ('sql_executed', ?)
41
+ `).run(JSON.stringify({
42
+ sql,
43
+ type: result.type,
44
+ executionTime: result.executionTime,
45
+ changes: result.changes,
46
+ rowCount: result.rows?.length
47
+ }));
48
+
49
+ return result;
50
+ } catch (error) {
51
+ // Log error
52
+ metaDb.prepare(`
53
+ INSERT INTO events (type, data)
54
+ VALUES ('sql_error', ?)
55
+ `).run(JSON.stringify({
56
+ sql,
57
+ error: error.message,
58
+ executionTime: Date.now() - startTime
59
+ }));
60
+
61
+ return reply.code(400).send({
62
+ error: error.message,
63
+ sql
64
+ });
65
+ }
66
+ });
67
+ }
@@ -0,0 +1,206 @@
1
+ export default async function schemaRoutes(fastify, options) {
2
+ // Get full database schema
3
+ fastify.get('/', async (request, reply) => {
4
+ const db = fastify.getUserDb();
5
+
6
+ const schema = db.prepare(`
7
+ SELECT type, name, sql
8
+ FROM sqlite_master
9
+ WHERE sql NOT NULL
10
+ AND name NOT LIKE 'sqlite_%'
11
+ AND name NOT LIKE '_studio_%'
12
+ ORDER BY type, name
13
+ `).all();
14
+
15
+ return {
16
+ tables: schema.filter(s => s.type === 'table'),
17
+ indexes: schema.filter(s => s.type === 'index'),
18
+ views: schema.filter(s => s.type === 'view'),
19
+ triggers: schema.filter(s => s.type === 'trigger')
20
+ };
21
+ });
22
+
23
+ // Get foreign keys for a table
24
+ fastify.get('/foreign-keys/:tableName', async (request, reply) => {
25
+ const { tableName } = request.params;
26
+ const db = fastify.getUserDb();
27
+
28
+ try {
29
+ const foreignKeys = db.prepare(`PRAGMA foreign_key_list(${tableName})`).all();
30
+ return foreignKeys;
31
+ } catch (error) {
32
+ reply.code(500).send({ error: error.message });
33
+ }
34
+ });
35
+
36
+ // Get indexes for a table
37
+ fastify.get('/indexes/:tableName', async (request, reply) => {
38
+ const { tableName } = request.params;
39
+ const db = fastify.getUserDb();
40
+
41
+ try {
42
+ const indexes = db.prepare(`PRAGMA index_list(${tableName})`).all();
43
+ return indexes;
44
+ } catch (error) {
45
+ reply.code(500).send({ error: error.message });
46
+ }
47
+ });
48
+
49
+ // Export full schema as SQL (state-based, not history)
50
+ fastify.get('/export', async (request, reply) => {
51
+ const db = fastify.getUserDb();
52
+ const currentBranch = fastify.getCurrentBranch();
53
+
54
+ try {
55
+ // Get all schema objects
56
+ const schema = db.prepare(`
57
+ SELECT type, name, sql
58
+ FROM sqlite_master
59
+ WHERE sql NOT NULL
60
+ AND name NOT LIKE 'sqlite_%'
61
+ AND name NOT LIKE '_studio_%'
62
+ ORDER BY
63
+ CASE type
64
+ WHEN 'table' THEN 1
65
+ WHEN 'index' THEN 2
66
+ WHEN 'view' THEN 3
67
+ WHEN 'trigger' THEN 4
68
+ ELSE 5
69
+ END,
70
+ name
71
+ `).all();
72
+
73
+ // Build SQL export
74
+ let exportSql = `-- Full schema export\\n`;
75
+ exportSql += `-- Branch: ${currentBranch}\\n`;
76
+ exportSql += `-- Generated: ${new Date().toISOString()}\\n`;
77
+ exportSql += `-- \\n`;
78
+ exportSql += `-- This is current database state, not migration history.\\n`;
79
+ exportSql += `\\n`;
80
+
81
+ let currentType = null;
82
+
83
+ for (const obj of schema) {
84
+ // Add section headers
85
+ if (obj.type !== currentType) {
86
+ currentType = obj.type;
87
+ exportSql += `\\n-- ========================================\\n`;
88
+ exportSql += `-- ${obj.type.toUpperCase()}S\\n`;
89
+ exportSql += `-- ========================================\\n\\n`;
90
+ }
91
+
92
+ exportSql += `-- ${obj.name}\\n`;
93
+ exportSql += obj.sql + ';\\n\\n';
94
+ }
95
+
96
+ const projectName = fastify.projectName;
97
+ const filename = `${projectName}_${currentBranch}_schema.sql`;
98
+
99
+ reply.header('Content-Type', 'text/plain');
100
+ reply.header('Content-Disposition', `attachment; filename=\"${filename}\"`);
101
+
102
+ return exportSql;
103
+ } catch (error) {
104
+ reply.code(500).send({ error: error.message });
105
+ }
106
+ });
107
+
108
+ // Get ER diagram data (tables + columns + foreign key relations)
109
+ fastify.get('/er', async (request, reply) => {
110
+ const db = fastify.getUserDb();
111
+
112
+ try {
113
+ const inferRelations = String(request.query?.infer || '').toLowerCase() === 'true'
114
+ // Get all tables (excluding SQLite internal tables)
115
+ const tables = db.prepare(`
116
+ SELECT name
117
+ FROM sqlite_master
118
+ WHERE type='table'
119
+ AND name NOT LIKE 'sqlite_%'
120
+ AND name NOT LIKE '_studio_%'
121
+ ORDER BY name
122
+ `).all();
123
+
124
+ const tablesWithColumns = [];
125
+ const relations = [];
126
+ const primaryKeysByTable = {};
127
+
128
+ for (const table of tables) {
129
+ // Get column info
130
+ const columns = db.prepare(`PRAGMA table_info(${table.name})`).all();
131
+
132
+ // Get foreign keys
133
+ const foreignKeys = db.prepare(`PRAGMA foreign_key_list(${table.name})`).all();
134
+
135
+ // Map columns to our format
136
+ const formattedColumns = columns.map(col => ({
137
+ name: col.name,
138
+ type: col.type,
139
+ pk: col.pk === 1,
140
+ notnull: col.notnull === 1,
141
+ dflt_value: col.dflt_value
142
+ }));
143
+
144
+ tablesWithColumns.push({
145
+ name: table.name,
146
+ columns: formattedColumns
147
+ });
148
+
149
+ primaryKeysByTable[table.name] = formattedColumns
150
+ .filter(col => col.pk)
151
+ .map(col => col.name);
152
+
153
+ // Map foreign keys to relations
154
+ for (const fk of foreignKeys) {
155
+ relations.push({
156
+ from: `${table.name}.${fk.from}`,
157
+ to: `${fk.table}.${fk.to}`,
158
+ fromTable: table.name,
159
+ toTable: fk.table,
160
+ fromColumn: fk.from,
161
+ toColumn: fk.to
162
+ });
163
+ }
164
+ }
165
+
166
+ const relationKeySet = new Set(
167
+ relations.map(rel => `${rel.fromTable}|${rel.fromColumn}|${rel.toTable}|${rel.toColumn}`)
168
+ );
169
+
170
+ if (inferRelations || relations.length === 0) {
171
+ for (const table of tablesWithColumns) {
172
+ for (const column of table.columns) {
173
+ if (column.pk) continue;
174
+
175
+ for (const [targetTable, pkColumns] of Object.entries(primaryKeysByTable)) {
176
+ if (targetTable === table.name) continue;
177
+
178
+ if (pkColumns.includes(column.name)) {
179
+ const key = `${table.name}|${column.name}|${targetTable}|${column.name}`;
180
+ if (relationKeySet.has(key)) continue;
181
+
182
+ relations.push({
183
+ from: `${table.name}.${column.name}`,
184
+ to: `${targetTable}.${column.name}`,
185
+ fromTable: table.name,
186
+ toTable: targetTable,
187
+ fromColumn: column.name,
188
+ toColumn: column.name,
189
+ isInferred: true
190
+ });
191
+ relationKeySet.add(key);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ return {
199
+ tables: tablesWithColumns,
200
+ relations
201
+ };
202
+ } catch (error) {
203
+ reply.code(500).send({ error: error.message });
204
+ }
205
+ });
206
+ }