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.
- package/package.json +11 -3
- 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,322 @@
|
|
|
1
|
+
import { copyFileSync, statSync, existsSync, mkdirSync, unlinkSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { closeBranchConnection } from '../db/connections.js';
|
|
4
|
+
|
|
5
|
+
export default async function snapshotsRoutes(fastify, options) {
|
|
6
|
+
/**
|
|
7
|
+
* List snapshots for current branch
|
|
8
|
+
*/
|
|
9
|
+
fastify.get('/', async (request, reply) => {
|
|
10
|
+
const snapshotsPath = join(fastify.projectPath, '.studio', 'snapshots');
|
|
11
|
+
const metaDb = fastify.getMetaDb();
|
|
12
|
+
const currentBranch = fastify.getCurrentBranch();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Ensure snapshots directory exists
|
|
16
|
+
if (!existsSync(snapshotsPath)) {
|
|
17
|
+
mkdirSync(snapshotsPath, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Get snapshots for current branch only
|
|
21
|
+
const snapshots = metaDb.prepare(`
|
|
22
|
+
SELECT * FROM snapshots
|
|
23
|
+
WHERE branch = ?
|
|
24
|
+
ORDER BY id DESC
|
|
25
|
+
`).all(currentBranch);
|
|
26
|
+
|
|
27
|
+
return snapshots.map(snapshot => {
|
|
28
|
+
const filePath = join(snapshotsPath, snapshot.filename);
|
|
29
|
+
let exists = false;
|
|
30
|
+
let size = snapshot.size;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const stats = statSync(filePath);
|
|
34
|
+
exists = stats.isFile();
|
|
35
|
+
size = stats.size;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
exists = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
id: snapshot.id,
|
|
42
|
+
branch: snapshot.branch,
|
|
43
|
+
filename: snapshot.filename,
|
|
44
|
+
name: snapshot.name,
|
|
45
|
+
description: snapshot.description,
|
|
46
|
+
type: snapshot.type || 'manual',
|
|
47
|
+
size,
|
|
48
|
+
createdAt: snapshot.created_at,
|
|
49
|
+
exists
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
reply.code(500).send({ error: error.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create snapshot from current branch
|
|
59
|
+
*/
|
|
60
|
+
fastify.post('/', async (request, reply) => {
|
|
61
|
+
const { name, description, type = 'manual' } = request.body;
|
|
62
|
+
const snapshotsPath = join(fastify.projectPath, '.studio', 'snapshots');
|
|
63
|
+
const metaDb = fastify.getMetaDb();
|
|
64
|
+
const currentBranch = fastify.getCurrentBranch();
|
|
65
|
+
|
|
66
|
+
if (!name) {
|
|
67
|
+
return reply.code(400).send({ error: 'Snapshot name is required' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate type
|
|
71
|
+
const validTypes = ['manual', 'auto-before-migration', 'auto-before-promote', 'import-baseline', 'auto-risky-query'];
|
|
72
|
+
const snapshotType = validTypes.includes(type) ? type : 'manual';
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Ensure snapshots directory exists
|
|
76
|
+
if (!existsSync(snapshotsPath)) {
|
|
77
|
+
mkdirSync(snapshotsPath, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Get current branch's DB file
|
|
81
|
+
const branchInfo = metaDb.prepare(`
|
|
82
|
+
SELECT db_file FROM branches WHERE name = ?
|
|
83
|
+
`).get(currentBranch);
|
|
84
|
+
|
|
85
|
+
if (!branchInfo) {
|
|
86
|
+
return reply.code(404).send({ error: 'Current branch not found' });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sourceDbPath = join(fastify.projectPath, branchInfo.db_file);
|
|
90
|
+
|
|
91
|
+
// Create snapshot filename with timestamp
|
|
92
|
+
const timestamp = Date.now();
|
|
93
|
+
const sanitizedName = name.replace(/[^a-zA-Z0-9_-]/g, '_').substring(0, 30);
|
|
94
|
+
const snapshotFilename = `${currentBranch}-${sanitizedName}-${timestamp}.snapshot.db`;
|
|
95
|
+
const snapshotPath = join(snapshotsPath, snapshotFilename);
|
|
96
|
+
|
|
97
|
+
// Checkpoint WAL before copying (ensures all data is in main DB file)
|
|
98
|
+
const db = fastify.getUserDb();
|
|
99
|
+
db.pragma('wal_checkpoint(FULL)');
|
|
100
|
+
|
|
101
|
+
// Copy database file
|
|
102
|
+
copyFileSync(sourceDbPath, snapshotPath);
|
|
103
|
+
|
|
104
|
+
// Get file size
|
|
105
|
+
const stats = statSync(snapshotPath);
|
|
106
|
+
|
|
107
|
+
// Record snapshot in meta DB
|
|
108
|
+
const result = metaDb.prepare(`
|
|
109
|
+
INSERT INTO snapshots (branch, filename, name, size, description, type)
|
|
110
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
111
|
+
`).run(currentBranch, snapshotFilename, name, stats.size, description || '', snapshotType);
|
|
112
|
+
|
|
113
|
+
// Log event
|
|
114
|
+
metaDb.prepare(`
|
|
115
|
+
INSERT INTO events (branch, type, data)
|
|
116
|
+
VALUES (?, 'snapshot_created', ?)
|
|
117
|
+
`).run(currentBranch, JSON.stringify({
|
|
118
|
+
name,
|
|
119
|
+
filename: snapshotFilename,
|
|
120
|
+
size: stats.size,
|
|
121
|
+
snapshot_type: snapshotType
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
id: result.lastInsertRowid,
|
|
126
|
+
branch: currentBranch,
|
|
127
|
+
filename: snapshotFilename,
|
|
128
|
+
name,
|
|
129
|
+
size: stats.size,
|
|
130
|
+
description: description || '',
|
|
131
|
+
type: snapshotType
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
reply.code(500).send({ error: error.message });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Restore snapshot (current branch only)
|
|
140
|
+
*/
|
|
141
|
+
fastify.post('/restore/:id', async (request, reply) => {
|
|
142
|
+
const { id } = request.params;
|
|
143
|
+
const snapshotsPath = join(fastify.projectPath, '.studio', 'snapshots');
|
|
144
|
+
const metaDb = fastify.getMetaDb();
|
|
145
|
+
const currentBranch = fastify.getCurrentBranch();
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Get snapshot
|
|
149
|
+
const snapshot = metaDb.prepare(`
|
|
150
|
+
SELECT * FROM snapshots WHERE id = ?
|
|
151
|
+
`).get(id);
|
|
152
|
+
|
|
153
|
+
if (!snapshot) {
|
|
154
|
+
return reply.code(404).send({ error: 'Snapshot not found' });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Verify snapshot belongs to current branch
|
|
158
|
+
if (snapshot.branch !== currentBranch) {
|
|
159
|
+
return reply.code(400).send({
|
|
160
|
+
error: `Cannot restore snapshot from "${snapshot.branch}" while on "${currentBranch}". Switch branches first.`
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const snapshotPath = join(snapshotsPath, snapshot.filename);
|
|
165
|
+
|
|
166
|
+
// Verify snapshot file exists
|
|
167
|
+
if (!existsSync(snapshotPath)) {
|
|
168
|
+
return reply.code(404).send({ error: 'Snapshot file not found' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Get current branch's DB file
|
|
172
|
+
const branchInfo = metaDb.prepare(`
|
|
173
|
+
SELECT db_file FROM branches WHERE name = ?
|
|
174
|
+
`).get(currentBranch);
|
|
175
|
+
|
|
176
|
+
if (!branchInfo) {
|
|
177
|
+
return reply.code(404).send({ error: 'Branch not found' });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const dbPath = join(fastify.projectPath, branchInfo.db_file);
|
|
181
|
+
|
|
182
|
+
// Close current branch DB connection
|
|
183
|
+
closeBranchConnection(fastify.projectPath, currentBranch);
|
|
184
|
+
|
|
185
|
+
// Copy snapshot over current DB
|
|
186
|
+
copyFileSync(snapshotPath, dbPath);
|
|
187
|
+
|
|
188
|
+
// Also remove WAL and SHM files (force fresh start)
|
|
189
|
+
['.wal', '.shm'].forEach(ext => {
|
|
190
|
+
const walPath = dbPath + ext;
|
|
191
|
+
if (existsSync(walPath)) {
|
|
192
|
+
try {
|
|
193
|
+
unlinkSync(walPath);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
// Ignore
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Log restore event
|
|
201
|
+
metaDb.prepare(`
|
|
202
|
+
INSERT INTO events (branch, type, data)
|
|
203
|
+
VALUES (?, 'snapshot_restored', ?)
|
|
204
|
+
`).run(currentBranch, JSON.stringify({
|
|
205
|
+
snapshot_id: id,
|
|
206
|
+
snapshot_name: snapshot.name,
|
|
207
|
+
filename: snapshot.filename
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
success: true,
|
|
212
|
+
message: 'Snapshot restored successfully',
|
|
213
|
+
snapshot: {
|
|
214
|
+
id: snapshot.id,
|
|
215
|
+
name: snapshot.name,
|
|
216
|
+
branch: snapshot.branch
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
} catch (error) {
|
|
220
|
+
reply.code(500).send({ error: error.message });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Delete snapshot
|
|
226
|
+
*/
|
|
227
|
+
fastify.delete('/:id', async (request, reply) => {
|
|
228
|
+
const { id } = request.params;
|
|
229
|
+
const snapshotsPath = join(fastify.projectPath, '.studio', 'snapshots');
|
|
230
|
+
const metaDb = fastify.getMetaDb();
|
|
231
|
+
const currentBranch = fastify.getCurrentBranch();
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const snapshot = metaDb.prepare(`
|
|
235
|
+
SELECT * FROM snapshots WHERE id = ?
|
|
236
|
+
`).get(id);
|
|
237
|
+
|
|
238
|
+
if (!snapshot) {
|
|
239
|
+
return reply.code(404).send({ error: 'Snapshot not found' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Optional: Only allow deleting snapshots from current branch
|
|
243
|
+
if (snapshot.branch !== currentBranch) {
|
|
244
|
+
return reply.code(400).send({
|
|
245
|
+
error: `Cannot delete snapshot from "${snapshot.branch}" while on "${currentBranch}"`
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Delete from meta DB
|
|
250
|
+
metaDb.prepare(`DELETE FROM snapshots WHERE id = ?`).run(id);
|
|
251
|
+
|
|
252
|
+
// Also delete the actual snapshot file
|
|
253
|
+
const snapshotFilePath = join(snapshotsPath, snapshot.filename);
|
|
254
|
+
try {
|
|
255
|
+
if (existsSync(snapshotFilePath)) {
|
|
256
|
+
unlinkSync(snapshotFilePath);
|
|
257
|
+
}
|
|
258
|
+
} catch (fileErr) {
|
|
259
|
+
// Log but don't fail if file deletion fails
|
|
260
|
+
console.warn('Failed to delete snapshot file:', fileErr.message);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Log event
|
|
264
|
+
metaDb.prepare(`
|
|
265
|
+
INSERT INTO events (branch, type, data)
|
|
266
|
+
VALUES (?, 'snapshot_deleted', ?)
|
|
267
|
+
`).run(currentBranch, JSON.stringify({
|
|
268
|
+
snapshot_id: id,
|
|
269
|
+
snapshot_name: snapshot.name
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
return { success: true };
|
|
273
|
+
} catch (error) {
|
|
274
|
+
reply.code(500).send({ error: error.message });
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get snapshot details
|
|
280
|
+
*/
|
|
281
|
+
fastify.get('/:id', async (request, reply) => {
|
|
282
|
+
const { id } = request.params;
|
|
283
|
+
const snapshotsPath = join(fastify.projectPath, '.studio', 'snapshots');
|
|
284
|
+
const metaDb = fastify.getMetaDb();
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const snapshot = metaDb.prepare(`
|
|
288
|
+
SELECT * FROM snapshots WHERE id = ?
|
|
289
|
+
`).get(id);
|
|
290
|
+
|
|
291
|
+
if (!snapshot) {
|
|
292
|
+
return reply.code(404).send({ error: 'Snapshot not found' });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const filePath = join(snapshotsPath, snapshot.filename);
|
|
296
|
+
let exists = false;
|
|
297
|
+
let size = snapshot.size;
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const stats = statSync(filePath);
|
|
301
|
+
exists = stats.isFile();
|
|
302
|
+
size = stats.size;
|
|
303
|
+
} catch (e) {
|
|
304
|
+
exists = false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
id: snapshot.id,
|
|
309
|
+
branch: snapshot.branch,
|
|
310
|
+
filename: snapshot.filename,
|
|
311
|
+
name: snapshot.name,
|
|
312
|
+
description: snapshot.description,
|
|
313
|
+
type: snapshot.type || 'manual',
|
|
314
|
+
size,
|
|
315
|
+
createdAt: snapshot.created_at,
|
|
316
|
+
exists
|
|
317
|
+
};
|
|
318
|
+
} catch (error) {
|
|
319
|
+
reply.code(500).send({ error: error.message });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export default async function tablesRoutes(fastify, options) {
|
|
2
|
+
// List all tables
|
|
3
|
+
fastify.get('/', async (request, reply) => {
|
|
4
|
+
const db = fastify.getUserDb();
|
|
5
|
+
|
|
6
|
+
const tables = db.prepare(`
|
|
7
|
+
SELECT name, sql
|
|
8
|
+
FROM sqlite_master
|
|
9
|
+
WHERE type = 'table'
|
|
10
|
+
AND name NOT LIKE 'sqlite_%'
|
|
11
|
+
AND name NOT LIKE '_studio_%'
|
|
12
|
+
ORDER BY name
|
|
13
|
+
`).all();
|
|
14
|
+
|
|
15
|
+
return tables.map(table => ({
|
|
16
|
+
name: table.name,
|
|
17
|
+
sql: table.sql
|
|
18
|
+
}));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Get table info
|
|
22
|
+
fastify.get('/:tableName', async (request, reply) => {
|
|
23
|
+
const { tableName } = request.params;
|
|
24
|
+
const db = fastify.getUserDb();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Get columns
|
|
28
|
+
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
29
|
+
|
|
30
|
+
// Get row count
|
|
31
|
+
const countResult = db.prepare(`SELECT COUNT(*) as count FROM ${tableName}`).get();
|
|
32
|
+
|
|
33
|
+
// Get CREATE statement
|
|
34
|
+
const tableInfo = db.prepare(`
|
|
35
|
+
SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?
|
|
36
|
+
`).get(tableName);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
name: tableName,
|
|
40
|
+
columns: columns.map(col => ({
|
|
41
|
+
name: col.name,
|
|
42
|
+
type: col.type,
|
|
43
|
+
notNull: col.notnull === 1,
|
|
44
|
+
defaultValue: col.dflt_value,
|
|
45
|
+
primaryKey: col.pk === 1
|
|
46
|
+
})),
|
|
47
|
+
rowCount: countResult.count,
|
|
48
|
+
sql: tableInfo?.sql
|
|
49
|
+
};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
reply.code(500).send({ error: error.message });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Get table data with pagination
|
|
56
|
+
fastify.get('/:tableName/data', async (request, reply) => {
|
|
57
|
+
const { tableName } = request.params;
|
|
58
|
+
const { limit = 100, offset = 0 } = request.query;
|
|
59
|
+
const db = fastify.getUserDb();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const rows = db.prepare(`
|
|
63
|
+
SELECT * FROM ${tableName}
|
|
64
|
+
LIMIT ? OFFSET ?
|
|
65
|
+
`).all(parseInt(limit), parseInt(offset));
|
|
66
|
+
|
|
67
|
+
const totalResult = db.prepare(`SELECT COUNT(*) as total FROM ${tableName}`).get();
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
data: rows,
|
|
71
|
+
total: totalResult.total,
|
|
72
|
+
limit: parseInt(limit),
|
|
73
|
+
offset: parseInt(offset)
|
|
74
|
+
};
|
|
75
|
+
} catch (error) {
|
|
76
|
+
reply.code(500).send({ error: error.message });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Create table
|
|
81
|
+
fastify.post('/', async (request, reply) => {
|
|
82
|
+
const { sql } = request.body;
|
|
83
|
+
const db = fastify.getUserDb();
|
|
84
|
+
const metaDb = fastify.getMetaDb();
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
db.exec(sql);
|
|
88
|
+
|
|
89
|
+
// Log event
|
|
90
|
+
metaDb.prepare(`
|
|
91
|
+
INSERT INTO events (type, data)
|
|
92
|
+
VALUES ('table_created', ?)
|
|
93
|
+
`).run(JSON.stringify({ sql }));
|
|
94
|
+
|
|
95
|
+
return { success: true };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
reply.code(400).send({ error: error.message });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Drop table
|
|
102
|
+
fastify.delete('/:tableName', async (request, reply) => {
|
|
103
|
+
const { tableName } = request.params;
|
|
104
|
+
const db = fastify.getUserDb();
|
|
105
|
+
const metaDb = fastify.getMetaDb();
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
db.exec(`DROP TABLE ${tableName}`);
|
|
109
|
+
|
|
110
|
+
// Log event
|
|
111
|
+
metaDb.prepare(`
|
|
112
|
+
INSERT INTO events (type, data)
|
|
113
|
+
VALUES ('table_dropped', ?)
|
|
114
|
+
`).run(JSON.stringify({ tableName }));
|
|
115
|
+
|
|
116
|
+
return { success: true };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
reply.code(500).send({ error: error.message });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export default async function timelineRoutes(fastify, options) {
|
|
2
|
+
/**
|
|
3
|
+
* Get timeline events for current branch
|
|
4
|
+
*/
|
|
5
|
+
fastify.get('/', async (request, reply) => {
|
|
6
|
+
const { limit = 50, offset = 0, all_branches } = request.query;
|
|
7
|
+
const metaDb = fastify.getMetaDb();
|
|
8
|
+
const currentBranch = fastify.getCurrentBranch();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
let events, totalResult;
|
|
12
|
+
|
|
13
|
+
if (all_branches === 'true') {
|
|
14
|
+
// Show events from all branches
|
|
15
|
+
events = metaDb.prepare(`
|
|
16
|
+
SELECT id, branch, type, data, created_at
|
|
17
|
+
FROM events
|
|
18
|
+
ORDER BY id DESC
|
|
19
|
+
LIMIT ? OFFSET ?
|
|
20
|
+
`).all(parseInt(limit), parseInt(offset));
|
|
21
|
+
|
|
22
|
+
totalResult = metaDb.prepare(`SELECT COUNT(*) as total FROM events`).get();
|
|
23
|
+
} else {
|
|
24
|
+
// Show events from current branch only (default)
|
|
25
|
+
events = metaDb.prepare(`
|
|
26
|
+
SELECT id, branch, type, data, created_at
|
|
27
|
+
FROM events
|
|
28
|
+
WHERE branch = ?
|
|
29
|
+
ORDER BY id DESC
|
|
30
|
+
LIMIT ? OFFSET ?
|
|
31
|
+
`).all(currentBranch, parseInt(limit), parseInt(offset));
|
|
32
|
+
|
|
33
|
+
totalResult = metaDb.prepare(`
|
|
34
|
+
SELECT COUNT(*) as total FROM events WHERE branch = ?
|
|
35
|
+
`).get(currentBranch);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
events: events.map(event => ({
|
|
40
|
+
id: event.id,
|
|
41
|
+
branch: event.branch,
|
|
42
|
+
type: event.type,
|
|
43
|
+
data: JSON.parse(event.data),
|
|
44
|
+
createdAt: event.created_at,
|
|
45
|
+
isCurrentBranch: event.branch === currentBranch
|
|
46
|
+
})),
|
|
47
|
+
currentBranch,
|
|
48
|
+
total: totalResult.total,
|
|
49
|
+
limit: parseInt(limit),
|
|
50
|
+
offset: parseInt(offset)
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
reply.code(500).send({ error: error.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Clear timeline for current branch
|
|
59
|
+
*/
|
|
60
|
+
fastify.delete('/', async (request, reply) => {
|
|
61
|
+
const metaDb = fastify.getMetaDb();
|
|
62
|
+
const currentBranch = fastify.getCurrentBranch();
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
metaDb.prepare(`DELETE FROM events WHERE branch = ?`).run(currentBranch);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
branch: currentBranch,
|
|
70
|
+
message: `Timeline cleared for branch "${currentBranch}"`
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
reply.code(500).send({ error: error.message });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get timeline summary/stats
|
|
79
|
+
*/
|
|
80
|
+
fastify.get('/stats', async (request, reply) => {
|
|
81
|
+
const metaDb = fastify.getMetaDb();
|
|
82
|
+
const currentBranch = fastify.getCurrentBranch();
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const stats = metaDb.prepare(`
|
|
86
|
+
SELECT
|
|
87
|
+
type,
|
|
88
|
+
COUNT(*) as count
|
|
89
|
+
FROM events
|
|
90
|
+
WHERE branch = ?
|
|
91
|
+
GROUP BY type
|
|
92
|
+
ORDER BY count DESC
|
|
93
|
+
`).all(currentBranch);
|
|
94
|
+
|
|
95
|
+
const totalEvents = metaDb.prepare(`
|
|
96
|
+
SELECT COUNT(*) as total FROM events WHERE branch = ?
|
|
97
|
+
`).get(currentBranch);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
branch: currentBranch,
|
|
101
|
+
total: totalEvents.total,
|
|
102
|
+
by_type: stats
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
reply.code(500).send({ error: error.message });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
package/server/server.js
ADDED
|
File without changes
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'child_process'
|
|
4
|
-
import { join, dirname } from 'path'
|
|
5
|
-
import { fileURLToPath } from 'url'
|
|
6
4
|
import chalk from 'chalk'
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
5
|
+
import { getServerEntryPath } from '../utils/paths.js'
|
|
9
6
|
|
|
10
7
|
export default function importServerCommand(port = 3000) {
|
|
11
8
|
console.log(chalk.cyan('→ Starting import server...'))
|
|
@@ -13,7 +10,7 @@ export default function importServerCommand(port = 3000) {
|
|
|
13
10
|
console.log(chalk.dim(' Mode: Import-only'))
|
|
14
11
|
console.log('')
|
|
15
12
|
|
|
16
|
-
const serverPath =
|
|
13
|
+
const serverPath = getServerEntryPath()
|
|
17
14
|
|
|
18
15
|
const serverProcess = spawn('node', [serverPath], {
|
|
19
16
|
stdio: 'inherit',
|
package/src/commands/import.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { existsSync, statSync, accessSync, constants, writeFileSync, mkdirSync } from 'fs'
|
|
4
|
-
import { resolve, extname, basename, join
|
|
5
|
-
import { fileURLToPath } from 'url'
|
|
4
|
+
import { resolve, extname, basename, join } from 'path'
|
|
6
5
|
import Database from 'better-sqlite3'
|
|
7
6
|
import chalk from 'chalk'
|
|
8
7
|
import { spawn } from 'child_process'
|
|
9
8
|
import http from 'http'
|
|
10
9
|
import open from 'open'
|
|
11
10
|
import { findFreePort } from '../utils/port-finder.js'
|
|
12
|
-
import { ensureSqlKiteDirs, LOGS_DIR } from '../utils/paths.js'
|
|
13
|
-
|
|
14
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
import { ensureSqlKiteDirs, LOGS_DIR, SQL_KITE_HOME, getStudioOutPath, getServerEntryPath } from '../utils/paths.js'
|
|
15
12
|
|
|
16
13
|
export default async function importCommand(dbPath) {
|
|
17
14
|
ensureSqlKiteDirs()
|
|
@@ -136,8 +133,7 @@ export default async function importCommand(dbPath) {
|
|
|
136
133
|
console.log('')
|
|
137
134
|
|
|
138
135
|
// Store import session data
|
|
139
|
-
const
|
|
140
|
-
const sqlKiteDir = join(homeDir, '.sql-kite')
|
|
136
|
+
const sqlKiteDir = SQL_KITE_HOME
|
|
141
137
|
const sessionFile = join(sqlKiteDir, 'import-pending.json')
|
|
142
138
|
|
|
143
139
|
// Ensure .sql-kite directory exists
|
|
@@ -223,7 +219,7 @@ export default async function importCommand(dbPath) {
|
|
|
223
219
|
console.log('')
|
|
224
220
|
console.log(chalk.dim(`Session saved to: ${sessionFile}`))
|
|
225
221
|
|
|
226
|
-
const studioPath =
|
|
222
|
+
const studioPath = getStudioOutPath()
|
|
227
223
|
if (!existsSync(studioPath)) {
|
|
228
224
|
console.log(chalk.red(`\n✗ Studio UI not built yet`))
|
|
229
225
|
console.log(chalk.dim(` Run: ${chalk.cyan(`cd packages/studio && npm run build`)}`))
|
|
@@ -234,7 +230,7 @@ export default async function importCommand(dbPath) {
|
|
|
234
230
|
const { port, alreadyRunning } = await getImportServerPort()
|
|
235
231
|
|
|
236
232
|
if (!alreadyRunning) {
|
|
237
|
-
const serverPath =
|
|
233
|
+
const serverPath = getServerEntryPath()
|
|
238
234
|
const logPath = join(LOGS_DIR, `import-server-${Date.now()}.log`)
|
|
239
235
|
const out = []
|
|
240
236
|
const serverProcess = spawn('node', [serverPath], {
|
package/src/commands/start.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
-
import { join
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
3
|
+
import { join } from 'path';
|
|
5
4
|
import http from 'http';
|
|
6
5
|
import chalk from 'chalk';
|
|
7
6
|
import ora from 'ora';
|
|
@@ -9,12 +8,12 @@ import open from 'open';
|
|
|
9
8
|
import {
|
|
10
9
|
getProjectPath,
|
|
11
10
|
getProjectServerInfoPath,
|
|
12
|
-
projectExists
|
|
11
|
+
projectExists,
|
|
12
|
+
getStudioOutPath,
|
|
13
|
+
getServerEntryPath
|
|
13
14
|
} from '../utils/paths.js';
|
|
14
15
|
import { findFreePort } from '../utils/port-finder.js';
|
|
15
16
|
|
|
16
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
|
|
18
17
|
// Helper function to check if server is ready
|
|
19
18
|
async function waitForServer(port, maxAttempts = 30) {
|
|
20
19
|
for (let i = 0; i < maxAttempts; i++) {
|
|
@@ -50,7 +49,7 @@ export async function startCommand(name) {
|
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
// Check if studio is built
|
|
53
|
-
const studioPath =
|
|
52
|
+
const studioPath = getStudioOutPath();
|
|
54
53
|
if (!existsSync(studioPath)) {
|
|
55
54
|
console.log(chalk.red(`✗ Studio UI not built yet`));
|
|
56
55
|
console.log(chalk.dim(` Run: ${chalk.cyan(`cd packages/studio && npm run build`)}`));
|
|
@@ -80,7 +79,7 @@ export async function startCommand(name) {
|
|
|
80
79
|
const projectPath = getProjectPath(name);
|
|
81
80
|
|
|
82
81
|
// Path to server package
|
|
83
|
-
const serverPath =
|
|
82
|
+
const serverPath = getServerEntryPath();
|
|
84
83
|
|
|
85
84
|
// Spawn server process
|
|
86
85
|
const serverProcess = spawn('node', [serverPath], {
|
package/src/commands/stop.js
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { getProjectServerInfoPath, projectExists } from '../utils/paths.js';
|
|
5
5
|
import { releasePort } from '../utils/port-finder.js';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
6
7
|
|
|
7
8
|
export async function stopCommand(name) {
|
|
8
9
|
if (!projectExists(name)) {
|
|
@@ -22,7 +23,7 @@ export async function stopCommand(name) {
|
|
|
22
23
|
try {
|
|
23
24
|
const serverInfo = JSON.parse(readFileSync(serverInfoPath, 'utf-8'));
|
|
24
25
|
|
|
25
|
-
//
|
|
26
|
+
// Try graceful shutdown first
|
|
26
27
|
try {
|
|
27
28
|
process.kill(serverInfo.pid, 'SIGTERM');
|
|
28
29
|
} catch (killError) {
|
|
@@ -49,7 +50,11 @@ export async function stopCommand(name) {
|
|
|
49
50
|
if (!processTerminated) {
|
|
50
51
|
// Force kill if graceful shutdown timed out
|
|
51
52
|
try {
|
|
52
|
-
process.
|
|
53
|
+
if (process.platform === 'win32') {
|
|
54
|
+
execSync(`taskkill /PID ${serverInfo.pid} /F /T`, { stdio: 'ignore' });
|
|
55
|
+
} else {
|
|
56
|
+
process.kill(serverInfo.pid, 'SIGKILL');
|
|
57
|
+
}
|
|
53
58
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
54
59
|
} catch (e) {
|
|
55
60
|
// Process might already be gone
|