sql-kite 1.0.6 → 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/README.md +10 -2
- package/package.json +12 -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,201 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { existsSync, copyFileSync, unlinkSync, statSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { getCurrentBranch } from '../db/connections.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Export Routes
|
|
8
|
+
*
|
|
9
|
+
* Handles exporting production database from main branch
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export default async function exportRoutes(fastify, options) {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get export status and pre-flight checks
|
|
16
|
+
*/
|
|
17
|
+
fastify.get('/status', async (request, reply) => {
|
|
18
|
+
try {
|
|
19
|
+
const metaDb = fastify.getMetaDb();
|
|
20
|
+
const projectPath = fastify.projectPath;
|
|
21
|
+
|
|
22
|
+
// Check if main branch exists
|
|
23
|
+
const mainBranch = metaDb.prepare(`
|
|
24
|
+
SELECT name, db_file, created_at FROM branches WHERE name = 'main'
|
|
25
|
+
`).get();
|
|
26
|
+
|
|
27
|
+
const mainExists = !!mainBranch;
|
|
28
|
+
let databaseHealthy = false;
|
|
29
|
+
let tableCount = 0;
|
|
30
|
+
let totalRows = 0;
|
|
31
|
+
let lastModified = null;
|
|
32
|
+
let pendingMigrations = 0;
|
|
33
|
+
|
|
34
|
+
if (mainExists) {
|
|
35
|
+
// Resolve database path consistently with the rest of the codebase
|
|
36
|
+
const mainDbPath = join(projectPath, mainBranch.db_file);
|
|
37
|
+
|
|
38
|
+
if (existsSync(mainDbPath)) {
|
|
39
|
+
try {
|
|
40
|
+
// Open main database to check health (not readonly to handle WAL mode properly)
|
|
41
|
+
const mainDb = new Database(mainDbPath);
|
|
42
|
+
|
|
43
|
+
// Check integrity - PRAGMA returns object with integrity_check property
|
|
44
|
+
const integrityResult = mainDb.prepare('PRAGMA integrity_check').get();
|
|
45
|
+
// Access the property directly - it returns { integrity_check: 'ok' } when healthy
|
|
46
|
+
const integrityValue = integrityResult?.integrity_check;
|
|
47
|
+
databaseHealthy = integrityValue === 'ok';
|
|
48
|
+
|
|
49
|
+
// Get table count
|
|
50
|
+
const tables = mainDb.prepare(`
|
|
51
|
+
SELECT name FROM sqlite_master
|
|
52
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
53
|
+
`).all();
|
|
54
|
+
tableCount = tables.length;
|
|
55
|
+
|
|
56
|
+
// Get total row count across tables
|
|
57
|
+
for (const table of tables) {
|
|
58
|
+
try {
|
|
59
|
+
const count = mainDb.prepare(`SELECT COUNT(*) as cnt FROM "${table.name}"`).get();
|
|
60
|
+
totalRows += count?.cnt || 0;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// Skip tables that can't be counted
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
mainDb.close();
|
|
67
|
+
|
|
68
|
+
// Get file modification time
|
|
69
|
+
const stat = statSync(mainDbPath);
|
|
70
|
+
lastModified = stat.mtime.toISOString();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('Error checking main database health:', error);
|
|
73
|
+
databaseHealthy = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for pending migrations
|
|
78
|
+
try {
|
|
79
|
+
const pendingMigrationCount = metaDb.prepare(`
|
|
80
|
+
SELECT COUNT(*) as cnt FROM migrations
|
|
81
|
+
WHERE status = 'pending' AND branch = 'main'
|
|
82
|
+
`).get();
|
|
83
|
+
pendingMigrations = pendingMigrationCount?.cnt || 0;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// Migrations table might not exist
|
|
86
|
+
pendingMigrations = 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
mainExists,
|
|
92
|
+
databaseHealthy,
|
|
93
|
+
tableCount,
|
|
94
|
+
totalRows,
|
|
95
|
+
lastModified,
|
|
96
|
+
pendingMigrations
|
|
97
|
+
};
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Export status error:', error);
|
|
100
|
+
return reply.code(500).send({
|
|
101
|
+
error: error.message
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Export the main branch database
|
|
108
|
+
*
|
|
109
|
+
* This creates a clean SQLite file:
|
|
110
|
+
* - WAL checkpoint performed
|
|
111
|
+
* - No WAL or SHM files
|
|
112
|
+
* - No branch metadata
|
|
113
|
+
* - No timeline
|
|
114
|
+
* - Just pure SQLite data
|
|
115
|
+
*/
|
|
116
|
+
fastify.post('/database', async (request, reply) => {
|
|
117
|
+
const { fileName = 'production' } = request.body || {};
|
|
118
|
+
const metaDb = fastify.getMetaDb();
|
|
119
|
+
const projectPath = fastify.projectPath;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Get main branch info
|
|
123
|
+
const mainBranch = metaDb.prepare(`
|
|
124
|
+
SELECT name, db_file FROM branches WHERE name = 'main'
|
|
125
|
+
`).get();
|
|
126
|
+
|
|
127
|
+
if (!mainBranch) {
|
|
128
|
+
return reply.code(400).send({
|
|
129
|
+
error: 'Main branch does not exist. Cannot export.'
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Resolve database path consistently
|
|
134
|
+
const mainDbPath = join(projectPath, mainBranch.db_file);
|
|
135
|
+
|
|
136
|
+
if (!existsSync(mainDbPath)) {
|
|
137
|
+
return reply.code(400).send({
|
|
138
|
+
error: 'Main database file not found.'
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Create a temporary export path
|
|
143
|
+
const exportPath = join(projectPath, '.studio', `export_${Date.now()}.db`);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// Open the main database
|
|
147
|
+
const sourceDb = new Database(mainDbPath);
|
|
148
|
+
|
|
149
|
+
// Perform WAL checkpoint to ensure all data is in main file
|
|
150
|
+
sourceDb.pragma('wal_checkpoint(TRUNCATE)');
|
|
151
|
+
|
|
152
|
+
// Use backup API for clean copy
|
|
153
|
+
await sourceDb.backup(exportPath);
|
|
154
|
+
|
|
155
|
+
sourceDb.close();
|
|
156
|
+
|
|
157
|
+
// Open the exported database and clean it up
|
|
158
|
+
const exportDb = new Database(exportPath);
|
|
159
|
+
|
|
160
|
+
// Vacuum to optimize
|
|
161
|
+
exportDb.exec('VACUUM');
|
|
162
|
+
|
|
163
|
+
// Ensure no WAL mode on export (use DELETE journal for compatibility)
|
|
164
|
+
exportDb.pragma('journal_mode = DELETE');
|
|
165
|
+
|
|
166
|
+
exportDb.close();
|
|
167
|
+
|
|
168
|
+
// Read the file and send as download
|
|
169
|
+
const fileBuffer = readFileSync(exportPath);
|
|
170
|
+
|
|
171
|
+
// Clean up temp file
|
|
172
|
+
try {
|
|
173
|
+
unlinkSync(exportPath);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.error('Failed to clean up temp export file:', e);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Send as binary download
|
|
179
|
+
reply
|
|
180
|
+
.header('Content-Type', 'application/x-sqlite3')
|
|
181
|
+
.header('Content-Disposition', `attachment; filename="${fileName}.db"`)
|
|
182
|
+
.header('Content-Length', fileBuffer.length)
|
|
183
|
+
.send(fileBuffer);
|
|
184
|
+
|
|
185
|
+
} catch (error) {
|
|
186
|
+
// Clean up temp file on error
|
|
187
|
+
try {
|
|
188
|
+
if (existsSync(exportPath)) {
|
|
189
|
+
unlinkSync(exportPath);
|
|
190
|
+
}
|
|
191
|
+
} catch (e) {}
|
|
192
|
+
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error('Export error:', error);
|
|
198
|
+
reply.code(500).send({ error: error.message });
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync, unlinkSync, writeFileSync, statSync } from 'fs'
|
|
2
|
+
import { join, dirname } from 'path'
|
|
3
|
+
import { spawn } from 'child_process'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
5
|
+
import { homedir } from 'os'
|
|
6
|
+
import Database from 'better-sqlite3'
|
|
7
|
+
|
|
8
|
+
const __routes_dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
|
|
10
|
+
// Resolve CLI utils - works in both layouts:
|
|
11
|
+
// monorepo: packages/server/src/routes/ -> ../../../cli/src/utils/
|
|
12
|
+
// npm: sql-kite/server/routes/ -> ../../src/utils/
|
|
13
|
+
const cliUtilsCandidates = [
|
|
14
|
+
join(__routes_dirname, '..', '..', 'src', 'utils'), // npm layout
|
|
15
|
+
join(__routes_dirname, '..', '..', '..', 'cli', 'src', 'utils') // monorepo layout
|
|
16
|
+
]
|
|
17
|
+
let cliUtilsDir
|
|
18
|
+
for (const candidate of cliUtilsCandidates) {
|
|
19
|
+
if (existsSync(join(candidate, 'paths.js'))) {
|
|
20
|
+
cliUtilsDir = candidate
|
|
21
|
+
break
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (!cliUtilsDir) {
|
|
25
|
+
throw new Error('Could not find CLI utils directory. Tried: ' + cliUtilsCandidates.join(', '))
|
|
26
|
+
}
|
|
27
|
+
const { findFreePort } = await import(pathToFileURL(join(cliUtilsDir, 'port-finder.js')).href)
|
|
28
|
+
const { migrateMetaDb } = await import(pathToFileURL(join(cliUtilsDir, 'meta-migration.js')).href)
|
|
29
|
+
const { validateProjectName } = await import(pathToFileURL(join(cliUtilsDir, 'paths.js')).href)
|
|
30
|
+
|
|
31
|
+
export default async function importRoutes(fastify, options) {
|
|
32
|
+
const homeDir = homedir()
|
|
33
|
+
const projectsRoot = join(homeDir, '.sql-kite', 'runtime')
|
|
34
|
+
const sessionFile = join(homeDir, '.sql-kite', 'import-pending.json')
|
|
35
|
+
|
|
36
|
+
// Get pending import session
|
|
37
|
+
fastify.get('/pending', async (request, reply) => {
|
|
38
|
+
try {
|
|
39
|
+
if (!existsSync(sessionFile)) {
|
|
40
|
+
return { pending: false }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const session = JSON.parse(readFileSync(sessionFile, 'utf-8'))
|
|
44
|
+
return { pending: true, ...session }
|
|
45
|
+
} catch (error) {
|
|
46
|
+
fastify.log.error(error)
|
|
47
|
+
return { pending: false, error: error.message }
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Clear pending import session
|
|
52
|
+
fastify.delete('/pending', async (request, reply) => {
|
|
53
|
+
try {
|
|
54
|
+
if (existsSync(sessionFile)) {
|
|
55
|
+
unlinkSync(sessionFile)
|
|
56
|
+
}
|
|
57
|
+
return { success: true }
|
|
58
|
+
} catch (error) {
|
|
59
|
+
fastify.log.error(error)
|
|
60
|
+
return reply.code(500).send({ error: error.message })
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Create project structure
|
|
65
|
+
fastify.post('/create', async (request, reply) => {
|
|
66
|
+
const { projectName, sourcePath, importMode } = request.body
|
|
67
|
+
|
|
68
|
+
// Validate project name to prevent path traversal
|
|
69
|
+
const validation = validateProjectName(projectName)
|
|
70
|
+
if (!validation.valid) {
|
|
71
|
+
return reply.code(400).send({ error: validation.error })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const safeName = validation.sanitized
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const projectPath = join(projectsRoot, safeName)
|
|
78
|
+
|
|
79
|
+
// Check if project already exists
|
|
80
|
+
if (existsSync(projectPath)) {
|
|
81
|
+
return reply.code(400).send({ error: 'Project already exists' })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Create project directories
|
|
85
|
+
mkdirSync(projectPath, { recursive: true })
|
|
86
|
+
mkdirSync(join(projectPath, 'migrations'), { recursive: true })
|
|
87
|
+
mkdirSync(join(projectPath, 'snapshots'), { recursive: true })
|
|
88
|
+
mkdirSync(join(projectPath, '.studio'), { recursive: true })
|
|
89
|
+
mkdirSync(join(projectPath, '.studio', 'snapshots'), { recursive: true })
|
|
90
|
+
mkdirSync(join(projectPath, '.studio', 'locks'), { recursive: true })
|
|
91
|
+
|
|
92
|
+
// Create config.json
|
|
93
|
+
const config = {
|
|
94
|
+
name: safeName,
|
|
95
|
+
created_at: new Date().toISOString(),
|
|
96
|
+
version: '1.0.0'
|
|
97
|
+
}
|
|
98
|
+
writeFileSync(
|
|
99
|
+
join(projectPath, 'config.json'),
|
|
100
|
+
JSON.stringify(config, null, 2)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Initialize meta.db using the shared migration
|
|
104
|
+
const metaDbPath = join(projectPath, '.studio', 'meta.db')
|
|
105
|
+
migrateMetaDb(metaDbPath)
|
|
106
|
+
|
|
107
|
+
// Update main branch description and ensure current branch
|
|
108
|
+
const metaDb = new Database(metaDbPath)
|
|
109
|
+
metaDb.prepare(`
|
|
110
|
+
UPDATE branches
|
|
111
|
+
SET description = 'Imported database baseline'
|
|
112
|
+
WHERE name = 'main'
|
|
113
|
+
`).run()
|
|
114
|
+
metaDb.prepare(`
|
|
115
|
+
INSERT OR REPLACE INTO settings (key, value)
|
|
116
|
+
VALUES ('current_branch', 'main')
|
|
117
|
+
`).run()
|
|
118
|
+
metaDb.close()
|
|
119
|
+
|
|
120
|
+
return { success: true, projectPath }
|
|
121
|
+
} catch (error) {
|
|
122
|
+
fastify.log.error(error)
|
|
123
|
+
return reply.code(500).send({ error: error.message })
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Copy database with WAL checkpoint
|
|
128
|
+
fastify.post('/copy', async (request, reply) => {
|
|
129
|
+
const { projectName, sourcePath, importMode } = request.body
|
|
130
|
+
|
|
131
|
+
// Validate project name
|
|
132
|
+
const validation = validateProjectName(projectName)
|
|
133
|
+
if (!validation.valid) {
|
|
134
|
+
return reply.code(400).send({ error: validation.error })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const projectPath = join(projectsRoot, validation.sanitized)
|
|
139
|
+
const targetPath = join(projectPath, 'db.sqlite')
|
|
140
|
+
|
|
141
|
+
if (importMode === 'copy') {
|
|
142
|
+
// Step 1: Open source DB and checkpoint WAL
|
|
143
|
+
let sourceDb
|
|
144
|
+
try {
|
|
145
|
+
sourceDb = new Database(sourcePath, { fileMustExist: true })
|
|
146
|
+
|
|
147
|
+
// Force WAL checkpoint to ensure all data is in main DB file
|
|
148
|
+
sourceDb.pragma('wal_checkpoint(FULL)')
|
|
149
|
+
|
|
150
|
+
sourceDb.close()
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (sourceDb) sourceDb.close()
|
|
153
|
+
throw new Error(`Failed to checkpoint source database: ${err.message}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Step 2: Copy database files
|
|
157
|
+
try {
|
|
158
|
+
// Copy main DB file
|
|
159
|
+
copyFileSync(sourcePath, targetPath)
|
|
160
|
+
|
|
161
|
+
// Copy WAL file if exists
|
|
162
|
+
const walPath = sourcePath + '-wal'
|
|
163
|
+
if (existsSync(walPath)) {
|
|
164
|
+
copyFileSync(walPath, targetPath + '-wal')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Copy SHM file if exists
|
|
168
|
+
const shmPath = sourcePath + '-shm'
|
|
169
|
+
if (existsSync(shmPath)) {
|
|
170
|
+
copyFileSync(shmPath, targetPath + '-shm')
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
throw new Error(`Failed to copy database files: ${err.message}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Step 3: Verify copied database
|
|
177
|
+
let targetDb
|
|
178
|
+
try {
|
|
179
|
+
targetDb = new Database(targetPath)
|
|
180
|
+
|
|
181
|
+
// Test query to verify integrity
|
|
182
|
+
targetDb.prepare('SELECT COUNT(*) FROM sqlite_master').get()
|
|
183
|
+
|
|
184
|
+
// Enable WAL mode for the copied database
|
|
185
|
+
targetDb.pragma('journal_mode = WAL')
|
|
186
|
+
|
|
187
|
+
targetDb.close()
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (targetDb) targetDb.close()
|
|
190
|
+
throw new Error(`Failed to verify copied database: ${err.message}`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
} else if (importMode === 'link') {
|
|
194
|
+
if (!existsSync(sourcePath)) {
|
|
195
|
+
return reply.code(400).send({ error: 'Source database not found' })
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Update main branch db_file
|
|
200
|
+
const metaDbPath = join(projectPath, '.studio', 'meta.db')
|
|
201
|
+
const metaDb = new Database(metaDbPath)
|
|
202
|
+
|
|
203
|
+
const dbFile = importMode === 'link' ? sourcePath : 'db.sqlite'
|
|
204
|
+
metaDb.prepare(`
|
|
205
|
+
UPDATE branches
|
|
206
|
+
SET db_file = ?
|
|
207
|
+
WHERE name = 'main'
|
|
208
|
+
`).run(dbFile)
|
|
209
|
+
|
|
210
|
+
// Log event in meta.db
|
|
211
|
+
metaDb.prepare(`
|
|
212
|
+
INSERT INTO events (branch, type, data)
|
|
213
|
+
VALUES ('main', 'database_imported', ?)
|
|
214
|
+
`).run(JSON.stringify({ sourcePath, importMode, db_file: dbFile }))
|
|
215
|
+
|
|
216
|
+
metaDb.close()
|
|
217
|
+
|
|
218
|
+
return { success: true }
|
|
219
|
+
} catch (error) {
|
|
220
|
+
fastify.log.error(error)
|
|
221
|
+
return reply.code(500).send({ error: error.message })
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// Create baseline snapshot
|
|
226
|
+
fastify.post('/snapshot', async (request, reply) => {
|
|
227
|
+
const { projectName } = request.body
|
|
228
|
+
|
|
229
|
+
// Validate project name
|
|
230
|
+
const validation = validateProjectName(projectName)
|
|
231
|
+
if (!validation.valid) {
|
|
232
|
+
return reply.code(400).send({ error: validation.error })
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const projectPath = join(projectsRoot, validation.sanitized)
|
|
237
|
+
const metaDbPath = join(projectPath, '.studio', 'meta.db')
|
|
238
|
+
const metaDb = new Database(metaDbPath)
|
|
239
|
+
const branchInfo = metaDb.prepare(`
|
|
240
|
+
SELECT db_file FROM branches WHERE name = 'main'
|
|
241
|
+
`).get()
|
|
242
|
+
|
|
243
|
+
if (!branchInfo) {
|
|
244
|
+
metaDb.close()
|
|
245
|
+
return reply.code(404).send({ error: 'Main branch not found' })
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const sourceDbPath = join(projectPath, branchInfo.db_file)
|
|
249
|
+
|
|
250
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
251
|
+
const snapshotFilename = `imported-baseline-${timestamp}.db`
|
|
252
|
+
const snapshotDir = join(projectPath, '.studio', 'snapshots')
|
|
253
|
+
|
|
254
|
+
// Ensure snapshots directory exists
|
|
255
|
+
if (!existsSync(snapshotDir)) {
|
|
256
|
+
mkdirSync(snapshotDir, { recursive: true })
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const snapshotPath = join(snapshotDir, snapshotFilename)
|
|
260
|
+
|
|
261
|
+
// Copy database to snapshot
|
|
262
|
+
let sourceDb
|
|
263
|
+
try {
|
|
264
|
+
sourceDb = new Database(sourceDbPath)
|
|
265
|
+
sourceDb.pragma('wal_checkpoint(FULL)')
|
|
266
|
+
sourceDb.close()
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (sourceDb) sourceDb.close()
|
|
269
|
+
throw err
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
copyFileSync(sourceDbPath, snapshotPath)
|
|
273
|
+
const stats = statSync(snapshotPath)
|
|
274
|
+
|
|
275
|
+
metaDb.prepare(`
|
|
276
|
+
INSERT INTO snapshots (branch, filename, name, size, description)
|
|
277
|
+
VALUES ('main', ?, 'Imported Baseline', ?, 'Baseline snapshot created during import')
|
|
278
|
+
`).run(snapshotFilename, stats.size)
|
|
279
|
+
|
|
280
|
+
metaDb.prepare(`
|
|
281
|
+
INSERT INTO events (branch, type, data)
|
|
282
|
+
VALUES ('main', 'snapshot_created', ?)
|
|
283
|
+
`).run(JSON.stringify({ filename: snapshotFilename }))
|
|
284
|
+
|
|
285
|
+
metaDb.close()
|
|
286
|
+
|
|
287
|
+
return { success: true, snapshotId: snapshotFilename }
|
|
288
|
+
} catch (error) {
|
|
289
|
+
fastify.log.error(error)
|
|
290
|
+
return reply.code(500).send({ error: error.message })
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
// Finalize import
|
|
295
|
+
fastify.post('/finalize', async (request, reply) => {
|
|
296
|
+
const { projectName } = request.body
|
|
297
|
+
|
|
298
|
+
// Validate project name
|
|
299
|
+
const validation = validateProjectName(projectName)
|
|
300
|
+
if (!validation.valid) {
|
|
301
|
+
return reply.code(400).send({ error: validation.error })
|
|
302
|
+
}
|
|
303
|
+
const safeName = validation.sanitized
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const projectPath = join(projectsRoot, safeName)
|
|
307
|
+
const metaDbPath = join(projectPath, '.studio', 'meta.db')
|
|
308
|
+
const metaDb = new Database(metaDbPath)
|
|
309
|
+
|
|
310
|
+
// Create baseline migration marker (NOT a SQL file)
|
|
311
|
+
metaDb.prepare(`
|
|
312
|
+
INSERT INTO migrations (branch, filename, applied_at)
|
|
313
|
+
VALUES ('main', 'baseline', datetime('now'))
|
|
314
|
+
`).run()
|
|
315
|
+
|
|
316
|
+
// Log finalization event
|
|
317
|
+
metaDb.prepare(`
|
|
318
|
+
INSERT INTO events (branch, type, data)
|
|
319
|
+
VALUES ('main', 'import_completed', ?)
|
|
320
|
+
`).run(JSON.stringify({ completed_at: new Date().toISOString() }))
|
|
321
|
+
|
|
322
|
+
metaDb.close()
|
|
323
|
+
|
|
324
|
+
// Create lock file for main branch
|
|
325
|
+
const locksDir = join(projectPath, '.studio', 'locks')
|
|
326
|
+
if (!existsSync(locksDir)) {
|
|
327
|
+
mkdirSync(locksDir, { recursive: true })
|
|
328
|
+
}
|
|
329
|
+
const lockPath = join(locksDir, 'main.lock')
|
|
330
|
+
if (!existsSync(lockPath)) {
|
|
331
|
+
writeFileSync(
|
|
332
|
+
lockPath,
|
|
333
|
+
JSON.stringify({
|
|
334
|
+
branch: 'main',
|
|
335
|
+
created_at: new Date().toISOString(),
|
|
336
|
+
reason: 'import'
|
|
337
|
+
}, null, 2)
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Start project server automatically
|
|
342
|
+
const serverInfoPath = join(projectPath, '.studio', 'server.json')
|
|
343
|
+
if (existsSync(serverInfoPath)) {
|
|
344
|
+
const serverInfo = JSON.parse(readFileSync(serverInfoPath, 'utf-8'))
|
|
345
|
+
return { success: true, server: { running: true, port: serverInfo.port } }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const port = await findFreePort(3000, safeName)
|
|
349
|
+
const serverPath = join(__routes_dirname, '..', 'index.js')
|
|
350
|
+
const serverProcess = spawn('node', [serverPath], {
|
|
351
|
+
detached: true,
|
|
352
|
+
stdio: 'ignore',
|
|
353
|
+
env: {
|
|
354
|
+
...process.env,
|
|
355
|
+
PROJECT_NAME: safeName,
|
|
356
|
+
PROJECT_PATH: projectPath,
|
|
357
|
+
PORT: port.toString(),
|
|
358
|
+
IMPORT_MODE: 'false'
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
serverProcess.unref()
|
|
362
|
+
|
|
363
|
+
writeFileSync(serverInfoPath, JSON.stringify({
|
|
364
|
+
pid: serverProcess.pid,
|
|
365
|
+
port,
|
|
366
|
+
started_at: new Date().toISOString()
|
|
367
|
+
}, null, 2))
|
|
368
|
+
|
|
369
|
+
return { success: true, server: { running: true, port } }
|
|
370
|
+
} catch (error) {
|
|
371
|
+
fastify.log.error(error)
|
|
372
|
+
return reply.code(500).send({ error: error.message })
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
}
|