nicefox-graphdb 0.1.0
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/LICENSE +21 -0
- package/README.md +417 -0
- package/package.json +78 -0
- package/packages/nicefox-graphdb/LICENSE +21 -0
- package/packages/nicefox-graphdb/README.md +417 -0
- package/packages/nicefox-graphdb/dist/auth.d.ts +66 -0
- package/packages/nicefox-graphdb/dist/auth.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/auth.js +148 -0
- package/packages/nicefox-graphdb/dist/auth.js.map +1 -0
- package/packages/nicefox-graphdb/dist/backup.d.ts +51 -0
- package/packages/nicefox-graphdb/dist/backup.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/backup.js +201 -0
- package/packages/nicefox-graphdb/dist/backup.js.map +1 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.d.ts +17 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.js +121 -0
- package/packages/nicefox-graphdb/dist/cli-helpers.js.map +1 -0
- package/packages/nicefox-graphdb/dist/cli.d.ts +3 -0
- package/packages/nicefox-graphdb/dist/cli.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/cli.js +660 -0
- package/packages/nicefox-graphdb/dist/cli.js.map +1 -0
- package/packages/nicefox-graphdb/dist/db.d.ts +118 -0
- package/packages/nicefox-graphdb/dist/db.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/db.js +245 -0
- package/packages/nicefox-graphdb/dist/db.js.map +1 -0
- package/packages/nicefox-graphdb/dist/executor.d.ts +272 -0
- package/packages/nicefox-graphdb/dist/executor.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/executor.js +3579 -0
- package/packages/nicefox-graphdb/dist/executor.js.map +1 -0
- package/packages/nicefox-graphdb/dist/index.d.ts +54 -0
- package/packages/nicefox-graphdb/dist/index.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/index.js +74 -0
- package/packages/nicefox-graphdb/dist/index.js.map +1 -0
- package/packages/nicefox-graphdb/dist/local.d.ts +7 -0
- package/packages/nicefox-graphdb/dist/local.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/local.js +115 -0
- package/packages/nicefox-graphdb/dist/local.js.map +1 -0
- package/packages/nicefox-graphdb/dist/parser.d.ts +300 -0
- package/packages/nicefox-graphdb/dist/parser.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/parser.js +1891 -0
- package/packages/nicefox-graphdb/dist/parser.js.map +1 -0
- package/packages/nicefox-graphdb/dist/remote.d.ts +6 -0
- package/packages/nicefox-graphdb/dist/remote.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/remote.js +87 -0
- package/packages/nicefox-graphdb/dist/remote.js.map +1 -0
- package/packages/nicefox-graphdb/dist/routes.d.ts +31 -0
- package/packages/nicefox-graphdb/dist/routes.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/routes.js +202 -0
- package/packages/nicefox-graphdb/dist/routes.js.map +1 -0
- package/packages/nicefox-graphdb/dist/translator.d.ts +136 -0
- package/packages/nicefox-graphdb/dist/translator.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/translator.js +4849 -0
- package/packages/nicefox-graphdb/dist/translator.js.map +1 -0
- package/packages/nicefox-graphdb/dist/types.d.ts +133 -0
- package/packages/nicefox-graphdb/dist/types.d.ts.map +1 -0
- package/packages/nicefox-graphdb/dist/types.js +21 -0
- package/packages/nicefox-graphdb/dist/types.js.map +1 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { serve } from "@hono/node-server";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { createServer, GraphDatabase, Executor, BackupManager, generateApiKey, VERSION, } from "./index.js";
|
|
8
|
+
import { formatBytes, getApiKeysPath, loadApiKeys, saveApiKeys, ensureDataDir, calculateColumnWidths, formatTableRow, listProjects, getProjectKeyCount, } from "./cli-helpers.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("nicefox-graphdb")
|
|
12
|
+
.description("NiceFox GraphDB - SQLite-based graph database with Cypher queries")
|
|
13
|
+
.version(VERSION);
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// serve - Start the HTTP server
|
|
16
|
+
// ============================================================================
|
|
17
|
+
program
|
|
18
|
+
.command("serve")
|
|
19
|
+
.description("Start the NiceFox GraphDB HTTP server")
|
|
20
|
+
.option("-p, --port <port>", "Port to listen on", "3000")
|
|
21
|
+
.option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
|
|
22
|
+
.option("-H, --host <host>", "Host to bind to", "localhost")
|
|
23
|
+
.option("-b, --backup <path>", "Backup directory (enables backup endpoints)")
|
|
24
|
+
.action(async (options) => {
|
|
25
|
+
const port = parseInt(options.port, 10);
|
|
26
|
+
const dataPath = path.resolve(options.data);
|
|
27
|
+
const host = options.host;
|
|
28
|
+
const backupPath = options.backup ? path.resolve(options.backup) : undefined;
|
|
29
|
+
// Ensure data directory exists
|
|
30
|
+
ensureDataDir(dataPath);
|
|
31
|
+
// Load API keys from data directory
|
|
32
|
+
let apiKeys;
|
|
33
|
+
const keysFile = getApiKeysPath(dataPath);
|
|
34
|
+
if (fs.existsSync(keysFile)) {
|
|
35
|
+
try {
|
|
36
|
+
apiKeys = JSON.parse(fs.readFileSync(keysFile, "utf-8"));
|
|
37
|
+
console.log(`Loaded ${Object.keys(apiKeys).length} API key(s) from ${keysFile}`);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
console.error(`Failed to load API keys from ${keysFile}:`, err);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const { app, dbManager } = createServer({
|
|
45
|
+
port,
|
|
46
|
+
dataPath,
|
|
47
|
+
backupPath,
|
|
48
|
+
apiKeys,
|
|
49
|
+
});
|
|
50
|
+
const authStatus = apiKeys ? "enabled" : "disabled";
|
|
51
|
+
const backupStatus = backupPath ? backupPath.slice(0, 30) : "disabled";
|
|
52
|
+
console.log(`
|
|
53
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
54
|
+
║ NiceFox GraphDB Server v${VERSION} ║
|
|
55
|
+
╠═══════════════════════════════════════════════════════════╣
|
|
56
|
+
║ Endpoint: http://${host}:${port.toString().padEnd(5)} ║
|
|
57
|
+
║ Data: ${dataPath.slice(0, 43).padEnd(43)} ║
|
|
58
|
+
║ Backups: ${backupStatus.padEnd(43)} ║
|
|
59
|
+
║ Auth: ${authStatus.padEnd(43)} ║
|
|
60
|
+
║ ║
|
|
61
|
+
║ Routes: ║
|
|
62
|
+
║ POST /query/:env/:project - Execute Cypher queries ║
|
|
63
|
+
║ GET /health - Health check ║
|
|
64
|
+
║ GET /admin/list - List all projects ║
|
|
65
|
+
║ GET /admin/backup - Backup status ║
|
|
66
|
+
║ POST /admin/backup - Trigger backup ║
|
|
67
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
68
|
+
`);
|
|
69
|
+
serve({
|
|
70
|
+
fetch: app.fetch,
|
|
71
|
+
port,
|
|
72
|
+
hostname: host,
|
|
73
|
+
});
|
|
74
|
+
// Handle graceful shutdown
|
|
75
|
+
process.on("SIGINT", () => {
|
|
76
|
+
console.log("\nShutting down...");
|
|
77
|
+
dbManager.closeAll();
|
|
78
|
+
process.exit(0);
|
|
79
|
+
});
|
|
80
|
+
process.on("SIGTERM", () => {
|
|
81
|
+
console.log("\nShutting down...");
|
|
82
|
+
dbManager.closeAll();
|
|
83
|
+
process.exit(0);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// create - Create a new project (both production and test DBs)
|
|
88
|
+
// ============================================================================
|
|
89
|
+
program
|
|
90
|
+
.command("create <project>")
|
|
91
|
+
.description("Create a new project with databases and API key")
|
|
92
|
+
.option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
|
|
93
|
+
.option("--no-key", "Skip API key generation")
|
|
94
|
+
.action((project, options) => {
|
|
95
|
+
const dataPath = path.resolve(options.data);
|
|
96
|
+
ensureDataDir(dataPath);
|
|
97
|
+
const envs = ["production", "test"];
|
|
98
|
+
let created = false;
|
|
99
|
+
for (const env of envs) {
|
|
100
|
+
const dbPath = path.join(dataPath, env, `${project}.db`);
|
|
101
|
+
const dbDir = path.dirname(dbPath);
|
|
102
|
+
if (!fs.existsSync(dbDir)) {
|
|
103
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
if (fs.existsSync(dbPath)) {
|
|
106
|
+
console.log(` [skip] ${env}/${project}.db already exists`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const db = new GraphDatabase(dbPath);
|
|
110
|
+
db.initialize();
|
|
111
|
+
db.close();
|
|
112
|
+
console.log(` [created] ${env}/${project}.db`);
|
|
113
|
+
created = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Generate API keys for the project (one for production, one for test)
|
|
117
|
+
if (options.key && created) {
|
|
118
|
+
const keys = loadApiKeys(dataPath);
|
|
119
|
+
const prodKey = generateApiKey();
|
|
120
|
+
const testKey = generateApiKey();
|
|
121
|
+
keys[prodKey] = { project, env: "production" };
|
|
122
|
+
keys[testKey] = { project, env: "test" };
|
|
123
|
+
saveApiKeys(dataPath, keys);
|
|
124
|
+
console.log(`\nProject '${project}' is ready.`);
|
|
125
|
+
console.log(`\nAPI Keys:`);
|
|
126
|
+
console.log(` production: ${prodKey}`);
|
|
127
|
+
console.log(` test: ${testKey}`);
|
|
128
|
+
}
|
|
129
|
+
else if (!created) {
|
|
130
|
+
console.log(`\nProject '${project}' already exists.`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(`\nProject '${project}' is ready (no API key generated).`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// delete - Delete a project (both production and test DBs)
|
|
138
|
+
// ============================================================================
|
|
139
|
+
program
|
|
140
|
+
.command("delete <project>")
|
|
141
|
+
.description("Delete a project (removes databases and API keys)")
|
|
142
|
+
.option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
|
|
143
|
+
.option("-f, --force", "Skip confirmation prompt", false)
|
|
144
|
+
.action((project, options) => {
|
|
145
|
+
const dataPath = path.resolve(options.data);
|
|
146
|
+
const envs = ["production", "test"];
|
|
147
|
+
// Check what exists
|
|
148
|
+
const existing = [];
|
|
149
|
+
for (const env of envs) {
|
|
150
|
+
const dbPath = path.join(dataPath, env, `${project}.db`);
|
|
151
|
+
if (fs.existsSync(dbPath)) {
|
|
152
|
+
existing.push(`${env}/${project}.db`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Check for API keys
|
|
156
|
+
const keys = loadApiKeys(dataPath);
|
|
157
|
+
const projectKeys = Object.entries(keys).filter(([_, config]) => config.project === project);
|
|
158
|
+
if (existing.length === 0 && projectKeys.length === 0) {
|
|
159
|
+
console.log(`Project '${project}' does not exist.`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
if (!options.force) {
|
|
163
|
+
console.log(`This will delete:`);
|
|
164
|
+
for (const file of existing) {
|
|
165
|
+
console.log(` - ${file}`);
|
|
166
|
+
}
|
|
167
|
+
if (projectKeys.length > 0) {
|
|
168
|
+
console.log(` - ${projectKeys.length} API key(s)`);
|
|
169
|
+
}
|
|
170
|
+
console.log(`\nUse --force to confirm deletion.`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
// Delete database files
|
|
174
|
+
for (const env of envs) {
|
|
175
|
+
const dbPath = path.join(dataPath, env, `${project}.db`);
|
|
176
|
+
if (fs.existsSync(dbPath)) {
|
|
177
|
+
fs.unlinkSync(dbPath);
|
|
178
|
+
console.log(` [deleted] ${env}/${project}.db`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Delete API keys for this project
|
|
182
|
+
if (projectKeys.length > 0) {
|
|
183
|
+
for (const [key] of projectKeys) {
|
|
184
|
+
delete keys[key];
|
|
185
|
+
}
|
|
186
|
+
saveApiKeys(dataPath, keys);
|
|
187
|
+
console.log(` [deleted] ${projectKeys.length} API key(s)`);
|
|
188
|
+
}
|
|
189
|
+
console.log(`\nProject '${project}' has been deleted.`);
|
|
190
|
+
});
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// list - List all projects
|
|
193
|
+
// ============================================================================
|
|
194
|
+
program
|
|
195
|
+
.command("list")
|
|
196
|
+
.description("List all projects and their environments")
|
|
197
|
+
.option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
|
|
198
|
+
.action((options) => {
|
|
199
|
+
const dataPath = path.resolve(options.data);
|
|
200
|
+
if (!fs.existsSync(dataPath)) {
|
|
201
|
+
console.log("No data directory found. Run 'nicefox-graphdb create <project>' first.");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const projects = listProjects(dataPath);
|
|
205
|
+
if (projects.size === 0) {
|
|
206
|
+
console.log("No projects found.");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Load API keys to show key count per project
|
|
210
|
+
const keys = loadApiKeys(dataPath);
|
|
211
|
+
console.log("\nProjects:\n");
|
|
212
|
+
for (const [project, envs] of projects) {
|
|
213
|
+
const envList = envs.map((e) => (e === "production" ? "prod" : "test")).join(", ");
|
|
214
|
+
const keyCount = getProjectKeyCount(keys, project);
|
|
215
|
+
const keyInfo = keyCount > 0 ? ` (${keyCount} key${keyCount > 1 ? "s" : ""})` : " (no keys)";
|
|
216
|
+
console.log(` ${project} [${envList}]${keyInfo}`);
|
|
217
|
+
}
|
|
218
|
+
console.log("");
|
|
219
|
+
});
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// query - Execute a Cypher query
|
|
222
|
+
// ============================================================================
|
|
223
|
+
program
|
|
224
|
+
.command("query <env> <project> <cypher>")
|
|
225
|
+
.description("Execute a Cypher query against a project database")
|
|
226
|
+
.option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
|
|
227
|
+
.option("-p, --params <json>", "Query parameters as JSON", "{}")
|
|
228
|
+
.option("--json", "Output raw JSON", false)
|
|
229
|
+
.action((env, project, cypher, options) => {
|
|
230
|
+
const dataPath = path.resolve(options.data);
|
|
231
|
+
if (env !== "production" && env !== "test") {
|
|
232
|
+
console.error(`Invalid environment: ${env}. Must be 'production' or 'test'.`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
const dbPath = path.join(dataPath, env, `${project}.db`);
|
|
236
|
+
if (!fs.existsSync(dbPath)) {
|
|
237
|
+
console.error(`Database not found: ${dbPath}`);
|
|
238
|
+
console.error(`Run 'nicefox-graphdb create ${project}' first.`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
let params = {};
|
|
242
|
+
try {
|
|
243
|
+
params = JSON.parse(options.params);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
console.error("Invalid JSON in --params");
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
const db = new GraphDatabase(dbPath);
|
|
250
|
+
db.initialize();
|
|
251
|
+
const executor = new Executor(db);
|
|
252
|
+
const result = executor.execute(cypher, params);
|
|
253
|
+
db.close();
|
|
254
|
+
if (!result.success) {
|
|
255
|
+
console.error(`Query failed: ${result.error.message}`);
|
|
256
|
+
if (result.error.position !== undefined) {
|
|
257
|
+
console.error(` at position ${result.error.position} (line ${result.error.line}, column ${result.error.column})`);
|
|
258
|
+
}
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
if (options.json) {
|
|
262
|
+
console.log(JSON.stringify(result, null, 2));
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
console.log(`\nResults (${result.meta.count} rows, ${result.meta.time_ms}ms):\n`);
|
|
266
|
+
if (result.data.length === 0) {
|
|
267
|
+
console.log(" (no results)");
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
// Print as table
|
|
271
|
+
const columns = Object.keys(result.data[0]);
|
|
272
|
+
printTable(columns, result.data);
|
|
273
|
+
}
|
|
274
|
+
console.log("");
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
// ============================================================================
|
|
278
|
+
// wipe - Wipe a test database (refuses on production)
|
|
279
|
+
// ============================================================================
|
|
280
|
+
program
|
|
281
|
+
.command("wipe <project>")
|
|
282
|
+
.description("Wipe test database for a project (production is protected)")
|
|
283
|
+
.option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
|
|
284
|
+
.option("-f, --force", "Skip confirmation prompt", false)
|
|
285
|
+
.action((project, options) => {
|
|
286
|
+
const dataPath = path.resolve(options.data);
|
|
287
|
+
const dbPath = path.join(dataPath, "test", `${project}.db`);
|
|
288
|
+
if (!fs.existsSync(dbPath)) {
|
|
289
|
+
console.error(`Test database not found: ${dbPath}`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
if (!options.force) {
|
|
293
|
+
console.log(`This will delete all data in test/${project}.db`);
|
|
294
|
+
console.log(`\nUse --force to confirm.`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
const db = new GraphDatabase(dbPath);
|
|
298
|
+
db.initialize();
|
|
299
|
+
db.execute("DELETE FROM edges");
|
|
300
|
+
db.execute("DELETE FROM nodes");
|
|
301
|
+
db.close();
|
|
302
|
+
console.log(`Wiped test/${project}.db`);
|
|
303
|
+
});
|
|
304
|
+
// ============================================================================
|
|
305
|
+
// clone - Clone production to test
|
|
306
|
+
// ============================================================================
|
|
307
|
+
program
|
|
308
|
+
.command("clone <project>")
|
|
309
|
+
.description("Clone production database to test (overwrites test)")
|
|
310
|
+
.option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
|
|
311
|
+
.option("-f, --force", "Skip confirmation prompt", false)
|
|
312
|
+
.action((project, options) => {
|
|
313
|
+
const dataPath = path.resolve(options.data);
|
|
314
|
+
const prodPath = path.join(dataPath, "production", `${project}.db`);
|
|
315
|
+
const testPath = path.join(dataPath, "test", `${project}.db`);
|
|
316
|
+
if (!fs.existsSync(prodPath)) {
|
|
317
|
+
console.error(`Production database not found: ${prodPath}`);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
if (!options.force) {
|
|
321
|
+
console.log(`This will overwrite test/${project}.db with production/${project}.db`);
|
|
322
|
+
console.log(`\nUse --force to confirm.`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
// Ensure test directory exists
|
|
326
|
+
const testDir = path.dirname(testPath);
|
|
327
|
+
if (!fs.existsSync(testDir)) {
|
|
328
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
329
|
+
}
|
|
330
|
+
// Copy file
|
|
331
|
+
fs.copyFileSync(prodPath, testPath);
|
|
332
|
+
console.log(`Cloned production/${project}.db → test/${project}.db`);
|
|
333
|
+
});
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// migrate - Migrate databases from old label format to JSON array
|
|
336
|
+
// ============================================================================
|
|
337
|
+
program
|
|
338
|
+
.command("migrate")
|
|
339
|
+
.description("Migrate databases from old label format (TEXT) to new format (JSON array)")
|
|
340
|
+
.option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
|
|
341
|
+
.option("-p, --project <name>", "Migrate specific project only")
|
|
342
|
+
.option("--dry-run", "Preview changes without modifying data", false)
|
|
343
|
+
.option("-f, --force", "Skip confirmation prompt", false)
|
|
344
|
+
.action((options) => {
|
|
345
|
+
const dataPath = path.resolve(options.data);
|
|
346
|
+
if (!fs.existsSync(dataPath)) {
|
|
347
|
+
console.error(`Data directory not found: ${dataPath}`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
// Find all databases to migrate
|
|
351
|
+
const databases = [];
|
|
352
|
+
for (const env of ["production", "test"]) {
|
|
353
|
+
const envPath = path.join(dataPath, env);
|
|
354
|
+
if (fs.existsSync(envPath)) {
|
|
355
|
+
const files = fs.readdirSync(envPath).filter((f) => f.endsWith(".db"));
|
|
356
|
+
for (const file of files) {
|
|
357
|
+
const project = file.replace(".db", "");
|
|
358
|
+
if (!options.project || options.project === project) {
|
|
359
|
+
databases.push({
|
|
360
|
+
env,
|
|
361
|
+
project,
|
|
362
|
+
path: path.join(envPath, file),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (databases.length === 0) {
|
|
369
|
+
if (options.project) {
|
|
370
|
+
console.error(`Project '${options.project}' not found.`);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
console.log("No databases found.");
|
|
374
|
+
}
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
// Check what needs migration
|
|
378
|
+
console.log("\nChecking databases for migration...\n");
|
|
379
|
+
const toMigrate = [];
|
|
380
|
+
for (const dbInfo of databases) {
|
|
381
|
+
// Open database directly with better-sqlite3 to avoid schema initialization
|
|
382
|
+
const db = new Database(dbInfo.path);
|
|
383
|
+
try {
|
|
384
|
+
// Check if nodes table exists
|
|
385
|
+
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='nodes'").get();
|
|
386
|
+
if (!tableExists) {
|
|
387
|
+
console.log(` ${dbInfo.env}/${dbInfo.project}.db: no nodes table (skipped)`);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
// Count nodes that need migration (label is not valid JSON)
|
|
391
|
+
const result = db.prepare("SELECT COUNT(*) as count FROM nodes WHERE json_valid(label) = 0").get();
|
|
392
|
+
if (result.count > 0) {
|
|
393
|
+
toMigrate.push({ ...dbInfo, count: result.count });
|
|
394
|
+
console.log(` ${dbInfo.env}/${dbInfo.project}.db: ${result.count} node(s) need migration`);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
console.log(` ${dbInfo.env}/${dbInfo.project}.db: already migrated`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
db.close();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (toMigrate.length === 0) {
|
|
405
|
+
console.log("\nAll databases are already migrated.");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
// Dry run - just show what would be done
|
|
409
|
+
if (options.dryRun) {
|
|
410
|
+
console.log(`\n[dry-run] Would migrate ${toMigrate.length} database(s)`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// Confirm before migrating
|
|
414
|
+
if (!options.force) {
|
|
415
|
+
console.log(`\nThis will migrate ${toMigrate.length} database(s).`);
|
|
416
|
+
console.log("Use --force to confirm, or --dry-run to preview.");
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
// Perform migration
|
|
420
|
+
console.log("\nMigrating...\n");
|
|
421
|
+
let successCount = 0;
|
|
422
|
+
let failCount = 0;
|
|
423
|
+
for (const dbInfo of toMigrate) {
|
|
424
|
+
const db = new Database(dbInfo.path);
|
|
425
|
+
try {
|
|
426
|
+
const start = Date.now();
|
|
427
|
+
// Migrate: wrap plain text labels in JSON array
|
|
428
|
+
const result = db.prepare("UPDATE nodes SET label = json_array(label) WHERE json_valid(label) = 0").run();
|
|
429
|
+
const duration = Date.now() - start;
|
|
430
|
+
console.log(` ${dbInfo.env}/${dbInfo.project}.db: ${result.changes} node(s) migrated (${duration}ms)`);
|
|
431
|
+
successCount++;
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
435
|
+
console.error(` ${dbInfo.env}/${dbInfo.project}.db: FAILED - ${message}`);
|
|
436
|
+
failCount++;
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
db.close();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
console.log(`\nMigration complete: ${successCount} database(s) updated`);
|
|
443
|
+
if (failCount > 0) {
|
|
444
|
+
console.log(` ${failCount} database(s) failed`);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
// ============================================================================
|
|
449
|
+
// backup - Backup databases
|
|
450
|
+
// ============================================================================
|
|
451
|
+
program
|
|
452
|
+
.command("backup")
|
|
453
|
+
.description("Backup production databases")
|
|
454
|
+
.option("-d, --data <path>", "Data directory for databases", "/var/data/nicefox-graphdb")
|
|
455
|
+
.option("-o, --output <path>", "Backup output directory", "./backups")
|
|
456
|
+
.option("-p, --project <name>", "Backup specific project only")
|
|
457
|
+
.option("--include-test", "Also backup test databases", false)
|
|
458
|
+
.option("--keep <count>", "Number of backups to keep per project", "5")
|
|
459
|
+
.option("--status", "Show backup status only", false)
|
|
460
|
+
.action(async (options) => {
|
|
461
|
+
const dataPath = path.resolve(options.data);
|
|
462
|
+
const backupPath = path.resolve(options.output);
|
|
463
|
+
const keepCount = parseInt(options.keep, 10);
|
|
464
|
+
const manager = new BackupManager(backupPath);
|
|
465
|
+
// Status only mode
|
|
466
|
+
if (options.status) {
|
|
467
|
+
const status = manager.getBackupStatus();
|
|
468
|
+
console.log("\nBackup Status:\n");
|
|
469
|
+
console.log(` Total backups: ${status.totalBackups}`);
|
|
470
|
+
console.log(` Total size: ${formatBytes(status.totalSizeBytes)}`);
|
|
471
|
+
console.log(` Projects: ${status.projects.join(", ") || "(none)"}`);
|
|
472
|
+
if (status.oldestBackup) {
|
|
473
|
+
console.log(` Oldest backup: ${status.oldestBackup}`);
|
|
474
|
+
}
|
|
475
|
+
if (status.newestBackup) {
|
|
476
|
+
console.log(` Newest backup: ${status.newestBackup}`);
|
|
477
|
+
}
|
|
478
|
+
console.log("");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// Check data directory exists
|
|
482
|
+
if (!fs.existsSync(dataPath)) {
|
|
483
|
+
console.error(`Data directory not found: ${dataPath}`);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
// Single project backup
|
|
487
|
+
if (options.project) {
|
|
488
|
+
const sourcePath = path.join(dataPath, "production", `${options.project}.db`);
|
|
489
|
+
if (!fs.existsSync(sourcePath)) {
|
|
490
|
+
console.error(`Project not found: ${options.project}`);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
console.log(`Backing up ${options.project}...`);
|
|
494
|
+
const result = await manager.backupDatabase(sourcePath, options.project);
|
|
495
|
+
if (result.success) {
|
|
496
|
+
console.log(` [success] ${result.backupPath}`);
|
|
497
|
+
console.log(` Size: ${formatBytes(result.sizeBytes || 0)}, Duration: ${result.durationMs}ms`);
|
|
498
|
+
// Cleanup old backups
|
|
499
|
+
const deleted = manager.cleanOldBackups(options.project, keepCount);
|
|
500
|
+
if (deleted > 0) {
|
|
501
|
+
console.log(` Cleaned up ${deleted} old backup(s)`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
console.error(` [failed] ${result.error}`);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Backup all databases
|
|
511
|
+
console.log(`\nBacking up databases from ${dataPath}...\n`);
|
|
512
|
+
const results = await manager.backupAll(dataPath, { includeTest: options.includeTest });
|
|
513
|
+
if (results.length === 0) {
|
|
514
|
+
console.log("No databases found to backup.");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
let successCount = 0;
|
|
518
|
+
let failCount = 0;
|
|
519
|
+
for (const result of results) {
|
|
520
|
+
if (result.success) {
|
|
521
|
+
console.log(` [success] ${result.project} → ${path.basename(result.backupPath)}`);
|
|
522
|
+
successCount++;
|
|
523
|
+
// Cleanup old backups
|
|
524
|
+
const deleted = manager.cleanOldBackups(result.project, keepCount);
|
|
525
|
+
if (deleted > 0) {
|
|
526
|
+
console.log(` Cleaned up ${deleted} old backup(s)`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
console.log(` [failed] ${result.project}: ${result.error}`);
|
|
531
|
+
failCount++;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
console.log(`\nBackup complete: ${successCount} succeeded, ${failCount} failed`);
|
|
535
|
+
if (failCount > 0) {
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
// ============================================================================
|
|
540
|
+
// apikey - API Key Management
|
|
541
|
+
// ============================================================================
|
|
542
|
+
const apikey = program
|
|
543
|
+
.command("apikey")
|
|
544
|
+
.description("Manage API keys for project access");
|
|
545
|
+
apikey
|
|
546
|
+
.command("add <project>")
|
|
547
|
+
.description("Generate and add a new API key for a project")
|
|
548
|
+
.option("-d, --data <path>", "Data directory", "/var/data/nicefox-graphdb")
|
|
549
|
+
.option("-e, --env <env>", "Restrict to specific environment (production/test)")
|
|
550
|
+
.option("--admin", "Create an admin key (ignores project/env)", false)
|
|
551
|
+
.action((project, options) => {
|
|
552
|
+
const dataPath = path.resolve(options.data);
|
|
553
|
+
const keys = loadApiKeys(dataPath);
|
|
554
|
+
// Generate new key
|
|
555
|
+
const newKey = generateApiKey();
|
|
556
|
+
// Build config
|
|
557
|
+
const config = {};
|
|
558
|
+
if (options.admin) {
|
|
559
|
+
config.admin = true;
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
config.project = project;
|
|
563
|
+
if (options.env) {
|
|
564
|
+
if (options.env !== "production" && options.env !== "test") {
|
|
565
|
+
console.error(`Invalid environment: ${options.env}. Must be 'production' or 'test'.`);
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
config.env = options.env;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
keys[newKey] = config;
|
|
572
|
+
saveApiKeys(dataPath, keys);
|
|
573
|
+
console.log(`\nAPI Key: ${newKey}`);
|
|
574
|
+
if (options.admin) {
|
|
575
|
+
console.log(`Access: admin (full access)`);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
console.log(`Project: ${project}`);
|
|
579
|
+
console.log(`Env: ${options.env || "all"}`);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
apikey
|
|
583
|
+
.command("list")
|
|
584
|
+
.description("List all API keys (shows prefixes only)")
|
|
585
|
+
.option("-d, --data <path>", "Data directory", "/var/data/nicefox-graphdb")
|
|
586
|
+
.action((options) => {
|
|
587
|
+
const dataPath = path.resolve(options.data);
|
|
588
|
+
const keys = loadApiKeys(dataPath);
|
|
589
|
+
if (Object.keys(keys).length === 0) {
|
|
590
|
+
console.log("No API keys configured.");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
console.log("\nAPI Keys:\n");
|
|
594
|
+
console.log(" Prefix | Access");
|
|
595
|
+
console.log(" ------------+---------------------------");
|
|
596
|
+
for (const [key, config] of Object.entries(keys)) {
|
|
597
|
+
const prefix = key.slice(0, 8) + "...";
|
|
598
|
+
let access;
|
|
599
|
+
if (config.admin) {
|
|
600
|
+
access = "admin";
|
|
601
|
+
}
|
|
602
|
+
else if (config.project) {
|
|
603
|
+
access = config.env ? `${config.project}/${config.env}` : `${config.project}/*`;
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
access = "*/*";
|
|
607
|
+
}
|
|
608
|
+
console.log(` ${prefix.padEnd(12)}| ${access}`);
|
|
609
|
+
}
|
|
610
|
+
console.log("");
|
|
611
|
+
});
|
|
612
|
+
apikey
|
|
613
|
+
.command("remove <prefix>")
|
|
614
|
+
.description("Remove an API key by its prefix (first 8+ characters)")
|
|
615
|
+
.option("-d, --data <path>", "Data directory", "/var/data/nicefox-graphdb")
|
|
616
|
+
.action((prefix, options) => {
|
|
617
|
+
const dataPath = path.resolve(options.data);
|
|
618
|
+
const keys = loadApiKeys(dataPath);
|
|
619
|
+
// Find key by prefix
|
|
620
|
+
const matchingKeys = Object.keys(keys).filter((k) => k.startsWith(prefix));
|
|
621
|
+
if (matchingKeys.length === 0) {
|
|
622
|
+
console.error(`No key found with prefix: ${prefix}`);
|
|
623
|
+
process.exit(1);
|
|
624
|
+
}
|
|
625
|
+
if (matchingKeys.length > 1) {
|
|
626
|
+
console.error(`Multiple keys match prefix '${prefix}'. Please be more specific.`);
|
|
627
|
+
for (const key of matchingKeys) {
|
|
628
|
+
console.error(` - ${key.slice(0, 12)}...`);
|
|
629
|
+
}
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
const keyToRemove = matchingKeys[0];
|
|
633
|
+
const config = keys[keyToRemove];
|
|
634
|
+
delete keys[keyToRemove];
|
|
635
|
+
saveApiKeys(dataPath, keys);
|
|
636
|
+
console.log(`\nRemoved API key: ${keyToRemove.slice(0, 8)}...`);
|
|
637
|
+
if (config.admin) {
|
|
638
|
+
console.log(`Access: admin`);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
console.log(`Access: ${config.project || "*"}/${config.env || "*"}`);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
// ============================================================================
|
|
645
|
+
// Helpers
|
|
646
|
+
// ============================================================================
|
|
647
|
+
function printTable(columns, rows) {
|
|
648
|
+
const widths = calculateColumnWidths(columns, rows);
|
|
649
|
+
// Print header
|
|
650
|
+
const header = columns.map((col) => col.padEnd(widths[col])).join(" | ");
|
|
651
|
+
console.log(` ${header}`);
|
|
652
|
+
console.log(` ${columns.map((col) => "-".repeat(widths[col])).join("-+-")}`);
|
|
653
|
+
// Print rows
|
|
654
|
+
for (const row of rows) {
|
|
655
|
+
console.log(` ${formatTableRow(columns, row, widths)}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Parse and run
|
|
659
|
+
program.parse();
|
|
660
|
+
//# sourceMappingURL=cli.js.map
|