admindb 2.1.1 → 2.2.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/README.md +41 -12
- package/dist/app.js +230 -1
- package/dist/auth/config.js +121 -1
- package/dist/auth/crypto.js +108 -1
- package/dist/auth/index.js +21 -1
- package/dist/auth/middleware.js +85 -1
- package/dist/auth/routes.js +70 -1
- package/dist/auth/types.js +7 -1
- package/dist/cli/args.js +320 -1
- package/dist/cli/config.js +128 -1
- package/dist/cli/index.js +20 -1
- package/dist/cli/runner.js +211 -1
- package/dist/cli.js +4 -1
- package/dist/data/chain.d.ts +1 -0
- package/dist/data/chain.js +707 -0
- package/dist/data/datasets.js +129 -1
- package/dist/data/detector.d.ts +1 -1
- package/dist/data/detector.js +511 -1
- package/dist/data/engine.d.ts +1 -1
- package/dist/data/engine.js +245 -1
- package/dist/data/index.d.ts +1 -1
- package/dist/data/index.js +22 -1
- package/dist/data/strategies.d.ts +1 -1
- package/dist/data/strategies.js +339 -1
- package/dist/data/types.d.ts +1 -1
- package/dist/data/types.js +5 -1
- package/dist/db/database.d.ts +1 -1
- package/dist/db/database.js +477 -1
- package/dist/db/export.d.ts +1 -1
- package/dist/db/export.js +90 -1
- package/dist/db/filters.js +142 -1
- package/dist/db/index.js +24 -1
- package/dist/db/introspection.js +167 -1
- package/dist/db/manager.js +351 -1
- package/dist/db/migrations.js +107 -1
- package/dist/db/postgres.d.ts +1 -1
- package/dist/db/postgres.js +735 -1
- package/dist/db/types.d.ts +1 -1
- package/dist/db/types.js +6 -1
- package/dist/index.js +52 -1
- package/dist/public/css/app.css +1 -1
- package/dist/public/js/api.js +1 -1
- package/dist/public/js/browse.js +1 -1
- package/dist/public/js/common.js +1 -0
- package/dist/public/js/databases.js +1 -1
- package/dist/public/js/designer.js +1 -1
- package/dist/public/js/forms.js +1 -1
- package/dist/public/js/home.js +1 -0
- package/dist/public/js/inspector.js +1 -1
- package/dist/public/js/query.js +1 -1
- package/dist/public/js/schema.js +1 -1
- package/dist/public/js/seed.js +1 -1
- package/dist/routes/api/erd.d.ts +1 -0
- package/dist/routes/api/erd.js +50 -0
- package/dist/routes/api/helpers.d.ts +1 -1
- package/dist/routes/api/helpers.js +163 -1
- package/dist/routes/api/import-export.js +83 -1
- package/dist/routes/api/index.js +48 -1
- package/dist/routes/api/query.js +78 -1
- package/dist/routes/api/rows.js +362 -1
- package/dist/routes/api/seed.js +153 -1
- package/dist/routes/api/tables.js +231 -1
- package/dist/routes/databases.js +228 -1
- package/dist/routes/index.js +19 -1
- package/dist/routes/pages.js +397 -1
- package/dist/serverless.js +174 -1
- package/dist/sql/classifier.js +24 -1
- package/dist/sql/error-analyzer.d.ts +1 -0
- package/dist/sql/error-analyzer.js +550 -0
- package/dist/sql/generator.js +319 -1
- package/dist/sql/index.js +18 -1
- package/dist/types/api.js +2 -1
- package/dist/utils/colors.js +54 -1
- package/dist/utils/common.d.ts +1 -1
- package/dist/utils/common.js +225 -1
- package/dist/utils/csv.js +67 -1
- package/dist/utils/datatype.js +181 -1
- package/dist/utils/icons.js +81 -1
- package/dist/utils/index.js +20 -1
- package/dist/utils/logger.js +36 -1
- package/dist/views/layouts/main.hbs +14 -1
- package/dist/views/pages/erd.hbs +1241 -0
- package/dist/views/pages/home.hbs +98 -2
- package/dist/views/pages/info.hbs +107 -0
- package/dist/views/pages/query.hbs +54 -12
- package/dist/views/pages/seed.hbs +435 -171
- package/dist/views/partials/sidebar.hbs +12 -0
- package/docs/API.md +6 -1
- package/docs/EXAMPLES.md +3 -0
- package/package.json +5 -4
- package/dist/public/js/icons.js +0 -1
package/dist/routes/api/seed.js
CHANGED
|
@@ -1 +1,153 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerSeedRoutes = registerSeedRoutes;
|
|
4
|
+
const generator_1 = require("../../sql/generator");
|
|
5
|
+
const index_1 = require("../../data/index");
|
|
6
|
+
const helpers_1 = require("./helpers");
|
|
7
|
+
function registerSeedRoutes(router, ctx) {
|
|
8
|
+
const { db } = ctx;
|
|
9
|
+
router.get('/api/tables/:table/seed/config', (0, helpers_1.wrap)(async (req, res) => {
|
|
10
|
+
const info = await (0, helpers_1.requireTable)(db, req.params.table);
|
|
11
|
+
if (!info)
|
|
12
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
13
|
+
(0, helpers_1.ok)(res, { table: req.params.table, columns: (0, index_1.buildColumnConfigs)(info), maxRows: index_1.MAX_SEED_ROWS });
|
|
14
|
+
}));
|
|
15
|
+
router.get('/api/tables/:table/seed/plan', (0, helpers_1.wrap)(async (req, res) => {
|
|
16
|
+
const info = await (0, helpers_1.requireTable)(db, req.params.table);
|
|
17
|
+
if (!info)
|
|
18
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
19
|
+
(0, helpers_1.ok)(res, { table: req.params.table, columns: (0, index_1.buildColumnConfigs)(info), maxRows: index_1.MAX_SEED_ROWS });
|
|
20
|
+
}));
|
|
21
|
+
router.post('/api/tables/:table/seed/generate', (0, helpers_1.wrap)(async (req, res) => {
|
|
22
|
+
const info = await (0, helpers_1.requireTable)(db, req.params.table);
|
|
23
|
+
if (!info)
|
|
24
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
25
|
+
const { count, plan } = (0, helpers_1.parseSeedRequest)(req.body);
|
|
26
|
+
const configs = (0, index_1.buildColumnConfigs)(info);
|
|
27
|
+
const gen = await (0, index_1.generateRows)(db, info, configs, count, plan);
|
|
28
|
+
(0, helpers_1.ok)(res, {
|
|
29
|
+
table: req.params.table,
|
|
30
|
+
count: gen.rows.length,
|
|
31
|
+
sql: (0, index_1.buildSeedInsertSql)(req.params.table, gen.rows),
|
|
32
|
+
previewRows: gen.previewRows,
|
|
33
|
+
warnings: gen.warnings,
|
|
34
|
+
});
|
|
35
|
+
}));
|
|
36
|
+
router.post('/api/tables/:table/seed/preview', (0, helpers_1.wrap)(async (req, res) => {
|
|
37
|
+
const info = await (0, helpers_1.requireTable)(db, req.params.table);
|
|
38
|
+
if (!info)
|
|
39
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
40
|
+
const { count, plan } = (0, helpers_1.parseSeedRequest)(req.body);
|
|
41
|
+
const configs = (0, index_1.buildColumnConfigs)(info);
|
|
42
|
+
const gen = await (0, index_1.generateRows)(db, info, configs, Math.min(50, count), plan);
|
|
43
|
+
(0, helpers_1.ok)(res, {
|
|
44
|
+
table: req.params.table,
|
|
45
|
+
previewRows: gen.previewRows,
|
|
46
|
+
warnings: gen.warnings,
|
|
47
|
+
});
|
|
48
|
+
}));
|
|
49
|
+
router.post('/api/tables/:table/seed', (0, helpers_1.wrap)(async (req, res) => {
|
|
50
|
+
const info = await (0, helpers_1.requireTable)(db, req.params.table);
|
|
51
|
+
if (!info)
|
|
52
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
53
|
+
const { count, plan, truncate } = (0, helpers_1.parseSeedRequest)(req.body);
|
|
54
|
+
const configs = (0, index_1.buildColumnConfigs)(info);
|
|
55
|
+
const t0 = Date.now();
|
|
56
|
+
if (truncate) {
|
|
57
|
+
const clearRes = await db.run(`DELETE FROM ${(0, generator_1.quoteIdentifier)(req.params.table)}`);
|
|
58
|
+
if (!clearRes.success) {
|
|
59
|
+
return (0, helpers_1.fail)(res, clearRes.error ?? 'Failed to clear table before seeding.', 400);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const gen = await (0, index_1.generateRows)(db, info, configs, count, plan, truncate);
|
|
63
|
+
const result = await db.insertRows(req.params.table, gen.rows);
|
|
64
|
+
if (!result.success)
|
|
65
|
+
return (0, helpers_1.fail)(res, result.error ?? 'Seed failed.', 400);
|
|
66
|
+
const elapsedMs = Date.now() - t0;
|
|
67
|
+
(0, helpers_1.ok)(res, {
|
|
68
|
+
message: `${truncate ? 'Cleared table and inserted' : 'Inserted'} ${result.data?.inserted ?? 0} row(s) in ${elapsedMs}ms.`,
|
|
69
|
+
inserted: result.data?.inserted ?? 0,
|
|
70
|
+
skipped: result.data?.skipped ?? 0,
|
|
71
|
+
warnings: gen.warnings,
|
|
72
|
+
elapsedMs,
|
|
73
|
+
}, 201);
|
|
74
|
+
}));
|
|
75
|
+
router.get('/api/tables/:table/seed/chain', (0, helpers_1.wrap)(async (req, res) => {
|
|
76
|
+
const info = await (0, helpers_1.requireTable)(db, req.params.table);
|
|
77
|
+
if (!info)
|
|
78
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
79
|
+
const rawScope = String(req.query.scope ?? 'chain');
|
|
80
|
+
const scope = rawScope === 'ancestors' || rawScope === 'descendants' || rawScope === 'all' || rawScope === 'single'
|
|
81
|
+
? rawScope
|
|
82
|
+
: 'chain';
|
|
83
|
+
const config = await (0, index_1.getErChainConfig)(db, req.params.table, scope);
|
|
84
|
+
(0, helpers_1.ok)(res, config);
|
|
85
|
+
}));
|
|
86
|
+
router.post('/api/tables/:table/seed/chain/preview', (0, helpers_1.wrap)(async (req, res) => {
|
|
87
|
+
const info = await (0, helpers_1.requireTable)(db, req.params.table);
|
|
88
|
+
if (!info)
|
|
89
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
90
|
+
const { scope, counts, plans } = (0, helpers_1.parseChainSeedRequest)(req.body);
|
|
91
|
+
const config = await (0, index_1.getErChainConfig)(db, req.params.table, scope);
|
|
92
|
+
const previewCounts = {};
|
|
93
|
+
for (const t of config.tables) {
|
|
94
|
+
previewCounts[t.name] = Math.min(50, counts[t.name] ?? t.suggestedCount ?? 10);
|
|
95
|
+
}
|
|
96
|
+
const gen = await (0, index_1.generateChainRows)(db, config, plans, previewCounts);
|
|
97
|
+
(0, helpers_1.ok)(res, gen);
|
|
98
|
+
}));
|
|
99
|
+
router.post('/api/tables/:table/seed/chain/generate', (0, helpers_1.wrap)(async (req, res) => {
|
|
100
|
+
const info = await (0, helpers_1.requireTable)(db, req.params.table);
|
|
101
|
+
if (!info)
|
|
102
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
103
|
+
const { scope, counts, plans } = (0, helpers_1.parseChainSeedRequest)(req.body);
|
|
104
|
+
const config = await (0, index_1.getErChainConfig)(db, req.params.table, scope);
|
|
105
|
+
const gen = await (0, index_1.generateChainRows)(db, config, plans, counts);
|
|
106
|
+
(0, helpers_1.ok)(res, gen);
|
|
107
|
+
}));
|
|
108
|
+
router.post('/api/tables/:table/seed/chain', (0, helpers_1.wrap)(async (req, res) => {
|
|
109
|
+
const info = await (0, helpers_1.requireTable)(db, req.params.table);
|
|
110
|
+
if (!info)
|
|
111
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
112
|
+
const { scope, counts, plans, truncate } = (0, helpers_1.parseChainSeedRequest)(req.body);
|
|
113
|
+
const config = await (0, index_1.getErChainConfig)(db, req.params.table, scope);
|
|
114
|
+
const gen = await (0, index_1.generateChainRows)(db, config, plans, counts, truncate);
|
|
115
|
+
const exec = await (0, index_1.executeChainInsert)(db, gen, truncate);
|
|
116
|
+
(0, helpers_1.ok)(res, {
|
|
117
|
+
message: `${truncate ? 'Cleared tables in reverse order and seeded' : 'Seeded'} ${exec.totalInserted} row(s) across ${Object.keys(exec.inserted).length} table(s) in ${exec.elapsedMs}ms.`,
|
|
118
|
+
inserted: exec.inserted,
|
|
119
|
+
totalInserted: exec.totalInserted,
|
|
120
|
+
elapsedMs: exec.elapsedMs,
|
|
121
|
+
warnings: exec.warnings,
|
|
122
|
+
}, 201);
|
|
123
|
+
}));
|
|
124
|
+
router.get('/api/seed/chain', (0, helpers_1.wrap)(async (req, res) => {
|
|
125
|
+
const rawRoot = String(req.query.table ?? '');
|
|
126
|
+
const rawScope = String(req.query.scope ?? (rawRoot ? 'chain' : 'all'));
|
|
127
|
+
const scope = rawScope === 'ancestors' || rawScope === 'descendants' || rawScope === 'all' || rawScope === 'single'
|
|
128
|
+
? rawScope
|
|
129
|
+
: 'all';
|
|
130
|
+
const rootTable = rawRoot || (await db.listTables()).data?.[0]?.name || '';
|
|
131
|
+
if (!rootTable)
|
|
132
|
+
return (0, helpers_1.fail)(res, 'No tables found in database.', 404);
|
|
133
|
+
const config = await (0, index_1.getErChainConfig)(db, rootTable, scope);
|
|
134
|
+
(0, helpers_1.ok)(res, config);
|
|
135
|
+
}));
|
|
136
|
+
router.post('/api/seed/chain', (0, helpers_1.wrap)(async (req, res) => {
|
|
137
|
+
const { scope, counts, plans, truncate } = (0, helpers_1.parseChainSeedRequest)(req.body);
|
|
138
|
+
const rawRoot = String(req.body?.rootTable ?? '');
|
|
139
|
+
const rootTable = rawRoot || (await db.listTables()).data?.[0]?.name || '';
|
|
140
|
+
if (!rootTable)
|
|
141
|
+
return (0, helpers_1.fail)(res, 'No tables found in database.', 404);
|
|
142
|
+
const config = await (0, index_1.getErChainConfig)(db, rootTable, scope);
|
|
143
|
+
const gen = await (0, index_1.generateChainRows)(db, config, plans, counts, truncate);
|
|
144
|
+
const exec = await (0, index_1.executeChainInsert)(db, gen, truncate);
|
|
145
|
+
(0, helpers_1.ok)(res, {
|
|
146
|
+
message: `${truncate ? 'Cleared tables in reverse order and seeded' : 'Seeded'} ${exec.totalInserted} row(s) across ${Object.keys(exec.inserted).length} table(s) in ${exec.elapsedMs}ms.`,
|
|
147
|
+
inserted: exec.inserted,
|
|
148
|
+
totalInserted: exec.totalInserted,
|
|
149
|
+
elapsedMs: exec.elapsedMs,
|
|
150
|
+
warnings: exec.warnings,
|
|
151
|
+
}, 201);
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
@@ -1 +1,231 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerTableRoutes = registerTableRoutes;
|
|
4
|
+
const generator_1 = require("../../sql/generator");
|
|
5
|
+
const helpers_1 = require("./helpers");
|
|
6
|
+
function registerTableRoutes(router, ctx) {
|
|
7
|
+
const { db } = ctx;
|
|
8
|
+
router.get('/api/tables', (0, helpers_1.wrap)(async (_req, res) => {
|
|
9
|
+
const r = await db.listTables();
|
|
10
|
+
if (!r.success)
|
|
11
|
+
return (0, helpers_1.fail)(res, r.error ?? 'Failed to list tables.', 500);
|
|
12
|
+
(0, helpers_1.ok)(res, r.data);
|
|
13
|
+
}));
|
|
14
|
+
router.get('/api/tables/:table/info', (0, helpers_1.wrap)(async (req, res) => {
|
|
15
|
+
const info = await (0, helpers_1.requireTable)(db, req.params.table);
|
|
16
|
+
if (!info)
|
|
17
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
18
|
+
(0, helpers_1.ok)(res, info);
|
|
19
|
+
}));
|
|
20
|
+
router.get('/api/tables/:table/fk-options', (0, helpers_1.wrap)(async (req, res) => {
|
|
21
|
+
const info = await (0, helpers_1.requireTable)(db, req.params.table);
|
|
22
|
+
if (!info)
|
|
23
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
24
|
+
const options = {};
|
|
25
|
+
for (const fk of info.foreignKeys) {
|
|
26
|
+
try {
|
|
27
|
+
const refInfo = await (0, helpers_1.requireTable)(db, fk.table);
|
|
28
|
+
if (!refInfo)
|
|
29
|
+
continue;
|
|
30
|
+
const refCol = fk.to || refInfo.primaryKey[0] || refInfo.columns[0]?.name;
|
|
31
|
+
const pk = refInfo.primaryKey[0] ?? refInfo.columns[0]?.name;
|
|
32
|
+
const labelCol = refInfo.columns.find((c) => /name|title|label|username|email/i.test(c.name))?.name ?? pk;
|
|
33
|
+
const rows = await db.getRows(fk.table, { limit: 100, orderBy: labelCol });
|
|
34
|
+
if (rows.success && rows.data) {
|
|
35
|
+
options[fk.from] = rows.data.map((r) => ({
|
|
36
|
+
value: r[refCol],
|
|
37
|
+
label: r[labelCol] != null ? `${r[labelCol]} (${r[refCol]})` : String(r[refCol]),
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
(0, helpers_1.ok)(res, options);
|
|
45
|
+
}));
|
|
46
|
+
router.get('/api/tables/:table/ddl', (0, helpers_1.wrap)(async (req, res) => {
|
|
47
|
+
const r = await db.getCreateStatement(req.params.table);
|
|
48
|
+
if (!r.success)
|
|
49
|
+
return (0, helpers_1.fail)(res, r.error ?? 'Failed to fetch DDL.', 500);
|
|
50
|
+
if (!r.data)
|
|
51
|
+
return (0, helpers_1.fail)(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
52
|
+
(0, helpers_1.ok)(res, { sql: r.data });
|
|
53
|
+
}));
|
|
54
|
+
router.post('/api/tables/:table/rename', (0, helpers_1.wrap)(async (req, res) => {
|
|
55
|
+
const newName = String(req.body?.name ?? '').trim();
|
|
56
|
+
if (!newName)
|
|
57
|
+
return (0, helpers_1.fail)(res, 'New table name is required.');
|
|
58
|
+
const r = await db.renameTable(req.params.table, newName);
|
|
59
|
+
if (!r.success)
|
|
60
|
+
return (0, helpers_1.fail)(res, r.error ?? 'Failed to rename table.');
|
|
61
|
+
(0, helpers_1.ok)(res, { message: `Table renamed to "${newName}".`, name: newName });
|
|
62
|
+
}));
|
|
63
|
+
router.delete('/api/tables/:table', (0, helpers_1.wrap)(async (req, res) => {
|
|
64
|
+
const r = await db.dropTable(req.params.table);
|
|
65
|
+
if (!r.success)
|
|
66
|
+
return (0, helpers_1.fail)(res, r.error ?? 'Failed to drop table.');
|
|
67
|
+
(0, helpers_1.ok)(res, { message: `Table "${req.params.table}" dropped.` });
|
|
68
|
+
}));
|
|
69
|
+
router.post('/api/tables/bulk-drop', (0, helpers_1.wrap)(async (req, res) => {
|
|
70
|
+
const tables = Array.isArray(req.body?.tables)
|
|
71
|
+
? req.body.tables.filter((t) => typeof t === 'string' && t.trim().length > 0)
|
|
72
|
+
: [];
|
|
73
|
+
const force = Boolean(req.body?.force);
|
|
74
|
+
if (tables.length === 0)
|
|
75
|
+
return (0, helpers_1.fail)(res, 'No tables specified.');
|
|
76
|
+
const dropped = [];
|
|
77
|
+
const failed = [];
|
|
78
|
+
if (force && db.dialect === 'sqlite') {
|
|
79
|
+
await db.run('PRAGMA foreign_keys = OFF');
|
|
80
|
+
}
|
|
81
|
+
for (const table of tables) {
|
|
82
|
+
try {
|
|
83
|
+
if (force && db.dialect === 'postgres') {
|
|
84
|
+
const r = await db.run(`DROP TABLE IF EXISTS ${(0, generator_1.quoteIdentifier)(table)} CASCADE`);
|
|
85
|
+
if (!r.success)
|
|
86
|
+
throw new Error(r.error ?? 'Failed to drop table.');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const r = await db.dropTable(table);
|
|
90
|
+
if (!r.success)
|
|
91
|
+
throw new Error(r.error ?? 'Failed to drop table.');
|
|
92
|
+
}
|
|
93
|
+
dropped.push(table);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
failed.push({ table, error: err.message });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (force && db.dialect === 'sqlite') {
|
|
100
|
+
await db.run('PRAGMA foreign_keys = ON');
|
|
101
|
+
}
|
|
102
|
+
(0, helpers_1.ok)(res, {
|
|
103
|
+
dropped,
|
|
104
|
+
failed,
|
|
105
|
+
message: `Dropped ${dropped.length} table(s)${failed.length ? `, ${failed.length} failed` : ''}.`,
|
|
106
|
+
});
|
|
107
|
+
}));
|
|
108
|
+
router.post('/api/tables/bulk-truncate', (0, helpers_1.wrap)(async (req, res) => {
|
|
109
|
+
const tables = Array.isArray(req.body?.tables)
|
|
110
|
+
? req.body.tables.filter((t) => typeof t === 'string' && t.trim().length > 0)
|
|
111
|
+
: [];
|
|
112
|
+
const force = Boolean(req.body?.force);
|
|
113
|
+
if (tables.length === 0)
|
|
114
|
+
return (0, helpers_1.fail)(res, 'No tables specified.');
|
|
115
|
+
const cleared = [];
|
|
116
|
+
const failed = [];
|
|
117
|
+
if (force && db.dialect === 'sqlite') {
|
|
118
|
+
await db.run('PRAGMA foreign_keys = OFF');
|
|
119
|
+
}
|
|
120
|
+
for (const table of tables) {
|
|
121
|
+
try {
|
|
122
|
+
let r;
|
|
123
|
+
if (force && db.dialect === 'postgres') {
|
|
124
|
+
r = await db.run(`TRUNCATE ${(0, generator_1.quoteIdentifier)(table)} RESTART IDENTITY CASCADE`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
r = await db.run(`DELETE FROM ${(0, generator_1.quoteIdentifier)(table)}`);
|
|
128
|
+
}
|
|
129
|
+
if (!r.success)
|
|
130
|
+
throw new Error(r.error ?? 'Failed to clear table.');
|
|
131
|
+
cleared.push(table);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
failed.push({ table, error: err.message });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (force && db.dialect === 'sqlite') {
|
|
138
|
+
await db.run('PRAGMA foreign_keys = ON');
|
|
139
|
+
}
|
|
140
|
+
(0, helpers_1.ok)(res, {
|
|
141
|
+
cleared,
|
|
142
|
+
failed,
|
|
143
|
+
message: `Cleared ${cleared.length} table(s)${failed.length ? `, ${failed.length} failed` : ''}.`,
|
|
144
|
+
});
|
|
145
|
+
}));
|
|
146
|
+
router.post('/api/tables/generate', (0, helpers_1.wrap)(async (req, res) => {
|
|
147
|
+
const name = String(req.body?.name ?? '').trim();
|
|
148
|
+
const columns = (req.body?.columns ?? []);
|
|
149
|
+
if (!name)
|
|
150
|
+
return (0, helpers_1.fail)(res, 'Table name is required.');
|
|
151
|
+
if (!Array.isArray(columns) || columns.length === 0)
|
|
152
|
+
return (0, helpers_1.fail)(res, 'At least one column is required.');
|
|
153
|
+
try {
|
|
154
|
+
const sql = (0, generator_1.generateCreateTable)(name, columns);
|
|
155
|
+
(0, helpers_1.ok)(res, { sql });
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
(0, helpers_1.fail)(res, err.message);
|
|
159
|
+
}
|
|
160
|
+
}));
|
|
161
|
+
const createTableHandler = (0, helpers_1.wrap)(async (req, res) => {
|
|
162
|
+
const name = String(req.body?.name ?? '').trim();
|
|
163
|
+
const columns = (req.body?.columns ?? []);
|
|
164
|
+
if (!name)
|
|
165
|
+
return (0, helpers_1.fail)(res, 'Table name is required.');
|
|
166
|
+
if (!Array.isArray(columns) || columns.length === 0)
|
|
167
|
+
return (0, helpers_1.fail)(res, 'At least one column is required.');
|
|
168
|
+
let sql;
|
|
169
|
+
try {
|
|
170
|
+
sql = (0, generator_1.generateCreateTable)(name, columns);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
return (0, helpers_1.fail)(res, err.message);
|
|
174
|
+
}
|
|
175
|
+
const r = await db.execResult(sql);
|
|
176
|
+
if (!r.success)
|
|
177
|
+
return (0, helpers_1.fail)(res, r.error ?? 'Failed to create table.');
|
|
178
|
+
(0, helpers_1.ok)(res, { message: `Table "${name}" created.`, name }, 201);
|
|
179
|
+
});
|
|
180
|
+
router.post('/api/tables', createTableHandler);
|
|
181
|
+
router.post('/api/tables/create', createTableHandler);
|
|
182
|
+
router.post('/api/tables/:table/columns', (0, helpers_1.wrap)(async (req, res) => {
|
|
183
|
+
const col = req.body;
|
|
184
|
+
if (!col || !col.name)
|
|
185
|
+
return (0, helpers_1.fail)(res, 'Column name is required.');
|
|
186
|
+
const r = await db.addColumn(req.params.table, col);
|
|
187
|
+
if (!r.success)
|
|
188
|
+
return (0, helpers_1.fail)(res, r.error ?? 'Failed to add column.');
|
|
189
|
+
(0, helpers_1.ok)(res, { message: `Column "${col.name}" added.` }, 201);
|
|
190
|
+
}));
|
|
191
|
+
router.put('/api/tables/:table/columns/:column', (0, helpers_1.wrap)(async (req, res) => {
|
|
192
|
+
const col = req.body;
|
|
193
|
+
if (!col)
|
|
194
|
+
return (0, helpers_1.fail)(res, 'Column definition is required.');
|
|
195
|
+
const r = await db.modifyColumn(req.params.table, req.params.column, col);
|
|
196
|
+
if (!r.success)
|
|
197
|
+
return (0, helpers_1.fail)(res, r.error ?? 'Failed to modify column.');
|
|
198
|
+
(0, helpers_1.ok)(res, { message: `Column "${req.params.column}" updated.` });
|
|
199
|
+
}));
|
|
200
|
+
router.post('/api/tables/:table/columns/:column/rename', (0, helpers_1.wrap)(async (req, res) => {
|
|
201
|
+
const newName = String(req.body?.name ?? '').trim();
|
|
202
|
+
if (!newName)
|
|
203
|
+
return (0, helpers_1.fail)(res, 'New column name is required.');
|
|
204
|
+
const r = await db.renameColumn(req.params.table, req.params.column, newName);
|
|
205
|
+
if (!r.success)
|
|
206
|
+
return (0, helpers_1.fail)(res, r.error ?? 'Failed to rename column.');
|
|
207
|
+
(0, helpers_1.ok)(res, { message: `Column renamed to "${newName}".`, name: newName });
|
|
208
|
+
}));
|
|
209
|
+
router.delete('/api/tables/:table/columns/:column', (0, helpers_1.wrap)(async (req, res) => {
|
|
210
|
+
const r = await db.dropColumn(req.params.table, req.params.column);
|
|
211
|
+
if (!r.success)
|
|
212
|
+
return (0, helpers_1.fail)(res, r.error ?? 'Failed to drop column.');
|
|
213
|
+
(0, helpers_1.ok)(res, { message: `Column "${req.params.column}" dropped.` });
|
|
214
|
+
}));
|
|
215
|
+
router.post('/api/tables/:table/indexes', (0, helpers_1.wrap)(async (req, res) => {
|
|
216
|
+
const def = req.body;
|
|
217
|
+
if (!def || !Array.isArray(def.columns) || def.columns.length === 0) {
|
|
218
|
+
return (0, helpers_1.fail)(res, 'At least one column is required for an index.');
|
|
219
|
+
}
|
|
220
|
+
const r = await db.createIndex(req.params.table, def);
|
|
221
|
+
if (!r.success)
|
|
222
|
+
return (0, helpers_1.fail)(res, r.error ?? 'Failed to create index.');
|
|
223
|
+
(0, helpers_1.ok)(res, { message: 'Index created.' }, 201);
|
|
224
|
+
}));
|
|
225
|
+
router.delete('/api/tables/:table/indexes/:index', (0, helpers_1.wrap)(async (req, res) => {
|
|
226
|
+
const r = await db.dropIndex(req.params.index);
|
|
227
|
+
if (!r.success)
|
|
228
|
+
return (0, helpers_1.fail)(res, r.error ?? 'Failed to drop index.');
|
|
229
|
+
(0, helpers_1.ok)(res, { message: `Index "${req.params.index}" dropped.` });
|
|
230
|
+
}));
|
|
231
|
+
}
|
package/dist/routes/databases.js
CHANGED
|
@@ -1 +1,228 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerDatabasesRoutes = registerDatabasesRoutes;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const common_1 = require("../utils/common");
|
|
10
|
+
function formatDate(iso) {
|
|
11
|
+
try {
|
|
12
|
+
return new Date(iso).toLocaleString();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return iso;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function getDatabaseRows(manager) {
|
|
19
|
+
const entries = manager.list();
|
|
20
|
+
return Promise.all(entries.map(async (e) => ({
|
|
21
|
+
...e,
|
|
22
|
+
isPostgres: e.dialect === 'postgres',
|
|
23
|
+
isSqlite: e.dialect === 'sqlite',
|
|
24
|
+
sizeLabel: e.dialect === 'postgres' ? 'Remote' : (0, common_1.formatBytes)(e.size),
|
|
25
|
+
modifiedLabel: e.dialect === 'postgres' ? 'Connected' : (e.modified ? formatDate(e.modified) : '—'),
|
|
26
|
+
tables: await manager.countTables(e.id),
|
|
27
|
+
})));
|
|
28
|
+
}
|
|
29
|
+
function registerDatabasesRoutes(router, ctx) {
|
|
30
|
+
const { manager } = ctx;
|
|
31
|
+
const allowBrowse = ctx.allowBrowse ?? true;
|
|
32
|
+
const browseRoot = ctx.browseRoot ? node_path_1.default.resolve(ctx.browseRoot) : undefined;
|
|
33
|
+
const wrap = (fn) => async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
await fn(req, res);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
res.status(500).json({ success: false, error: (0, common_1.errorMessage)(err) });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
router.get('/', async (_req, res, next) => {
|
|
42
|
+
try {
|
|
43
|
+
const databases = await getDatabaseRows(manager);
|
|
44
|
+
res.render('pages/databases', {
|
|
45
|
+
title: 'Databases',
|
|
46
|
+
databases,
|
|
47
|
+
dbDir: manager.directory,
|
|
48
|
+
readonly: !!ctx.readonly,
|
|
49
|
+
allowBrowse,
|
|
50
|
+
browseRoot,
|
|
51
|
+
config: { basePath: ctx.basePath },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
next(err);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
router.get('/api/databases', wrap(async (_req, res) => {
|
|
59
|
+
const data = await getDatabaseRows(manager);
|
|
60
|
+
res.json({ success: true, data });
|
|
61
|
+
}));
|
|
62
|
+
const BLOCKED_NAMES = new Set([
|
|
63
|
+
'node_modules',
|
|
64
|
+
'.git',
|
|
65
|
+
'.svn',
|
|
66
|
+
'.hg',
|
|
67
|
+
'System Volume Information',
|
|
68
|
+
'$RECYCLE.BIN',
|
|
69
|
+
'.env',
|
|
70
|
+
'.aws',
|
|
71
|
+
'.ssh',
|
|
72
|
+
]);
|
|
73
|
+
function listDirectory(abs) {
|
|
74
|
+
const entries = [];
|
|
75
|
+
for (const name of (0, node_fs_1.readdirSync)(abs)) {
|
|
76
|
+
if (name.startsWith('.'))
|
|
77
|
+
continue;
|
|
78
|
+
if (BLOCKED_NAMES.has(name) || name.toLowerCase().startsWith('.env'))
|
|
79
|
+
continue;
|
|
80
|
+
const full = node_path_1.default.join(abs, name);
|
|
81
|
+
let isDir = false;
|
|
82
|
+
let size = 0;
|
|
83
|
+
try {
|
|
84
|
+
const st = (0, node_fs_1.statSync)(full);
|
|
85
|
+
isDir = st.isDirectory();
|
|
86
|
+
size = isDir ? 0 : st.size;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!isDir && !manager.isDbFile(name))
|
|
92
|
+
continue;
|
|
93
|
+
if (browseRoot && !(0, common_1.isPathWithinRoot)(browseRoot, full))
|
|
94
|
+
continue;
|
|
95
|
+
entries.push({ name, path: full, isDir, isDb: !isDir, size });
|
|
96
|
+
}
|
|
97
|
+
entries.sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) : a.isDir ? -1 : 1);
|
|
98
|
+
return entries;
|
|
99
|
+
}
|
|
100
|
+
router.get('/api/fs/list', wrap(async (req, res) => {
|
|
101
|
+
if (!allowBrowse) {
|
|
102
|
+
return res.status(403).json({ success: false, error: 'File browsing is disabled — this server was started with specific database files.' });
|
|
103
|
+
}
|
|
104
|
+
const raw = String(req.query.path ?? '').trim();
|
|
105
|
+
if (raw.includes('\0')) {
|
|
106
|
+
return res.status(400).json({ success: false, error: 'Invalid path.' });
|
|
107
|
+
}
|
|
108
|
+
const base = raw ? node_path_1.default.resolve(raw) : manager.directory;
|
|
109
|
+
if (browseRoot && !(0, common_1.isPathWithinRoot)(browseRoot, base)) {
|
|
110
|
+
return res.status(403).json({ success: false, error: `Path is outside the allowed folder: ${browseRoot}` });
|
|
111
|
+
}
|
|
112
|
+
if (!(0, node_fs_1.existsSync)(base))
|
|
113
|
+
return res.status(404).json({ success: false, error: `Path does not exist: ${base}` });
|
|
114
|
+
if (!(0, node_fs_1.statSync)(base).isDirectory())
|
|
115
|
+
return res.status(400).json({ success: false, error: `Not a directory: ${base}` });
|
|
116
|
+
let entries;
|
|
117
|
+
try {
|
|
118
|
+
entries = listDirectory(base);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
return res.status(400).json({ success: false, error: `Could not list "${base}": ${(0, common_1.errorMessage)(err)}` });
|
|
122
|
+
}
|
|
123
|
+
const parent = node_path_1.default.dirname(base) === base ? null : node_path_1.default.dirname(base);
|
|
124
|
+
const effectiveParent = parent && (!browseRoot || (0, common_1.isPathWithinRoot)(browseRoot, parent)) ? parent : null;
|
|
125
|
+
res.json({
|
|
126
|
+
success: true,
|
|
127
|
+
data: {
|
|
128
|
+
path: base,
|
|
129
|
+
name: node_path_1.default.basename(base) || base,
|
|
130
|
+
parent: effectiveParent,
|
|
131
|
+
entries,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}));
|
|
135
|
+
router.post('/api/databases/open', wrap(async (req, res) => {
|
|
136
|
+
if (!allowBrowse) {
|
|
137
|
+
return res.status(403).json({ success: false, error: 'File browsing is disabled — this server was started with specific database files.' });
|
|
138
|
+
}
|
|
139
|
+
const p = String(req.body?.path ?? '').trim();
|
|
140
|
+
if (!p || p.includes('\0'))
|
|
141
|
+
return res.status(400).json({ success: false, error: 'Valid database path is required.' });
|
|
142
|
+
const abs = node_path_1.default.resolve(p);
|
|
143
|
+
if (browseRoot && !(0, common_1.isPathWithinRoot)(browseRoot, abs)) {
|
|
144
|
+
return res.status(403).json({ success: false, error: `Cannot open a database outside the allowed folder: ${browseRoot}` });
|
|
145
|
+
}
|
|
146
|
+
const isRo = req.body?.readonly !== undefined ? Boolean(req.body.readonly) : undefined;
|
|
147
|
+
let id;
|
|
148
|
+
try {
|
|
149
|
+
id = manager.openFile(p, isRo);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
return res.status(400).json({ success: false, error: (0, common_1.errorMessage)(err) });
|
|
153
|
+
}
|
|
154
|
+
res.json({ success: true, data: { id, message: `Opened "${id}".` } });
|
|
155
|
+
}));
|
|
156
|
+
router.post('/api/databases/connect-postgres', wrap(async (req, res) => {
|
|
157
|
+
if (ctx.readonly)
|
|
158
|
+
return res.status(403).json({ success: false, error: 'Read-only mode — adding connections is disabled.' });
|
|
159
|
+
const name = String(req.body?.name ?? '').trim();
|
|
160
|
+
const connectionString = String(req.body?.connectionString ?? req.body?.connection ?? '').trim();
|
|
161
|
+
const isRo = Boolean(req.body?.readonly);
|
|
162
|
+
if (!connectionString) {
|
|
163
|
+
return res.status(400).json({ success: false, error: 'PostgreSQL connection string is required (e.g. postgresql://user:password@localhost:5432/dbname).' });
|
|
164
|
+
}
|
|
165
|
+
if (!(0, common_1.isPostgresConnectionString)(connectionString)) {
|
|
166
|
+
return res.status(400).json({ success: false, error: 'Invalid connection protocol. Connection string must start with postgres:// or postgresql://' });
|
|
167
|
+
}
|
|
168
|
+
let id;
|
|
169
|
+
try {
|
|
170
|
+
id = manager.addConnection(name, connectionString, isRo);
|
|
171
|
+
const db = manager.open(id);
|
|
172
|
+
await db.listTables();
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
if (id) {
|
|
176
|
+
manager.remove(id);
|
|
177
|
+
}
|
|
178
|
+
return res.status(400).json({ success: false, error: `Failed to connect to PostgreSQL: ${(0, common_1.errorMessage)(err)}` });
|
|
179
|
+
}
|
|
180
|
+
res.status(201).json({ success: true, data: { id, message: `Connected to PostgreSQL database "${id}".` } });
|
|
181
|
+
}));
|
|
182
|
+
router.post('/api/databases/:id/mode', wrap(async (req, res) => {
|
|
183
|
+
if (ctx.readonly && req.body?.readonly === false) {
|
|
184
|
+
return res.status(403).json({ success: false, error: 'Server is in global read-only mode.' });
|
|
185
|
+
}
|
|
186
|
+
const id = String(req.params.id);
|
|
187
|
+
if (!manager.has(id))
|
|
188
|
+
return res.status(404).json({ success: false, error: `Database "${id}" does not exist.` });
|
|
189
|
+
const targetRo = Boolean(req.body?.readonly);
|
|
190
|
+
manager.setReadonly(id, targetRo);
|
|
191
|
+
if (ctx.invalidate)
|
|
192
|
+
ctx.invalidate(id);
|
|
193
|
+
res.json({
|
|
194
|
+
success: true,
|
|
195
|
+
data: {
|
|
196
|
+
id,
|
|
197
|
+
readonly: targetRo,
|
|
198
|
+
message: `Database "${id}" mode set to ${targetRo ? 'Read-only' : 'Writable'}.`,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}));
|
|
202
|
+
router.post('/api/databases', wrap(async (req, res) => {
|
|
203
|
+
if (ctx.readonly)
|
|
204
|
+
return res.status(403).json({ success: false, error: 'Read-only mode — creating databases is disabled.' });
|
|
205
|
+
const name = String(req.body?.name ?? '').trim();
|
|
206
|
+
if (!name)
|
|
207
|
+
return res.status(400).json({ success: false, error: 'Database name is required.' });
|
|
208
|
+
let id;
|
|
209
|
+
try {
|
|
210
|
+
id = manager.create(name);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
return res.status(400).json({ success: false, error: (0, common_1.errorMessage)(err) });
|
|
214
|
+
}
|
|
215
|
+
res.status(201).json({ success: true, data: { id, message: `Database "${id}" created.` } });
|
|
216
|
+
}));
|
|
217
|
+
router.delete('/api/databases/:id', wrap(async (req, res) => {
|
|
218
|
+
if (ctx.readonly)
|
|
219
|
+
return res.status(403).json({ success: false, error: 'Read-only mode — deleting databases is disabled.' });
|
|
220
|
+
const id = String(req.params.id);
|
|
221
|
+
if (!manager.has(id))
|
|
222
|
+
return res.status(404).json({ success: false, error: `Database "${id}" does not exist.` });
|
|
223
|
+
if (ctx.invalidate)
|
|
224
|
+
ctx.invalidate(id);
|
|
225
|
+
manager.remove(id);
|
|
226
|
+
res.json({ success: true, data: { message: `Database "${id}" removed.` } });
|
|
227
|
+
}));
|
|
228
|
+
}
|