react-native-sqlite-mcp 1.0.0 → 1.0.1
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/CONTRIBUTING.md +35 -0
- package/LICENSE +21 -0
- package/README.md +52 -31
- package/dist/db.js +73 -26
- package/dist/index.js +126 -60
- package/dist/locator.js +34 -44
- package/dist/logger.js +33 -0
- package/dist/shell.js +57 -0
- package/package.json +1 -1
- package/src/db.ts +84 -38
- package/src/index.ts +181 -79
- package/src/locator.ts +88 -71
- package/src/logger.ts +37 -0
- package/src/shell.ts +81 -0
package/dist/locator.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
2
1
|
import fs from "fs";
|
|
3
2
|
import path from "path";
|
|
4
3
|
import os from "os";
|
|
4
|
+
import { shell } from "./shell.js";
|
|
5
|
+
import { logger } from "./logger.js";
|
|
5
6
|
export async function listDatabases(bundleId, targetPlatform) {
|
|
6
7
|
const results = [];
|
|
7
8
|
if (!targetPlatform || targetPlatform === 'ios') {
|
|
8
9
|
try {
|
|
9
|
-
const udidStr =
|
|
10
|
+
const udidStr = await shell("xcrun simctl list devices booted | awk -F '[()]' '/Booted/{print $2; exit}'", { timeout: 5_000, label: "xcrun-simctl-booted" });
|
|
10
11
|
if (udidStr) {
|
|
11
12
|
const appDataDir = `${process.env.HOME}/Library/Developer/CoreSimulator/Devices/${udidStr}/data/Containers/Data/Application`;
|
|
12
13
|
if (fs.existsSync(appDataDir)) {
|
|
13
14
|
try {
|
|
14
|
-
const
|
|
15
|
-
const found = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 10000 }).toString().trim();
|
|
15
|
+
const found = await shell(`find "${appDataDir}" -type f \\( -name "*.db" -o -name "*.sqlite" -o -name "*.sqlite3" \\) -maxdepth 7 -print`, { timeout: 15_000, label: "ios-find-dbs" });
|
|
16
16
|
if (found) {
|
|
17
17
|
results.push({
|
|
18
18
|
platform: 'ios',
|
|
@@ -22,7 +22,7 @@ export async function listDatabases(bundleId, targetPlatform) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
catch (e) {
|
|
25
|
-
|
|
25
|
+
logger.warn("iOS find failed", { error: String(e) });
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -38,7 +38,7 @@ export async function listDatabases(bundleId, targetPlatform) {
|
|
|
38
38
|
}
|
|
39
39
|
if (!targetPlatform || targetPlatform === 'android') {
|
|
40
40
|
try {
|
|
41
|
-
|
|
41
|
+
await shell("adb get-state", { timeout: 5_000, label: "adb-get-state" });
|
|
42
42
|
}
|
|
43
43
|
catch (e) {
|
|
44
44
|
if (targetPlatform === 'android') {
|
|
@@ -49,14 +49,14 @@ export async function listDatabases(bundleId, targetPlatform) {
|
|
|
49
49
|
}
|
|
50
50
|
return results;
|
|
51
51
|
}
|
|
52
|
-
//
|
|
52
|
+
// Discover packages to scan
|
|
53
53
|
let packagesToScan = [];
|
|
54
54
|
if (bundleId) {
|
|
55
55
|
packagesToScan = [bundleId];
|
|
56
56
|
}
|
|
57
57
|
else {
|
|
58
58
|
try {
|
|
59
|
-
const packagesStr =
|
|
59
|
+
const packagesStr = await shell("adb shell pm list packages -3", { timeout: 8_000, label: "adb-list-packages", retries: 1, retryDelay: 1_000 });
|
|
60
60
|
packagesToScan = packagesStr.split('\n')
|
|
61
61
|
.map(line => line.replace('package:', '').trim())
|
|
62
62
|
.filter(Boolean);
|
|
@@ -74,18 +74,16 @@ export async function listDatabases(bundleId, targetPlatform) {
|
|
|
74
74
|
const baseDirs = [`/data/user/0/${pkg}`, `/data/data/${pkg}`];
|
|
75
75
|
for (const baseDir of baseDirs) {
|
|
76
76
|
try {
|
|
77
|
-
|
|
77
|
+
await shell(`adb shell run-as ${pkg} ls -d ${baseDir}`, { timeout: 3_000, label: `adb-ls-${pkg}` });
|
|
78
78
|
let foundFiles = [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
foundFiles.push(...findOut.split('\n').map(l => l.trim().replace(/\r/g, '')).filter(Boolean));
|
|
84
|
-
}
|
|
79
|
+
// Find .db / .sqlite / .sqlite3 files recursively
|
|
80
|
+
const findOut = await shell(`adb shell "run-as ${pkg} find ${baseDir} -type f \\( -name \\"*.db\\" -o -name \\"*.sqlite\\" -o -name \\"*.sqlite3\\" \\)"`, { timeout: 8_000, ignoreErrors: true, label: `adb-find-${pkg}` });
|
|
81
|
+
if (findOut) {
|
|
82
|
+
foundFiles.push(...findOut.split('\n').map(l => l.trim().replace(/\r/g, '')).filter(Boolean));
|
|
85
83
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
// Also check for extensionless files in /databases
|
|
85
|
+
const lsOut = await shell(`adb shell run-as ${pkg} ls -1p ${baseDir}/databases`, { timeout: 3_000, ignoreErrors: true, label: `adb-ls-dbs-${pkg}` });
|
|
86
|
+
if (lsOut) {
|
|
89
87
|
const lsFiles = lsOut.split('\n')
|
|
90
88
|
.map(l => l.trim().replace(/\r/g, ''))
|
|
91
89
|
.filter(Boolean)
|
|
@@ -95,7 +93,6 @@ export async function listDatabases(bundleId, targetPlatform) {
|
|
|
95
93
|
.map(f => `${baseDir}/databases/${f}`);
|
|
96
94
|
foundFiles.push(...lsFiles);
|
|
97
95
|
}
|
|
98
|
-
catch (e) { }
|
|
99
96
|
// Deduplicate
|
|
100
97
|
foundFiles = [...new Set(foundFiles)];
|
|
101
98
|
if (foundFiles.length > 0) {
|
|
@@ -106,7 +103,7 @@ export async function listDatabases(bundleId, targetPlatform) {
|
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
105
|
catch (e) {
|
|
109
|
-
|
|
106
|
+
logger.debug(`Failed to list databases for app: ${pkg}`);
|
|
110
107
|
}
|
|
111
108
|
}
|
|
112
109
|
}
|
|
@@ -130,7 +127,6 @@ export async function listDatabases(bundleId, targetPlatform) {
|
|
|
130
127
|
* Returns the local file path (either the iOS original or a pulled Android copy).
|
|
131
128
|
*/
|
|
132
129
|
export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
|
|
133
|
-
// First, discover available databases
|
|
134
130
|
const locations = await listDatabases(bundleId, targetPlatform);
|
|
135
131
|
if (locations.length === 0) {
|
|
136
132
|
if (targetPlatform) {
|
|
@@ -144,13 +140,12 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
|
|
|
144
140
|
continue;
|
|
145
141
|
let targetDbNames = [];
|
|
146
142
|
if (!dbNameGlob) {
|
|
147
|
-
// Auto-select: find the first .db or .sqlite file in this location
|
|
148
143
|
const preferred = loc.databases.find(d => d.endsWith('.db') || d.endsWith('.sqlite'));
|
|
149
144
|
if (preferred) {
|
|
150
145
|
targetDbNames.push(preferred);
|
|
151
146
|
}
|
|
152
147
|
else if (loc.databases.length > 0) {
|
|
153
|
-
targetDbNames.push(loc.databases[0]);
|
|
148
|
+
targetDbNames.push(loc.databases[0]);
|
|
154
149
|
}
|
|
155
150
|
}
|
|
156
151
|
else {
|
|
@@ -163,32 +158,26 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
|
|
|
163
158
|
if (platform === 'ios') {
|
|
164
159
|
if (!appDir)
|
|
165
160
|
continue;
|
|
166
|
-
// Find the exact path of the targetDbName
|
|
167
|
-
const findCmd = `find "${appDir}" -type f -name "${targetDbName}" -maxdepth 7 -print | head -n 1`;
|
|
168
161
|
try {
|
|
169
|
-
const found =
|
|
162
|
+
const found = await shell(`find "${appDir}" -type f -name "${targetDbName}" -maxdepth 7 -print | head -n 1`, { timeout: 8_000, label: `ios-find-${targetDbName}` });
|
|
170
163
|
if (found && fs.existsSync(found)) {
|
|
171
|
-
|
|
164
|
+
logger.info(`Located iOS DB at: ${found}`);
|
|
172
165
|
synced.push({ localPath: found, dbName: targetDbName, platform: 'ios' });
|
|
173
166
|
}
|
|
174
167
|
}
|
|
175
168
|
catch (e) {
|
|
176
|
-
|
|
169
|
+
logger.warn(`Failed to locate full path for iOS DB: ${targetDbName}`);
|
|
177
170
|
}
|
|
178
171
|
continue;
|
|
179
172
|
}
|
|
180
173
|
// --- Android Logic ---
|
|
181
174
|
if (!appDir || !appDir.includes("::")) {
|
|
182
|
-
|
|
175
|
+
logger.warn(`Invalid Android appDir format: ${appDir}`);
|
|
183
176
|
continue;
|
|
184
177
|
}
|
|
185
178
|
const [targetDbDir, targetPkg] = appDir.split("::");
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
catch (e) {
|
|
190
|
-
console.error(`Failed to force-stop app: ${targetPkg}`);
|
|
191
|
-
}
|
|
179
|
+
// Force-stop is best-effort (ensures WAL is flushed)
|
|
180
|
+
await shell(`adb shell am force-stop ${targetPkg}`, { timeout: 5_000, ignoreErrors: true, label: `adb-force-stop-${targetPkg}` });
|
|
192
181
|
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "rn-sqlite-mcp-"));
|
|
193
182
|
const safeLocalName = targetDbName.replace(/\//g, '_');
|
|
194
183
|
const localDb = path.join(tmpdir, safeLocalName);
|
|
@@ -197,13 +186,13 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
|
|
|
197
186
|
const remoteMain = `${targetDbDir}/${targetDbName}`;
|
|
198
187
|
const remoteWal = `${remoteMain}-wal`;
|
|
199
188
|
const remoteShm = `${remoteMain}-shm`;
|
|
200
|
-
const pullOne = (remote, local) => {
|
|
189
|
+
const pullOne = async (remote, local) => {
|
|
201
190
|
try {
|
|
202
191
|
const remoteBase = path.basename(remote);
|
|
203
192
|
const tmpRemote = `/data/local/tmp/${targetPkg}_${remoteBase}_${Date.now()}`;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
193
|
+
await shell(`adb shell "run-as '${targetPkg}' cat '${remote}' > '${tmpRemote}'"`, { timeout: 10_000, retries: 1, retryDelay: 1_000, label: `adb-cat-${remoteBase}` });
|
|
194
|
+
await shell(`adb pull '${tmpRemote}' '${local}'`, { timeout: 10_000, label: `adb-pull-${remoteBase}` });
|
|
195
|
+
await shell(`adb shell rm '${tmpRemote}'`, { timeout: 5_000, ignoreErrors: true, label: `adb-rm-tmp-${remoteBase}` });
|
|
207
196
|
return fs.existsSync(local) && fs.statSync(local).size > 0;
|
|
208
197
|
}
|
|
209
198
|
catch (e) {
|
|
@@ -212,13 +201,14 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
|
|
|
212
201
|
return false;
|
|
213
202
|
}
|
|
214
203
|
};
|
|
215
|
-
if (!pullOne(remoteMain, localDb)) {
|
|
216
|
-
|
|
204
|
+
if (!(await pullOne(remoteMain, localDb))) {
|
|
205
|
+
logger.warn(`Failed to pull main DB file from Android: ${remoteMain}`);
|
|
217
206
|
continue;
|
|
218
207
|
}
|
|
219
|
-
|
|
220
|
-
pullOne(
|
|
221
|
-
|
|
208
|
+
// WAL and SHM are best-effort
|
|
209
|
+
await pullOne(remoteWal, localWal);
|
|
210
|
+
await pullOne(remoteShm, localShm);
|
|
211
|
+
logger.info(`Pulled Android DB to local temp: ${localDb}`);
|
|
222
212
|
synced.push({ localPath: localDb, dbName: targetDbName, platform: 'android' });
|
|
223
213
|
}
|
|
224
214
|
}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging utility for MCP servers.
|
|
3
|
+
* ALL output goes to stderr to avoid polluting the stdio JSON-RPC transport.
|
|
4
|
+
*/
|
|
5
|
+
const LOG_LEVELS = {
|
|
6
|
+
debug: 0,
|
|
7
|
+
info: 1,
|
|
8
|
+
warn: 2,
|
|
9
|
+
error: 3,
|
|
10
|
+
};
|
|
11
|
+
const minLevel = process.env.MCP_LOG_LEVEL || 'info';
|
|
12
|
+
function shouldLog(level) {
|
|
13
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
|
|
14
|
+
}
|
|
15
|
+
function formatTimestamp() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
function log(level, message, meta) {
|
|
19
|
+
if (!shouldLog(level))
|
|
20
|
+
return;
|
|
21
|
+
const parts = [`[${formatTimestamp()}]`, `[${level.toUpperCase()}]`, message];
|
|
22
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
23
|
+
parts.push(JSON.stringify(meta));
|
|
24
|
+
}
|
|
25
|
+
// CRITICAL: Always stderr — stdout is reserved for MCP JSON-RPC
|
|
26
|
+
process.stderr.write(parts.join(' ') + '\n');
|
|
27
|
+
}
|
|
28
|
+
export const logger = {
|
|
29
|
+
debug: (msg, meta) => log('debug', msg, meta),
|
|
30
|
+
info: (msg, meta) => log('info', msg, meta),
|
|
31
|
+
warn: (msg, meta) => log('warn', msg, meta),
|
|
32
|
+
error: (msg, meta) => log('error', msg, meta),
|
|
33
|
+
};
|
package/dist/shell.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async shell execution wrapper.
|
|
3
|
+
* Replaces all execSync usage to keep the Node.js event loop alive
|
|
4
|
+
* so MCP stdio transport can continue processing heartbeats/messages.
|
|
5
|
+
*/
|
|
6
|
+
import { exec as execCb } from "child_process";
|
|
7
|
+
import { promisify } from "util";
|
|
8
|
+
import { logger } from "./logger.js";
|
|
9
|
+
const execAsync = promisify(execCb);
|
|
10
|
+
function sleep(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Execute a shell command asynchronously with timeout and retry support.
|
|
15
|
+
* This is the core replacement for `execSync` — it does NOT block the event loop.
|
|
16
|
+
*/
|
|
17
|
+
export async function shell(command, options = {}) {
|
|
18
|
+
const { timeout = 10_000, retries = 0, retryDelay = 1_000, ignoreErrors = false, label, } = options;
|
|
19
|
+
const tag = label || command.slice(0, 60);
|
|
20
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
21
|
+
try {
|
|
22
|
+
if (attempt > 0) {
|
|
23
|
+
const delay = retryDelay * Math.pow(2, attempt - 1);
|
|
24
|
+
logger.debug(`Retry ${attempt}/${retries} for "${tag}" after ${delay}ms`);
|
|
25
|
+
await sleep(delay);
|
|
26
|
+
}
|
|
27
|
+
logger.debug(`Executing: "${tag}"`, { timeout, attempt });
|
|
28
|
+
const result = await execAsync(command, {
|
|
29
|
+
timeout,
|
|
30
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB — large schema dumps
|
|
31
|
+
env: { ...process.env },
|
|
32
|
+
});
|
|
33
|
+
return result.stdout.trim();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const isLastAttempt = attempt >= retries;
|
|
37
|
+
if (error.killed) {
|
|
38
|
+
logger.warn(`Command timed out after ${timeout}ms: "${tag}"`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
logger.debug(`Command failed: "${tag}"`, {
|
|
42
|
+
code: error.code,
|
|
43
|
+
stderr: error.stderr?.slice(0, 200),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (isLastAttempt) {
|
|
47
|
+
if (ignoreErrors) {
|
|
48
|
+
logger.debug(`Ignoring error for "${tag}"`);
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Shell command failed: ${tag}\n${error.message || error.stderr || "Unknown error"}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Unreachable, but TypeScript needs it
|
|
56
|
+
return "";
|
|
57
|
+
}
|
package/package.json
CHANGED
package/src/db.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import sqlite3 from "sqlite3";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
2
3
|
|
|
3
4
|
export class Database {
|
|
4
5
|
private db: sqlite3.Database;
|
|
@@ -35,51 +36,96 @@ export class Database {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
interface CachedConnection {
|
|
40
|
+
db: Database;
|
|
41
|
+
lastUsed: number;
|
|
42
|
+
timer: ReturnType<typeof setTimeout>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const CACHE_TTL_MS = 60_000; // close idle connections after 60s
|
|
46
|
+
const connectionCache = new Map<string, CachedConnection>();
|
|
47
|
+
|
|
48
|
+
function getCachedDb(dbPath: string): Database {
|
|
49
|
+
const existing = connectionCache.get(dbPath);
|
|
50
|
+
|
|
51
|
+
if (existing) {
|
|
52
|
+
clearTimeout(existing.timer);
|
|
53
|
+
existing.lastUsed = Date.now();
|
|
54
|
+
existing.timer = setTimeout(() => evictConnection(dbPath), CACHE_TTL_MS);
|
|
55
|
+
return existing.db;
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
const db = new Database(dbPath);
|
|
59
|
+
const timer = setTimeout(() => evictConnection(dbPath), CACHE_TTL_MS);
|
|
60
|
+
|
|
61
|
+
connectionCache.set(dbPath, { db, lastUsed: Date.now(), timer });
|
|
62
|
+
logger.debug(`Opened DB connection: ${dbPath}`);
|
|
63
|
+
return db;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function evictConnection(dbPath: string): Promise<void> {
|
|
67
|
+
const entry = connectionCache.get(dbPath);
|
|
68
|
+
if (!entry) return;
|
|
69
|
+
|
|
70
|
+
connectionCache.delete(dbPath);
|
|
43
71
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
72
|
+
await entry.db.close();
|
|
73
|
+
logger.debug(`Closed idle DB connection: ${dbPath}`);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
logger.warn(`Error closing DB: ${dbPath}`, { error: String(e) });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function closeAllConnections(): Promise<void> {
|
|
80
|
+
const paths = [...connectionCache.keys()];
|
|
81
|
+
for (const p of paths) {
|
|
82
|
+
await evictConnection(p);
|
|
47
83
|
}
|
|
84
|
+
logger.info(`Closed ${paths.length} cached DB connection(s)`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const QUERY_TIMEOUT_MS = 30_000;
|
|
88
|
+
|
|
89
|
+
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
90
|
+
return new Promise<T>((resolve, reject) => {
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
reject(new Error(`Query timed out after ${ms}ms: ${label}`));
|
|
93
|
+
}, ms);
|
|
94
|
+
|
|
95
|
+
promise
|
|
96
|
+
.then((result) => { clearTimeout(timer); resolve(result); })
|
|
97
|
+
.catch((err) => { clearTimeout(timer); reject(err); });
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function queryDb(dbPath: string, sql: string, params: any[] = []): Promise<any[]> {
|
|
102
|
+
const db = getCachedDb(dbPath);
|
|
103
|
+
return withTimeout(db.all(sql, params), QUERY_TIMEOUT_MS, sql.slice(0, 80));
|
|
48
104
|
}
|
|
49
105
|
|
|
50
|
-
/**
|
|
51
|
-
* Returns a detailed schema of all tables in the database.
|
|
52
|
-
*/
|
|
53
106
|
export async function inspectSchema(dbPath: string): Promise<any> {
|
|
54
|
-
const db =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
107
|
+
const db = getCachedDb(dbPath);
|
|
108
|
+
|
|
109
|
+
const tables = await withTimeout(
|
|
110
|
+
db.all<{ name: string }>("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"),
|
|
111
|
+
QUERY_TIMEOUT_MS,
|
|
112
|
+
"inspect_schema:tables"
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const schemaInfo: Record<string, any> = {};
|
|
116
|
+
|
|
117
|
+
for (const table of tables) {
|
|
118
|
+
const columns = await db.all(`PRAGMA table_info("${table.name}");`);
|
|
119
|
+
const createSql = await db.get<{ sql: string }>(
|
|
120
|
+
`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
|
|
121
|
+
[table.name]
|
|
59
122
|
);
|
|
60
123
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const columns = await db.all(
|
|
66
|
-
`PRAGMA table_info("${table.name}");`
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
// Get table creation SQL
|
|
70
|
-
const createSql = await db.get<{ sql: string }>(
|
|
71
|
-
`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
|
|
72
|
-
[table.name]
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
schemaInfo[table.name] = {
|
|
76
|
-
columns,
|
|
77
|
-
createSql: createSql?.sql
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return schemaInfo;
|
|
82
|
-
} finally {
|
|
83
|
-
await db.close();
|
|
124
|
+
schemaInfo[table.name] = {
|
|
125
|
+
columns,
|
|
126
|
+
createSql: createSql?.sql
|
|
127
|
+
};
|
|
84
128
|
}
|
|
129
|
+
|
|
130
|
+
return schemaInfo;
|
|
85
131
|
}
|