sql-kite 1.0.7 → 1.0.9
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/package.json +14 -4
- package/server/db/connections.js +114 -0
- package/server/db/meta-db.js +0 -0
- package/server/db/user-db.js +0 -0
- package/server/index.js +214 -0
- package/server/routes/branches.js +484 -0
- package/server/routes/compare.js +109 -0
- package/server/routes/export.js +201 -0
- package/server/routes/import.js +375 -0
- package/server/routes/migrations.js +332 -0
- package/server/routes/query.js +67 -0
- package/server/routes/schema.js +206 -0
- package/server/routes/snapshots.js +322 -0
- package/server/routes/tables.js +121 -0
- package/server/routes/timeline.js +108 -0
- package/server/server.js +0 -0
- package/src/commands/import-server.js +2 -5
- package/src/commands/import.js +5 -9
- package/src/commands/start.js +6 -7
- package/src/commands/stop.js +7 -2
- package/src/utils/paths.js +61 -1
- package/studio-out/404/index.html +1 -0
- package/studio-out/404.html +1 -0
- package/studio-out/__next.__PAGE__.txt +10 -0
- package/studio-out/__next._full.txt +20 -0
- package/studio-out/__next._head.txt +6 -0
- package/studio-out/__next._index.txt +6 -0
- package/studio-out/__next._tree.txt +3 -0
- package/studio-out/_next/static/LhecVBdPttfi1VZfXA-dL/_buildManifest.js +11 -0
- package/studio-out/_next/static/LhecVBdPttfi1VZfXA-dL/_clientMiddlewareManifest.json +1 -0
- package/studio-out/_next/static/LhecVBdPttfi1VZfXA-dL/_ssgManifest.js +1 -0
- package/studio-out/_next/static/chunks/118fc599da2f27aa.css +2 -0
- package/studio-out/_next/static/chunks/240f2fa81d4fb687.js +1 -0
- package/studio-out/_next/static/chunks/42c33ca704af9b68.js +1 -0
- package/studio-out/_next/static/chunks/99b69e65b599be96.js +5 -0
- package/studio-out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- package/studio-out/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
- package/studio-out/_next/static/chunks/b20313408e970968.css +1 -0
- package/studio-out/_next/static/chunks/d104f42a7b0c57b2.js +2 -0
- package/studio-out/_next/static/chunks/d4aa9be9c80c98d6.js +1 -0
- package/studio-out/_next/static/chunks/f2f58a7e93290fbb.js +1 -0
- package/studio-out/_next/static/chunks/f547e106c8e2aa8e.js +1 -0
- package/studio-out/_next/static/chunks/f5cb054219e2eeb8.js +109 -0
- package/studio-out/_next/static/chunks/turbopack-1577480078e795df.js +4 -0
- package/studio-out/_not-found/__next._full.txt +15 -0
- package/studio-out/_not-found/__next._head.txt +6 -0
- package/studio-out/_not-found/__next._index.txt +6 -0
- package/studio-out/_not-found/__next._not-found/__PAGE__.txt +5 -0
- package/studio-out/_not-found/__next._not-found.txt +4 -0
- package/studio-out/_not-found/__next._tree.txt +2 -0
- package/studio-out/_not-found/index.html +1 -0
- package/studio-out/_not-found/index.txt +15 -0
- package/studio-out/favicon.ico +10 -0
- package/studio-out/index.html +37 -0
- package/studio-out/index.txt +20 -0
- package/studio-out/logo.svg +5 -0
- package/studio-out/snapshots/__next._full.txt +15 -0
- package/studio-out/snapshots/__next._head.txt +6 -0
- package/studio-out/snapshots/__next._index.txt +6 -0
- package/studio-out/snapshots/__next._tree.txt +2 -0
- package/studio-out/snapshots/__next.snapshots/__PAGE__.txt +5 -0
- package/studio-out/snapshots/__next.snapshots.txt +4 -0
- package/studio-out/snapshots/index.html +1 -0
- package/studio-out/snapshots/index.txt +15 -0
- package/studio-out/sql/__next._full.txt +15 -0
- package/studio-out/sql/__next._head.txt +6 -0
- package/studio-out/sql/__next._index.txt +6 -0
- package/studio-out/sql/__next._tree.txt +2 -0
- package/studio-out/sql/__next.sql/__PAGE__.txt +5 -0
- package/studio-out/sql/__next.sql.txt +4 -0
- package/studio-out/sql/index.html +1 -0
- package/studio-out/sql/index.txt +15 -0
- package/studio-out/tables/__next._full.txt +15 -0
- package/studio-out/tables/__next._head.txt +6 -0
- package/studio-out/tables/__next._index.txt +6 -0
- package/studio-out/tables/__next._tree.txt +2 -0
- package/studio-out/tables/__next.tables/__PAGE__.txt +5 -0
- package/studio-out/tables/__next.tables.txt +4 -0
- package/studio-out/tables/index.html +1 -0
- package/studio-out/tables/index.txt +15 -0
- package/studio-out/timeline/__next._full.txt +15 -0
- package/studio-out/timeline/__next._head.txt +6 -0
- package/studio-out/timeline/__next._index.txt +6 -0
- package/studio-out/timeline/__next._tree.txt +2 -0
- package/studio-out/timeline/__next.timeline/__PAGE__.txt +5 -0
- package/studio-out/timeline/__next.timeline.txt +4 -0
- package/studio-out/timeline/index.html +1 -0
- 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
|
+
}
|