admindb 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/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/app.d.ts +36 -0
- package/dist/app.js +186 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +61 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +23 -0
- package/dist/config.js.map +1 -0
- package/dist/csv.d.ts +15 -0
- package/dist/csv.js +80 -0
- package/dist/csv.js.map +1 -0
- package/dist/db/database.d.ts +225 -0
- package/dist/db/database.js +497 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/export.d.ts +12 -0
- package/dist/db/export.js +37 -0
- package/dist/db/export.js.map +1 -0
- package/dist/db/manager.d.ts +62 -0
- package/dist/db/manager.js +206 -0
- package/dist/db/manager.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +10 -0
- package/dist/logger.js +25 -0
- package/dist/logger.js.map +1 -0
- package/dist/public/css/app.css +1 -0
- package/dist/public/css/input.css +179 -0
- package/dist/public/js/api.js +47 -0
- package/dist/public/js/app.js +120 -0
- package/dist/public/js/browse.js +123 -0
- package/dist/public/js/databases.js +45 -0
- package/dist/public/js/designer.js +205 -0
- package/dist/public/js/forms.js +244 -0
- package/dist/public/js/query.js +232 -0
- package/dist/public/js/schema.js +314 -0
- package/dist/public/js/triggers.js +155 -0
- package/dist/public/js/views.js +176 -0
- package/dist/routes/api.d.ts +9 -0
- package/dist/routes/api.js +622 -0
- package/dist/routes/api.js.map +1 -0
- package/dist/routes/databases.d.ts +19 -0
- package/dist/routes/databases.js +99 -0
- package/dist/routes/databases.js.map +1 -0
- package/dist/routes/pages.d.ts +9 -0
- package/dist/routes/pages.js +253 -0
- package/dist/routes/pages.js.map +1 -0
- package/dist/sql/classifier.d.ts +19 -0
- package/dist/sql/classifier.js +37 -0
- package/dist/sql/classifier.js.map +1 -0
- package/dist/sql/generator.d.ts +99 -0
- package/dist/sql/generator.js +259 -0
- package/dist/sql/generator.js.map +1 -0
- package/dist/test/classifier.test.d.ts +1 -0
- package/dist/test/classifier.test.js +44 -0
- package/dist/test/classifier.test.js.map +1 -0
- package/dist/test/csv.test.d.ts +1 -0
- package/dist/test/csv.test.js +64 -0
- package/dist/test/csv.test.js.map +1 -0
- package/dist/test/database.test.d.ts +1 -0
- package/dist/test/database.test.js +224 -0
- package/dist/test/database.test.js.map +1 -0
- package/dist/test/generator.test.d.ts +1 -0
- package/dist/test/generator.test.js +164 -0
- package/dist/test/generator.test.js.map +1 -0
- package/dist/test/manager.test.d.ts +1 -0
- package/dist/test/manager.test.js +146 -0
- package/dist/test/manager.test.js.map +1 -0
- package/dist/util.d.ts +29 -0
- package/dist/util.js +127 -0
- package/dist/util.js.map +1 -0
- package/dist/views/layouts/main.hbs +49 -0
- package/dist/views/pages/databases.hbs +102 -0
- package/dist/views/pages/designer.hbs +81 -0
- package/dist/views/pages/error.hbs +12 -0
- package/dist/views/pages/form.hbs +62 -0
- package/dist/views/pages/home.hbs +123 -0
- package/dist/views/pages/query.hbs +94 -0
- package/dist/views/pages/schema.hbs +290 -0
- package/dist/views/pages/table.hbs +203 -0
- package/dist/views/pages/triggers.hbs +162 -0
- package/dist/views/pages/views.hbs +134 -0
- package/dist/views/partials/navbar.hbs +50 -0
- package/dist/views/partials/sidebar.hbs +128 -0
- package/package.json +62 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerApi = registerApi;
|
|
4
|
+
const generator_1 = require("../sql/generator");
|
|
5
|
+
const classifier_1 = require("../sql/classifier");
|
|
6
|
+
const csv_1 = require("../csv");
|
|
7
|
+
const util_1 = require("../util");
|
|
8
|
+
function ok(res, data, status = 200) {
|
|
9
|
+
return res.status(status).json({ success: true, data });
|
|
10
|
+
}
|
|
11
|
+
function fail(res, error, status = 400) {
|
|
12
|
+
return res.status(status).json({ success: false, error });
|
|
13
|
+
}
|
|
14
|
+
function wrap(fn) {
|
|
15
|
+
return async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
await fn(req, res);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
fail(res, (0, util_1.errorMessage)(err), 500);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function requireTable(db, table) {
|
|
25
|
+
const info = await db.getTableInfo(table);
|
|
26
|
+
if (!info.success || !info.data || info.data.columns.length === 0)
|
|
27
|
+
return null;
|
|
28
|
+
return info.data;
|
|
29
|
+
}
|
|
30
|
+
function buildFields(info, values, opts = {}) {
|
|
31
|
+
const typeMap = new Map(info.columns.map((c) => [c.name, c.type]));
|
|
32
|
+
const pkSet = new Set(info.primaryKey);
|
|
33
|
+
const fields = [];
|
|
34
|
+
for (const [col, raw] of Object.entries(values ?? {})) {
|
|
35
|
+
if (!typeMap.has(col))
|
|
36
|
+
continue;
|
|
37
|
+
if (opts.excludePk && pkSet.has(col))
|
|
38
|
+
continue;
|
|
39
|
+
const val = (0, util_1.coerceFormValue)(raw, typeMap.get(col));
|
|
40
|
+
if (val === null)
|
|
41
|
+
continue; // empty input = "not set"
|
|
42
|
+
fields.push({ column: col, value: val });
|
|
43
|
+
}
|
|
44
|
+
return fields;
|
|
45
|
+
}
|
|
46
|
+
function pkWhere(info, encodedId) {
|
|
47
|
+
if (!info.primaryKey.length)
|
|
48
|
+
return null;
|
|
49
|
+
const vals = (0, util_1.decodePk)(encodedId);
|
|
50
|
+
if (vals.length !== info.primaryKey.length)
|
|
51
|
+
return null;
|
|
52
|
+
const typeMap = new Map(info.columns.map((c) => [c.name, c.type]));
|
|
53
|
+
return info.primaryKey.map((col, i) => ({ column: col, value: (0, util_1.coerceFormValue)(vals[i], typeMap.get(col)) }));
|
|
54
|
+
}
|
|
55
|
+
function registerApi(router, ctx) {
|
|
56
|
+
const { db, logger } = ctx;
|
|
57
|
+
// In read-only mode every mutating request is rejected (GET, the generate-only
|
|
58
|
+
// preview endpoints and the query runner stay available; the query runner
|
|
59
|
+
// rejects write statements itself).
|
|
60
|
+
router.use((req, res, next) => {
|
|
61
|
+
if (!db.isReadOnly || req.method === 'GET')
|
|
62
|
+
return next();
|
|
63
|
+
const p = req.path;
|
|
64
|
+
if (p.endsWith('/generate') || p.endsWith('/api/query/export') || p.endsWith('/api/query'))
|
|
65
|
+
return next();
|
|
66
|
+
return res.status(403).json({ success: false, error: 'Database is open in read-only mode — write operations are disabled.' });
|
|
67
|
+
});
|
|
68
|
+
// ---- Tables ------------------------------------------------------------
|
|
69
|
+
router.get('/api/tables', wrap(async (_req, res) => {
|
|
70
|
+
const r = await db.listTables();
|
|
71
|
+
if (!r.success)
|
|
72
|
+
return fail(res, r.error ?? 'Failed to list tables.', 500);
|
|
73
|
+
ok(res, r.data);
|
|
74
|
+
}));
|
|
75
|
+
router.get('/api/tables/:table/info', wrap(async (req, res) => {
|
|
76
|
+
const info = await requireTable(db, req.params.table);
|
|
77
|
+
if (!info)
|
|
78
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
79
|
+
ok(res, info);
|
|
80
|
+
}));
|
|
81
|
+
router.get('/api/tables/:table/fk-options', wrap(async (req, res) => {
|
|
82
|
+
const info = await requireTable(db, req.params.table);
|
|
83
|
+
if (!info)
|
|
84
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
85
|
+
const options = {};
|
|
86
|
+
for (const fk of info.foreignKeys) {
|
|
87
|
+
try {
|
|
88
|
+
const refInfo = await requireTable(db, fk.table);
|
|
89
|
+
if (!refInfo)
|
|
90
|
+
continue;
|
|
91
|
+
const pk = refInfo.primaryKey[0] ?? refInfo.columns[0]?.name;
|
|
92
|
+
const labelCols = refInfo.columns.filter((c) => c.name !== pk).slice(0, 2).map((c) => c.name);
|
|
93
|
+
const rowsR = await db.getAllRows(fk.table);
|
|
94
|
+
const rows = (rowsR.data ?? []).slice(0, 500);
|
|
95
|
+
options[fk.from] = rows.map((row) => ({
|
|
96
|
+
value: row[pk],
|
|
97
|
+
label: labelCols.length ? labelCols.map((c) => String(row[c] ?? '')).join(' · ') : String(row[pk] ?? ''),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
logger.warn(`fk-options for ${fk.from}: ${(0, util_1.errorMessage)(err)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
ok(res, options);
|
|
105
|
+
}));
|
|
106
|
+
router.get('/api/tables/:table/rows', wrap(async (req, res) => {
|
|
107
|
+
const info = await requireTable(db, req.params.table);
|
|
108
|
+
if (!info)
|
|
109
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
110
|
+
const page = Math.max(1, Number.parseInt(String(req.query.page ?? '1'), 10) || 1);
|
|
111
|
+
const limit = Math.max(1, Math.min(1000, Number.parseInt(String(req.query.limit ?? '50'), 10) || 50));
|
|
112
|
+
const orderBy = req.query.orderBy ? String(req.query.orderBy) : undefined;
|
|
113
|
+
const orderDir = String(req.query.orderDir ?? 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc';
|
|
114
|
+
const filters = (0, util_1.parseFilters)(req.query.f);
|
|
115
|
+
const rowsR = await db.getRows(req.params.table, { page, limit, orderBy, orderDir, filters });
|
|
116
|
+
if (!rowsR.success)
|
|
117
|
+
return fail(res, rowsR.error ?? 'Failed to load rows.', 500);
|
|
118
|
+
const countR = await db.getRowCount(req.params.table, filters);
|
|
119
|
+
ok(res, {
|
|
120
|
+
rows: (rowsR.data ?? []).map(util_1.normalizeRow),
|
|
121
|
+
count: countR.success ? countR.data : 0,
|
|
122
|
+
page,
|
|
123
|
+
limit,
|
|
124
|
+
filters,
|
|
125
|
+
});
|
|
126
|
+
}));
|
|
127
|
+
// ---- Export / import ---------------------------------------------------
|
|
128
|
+
router.get('/api/tables/:table/export', wrap(async (req, res) => {
|
|
129
|
+
const info = await requireTable(db, req.params.table);
|
|
130
|
+
if (!info)
|
|
131
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
132
|
+
const format = String(req.query.format ?? 'csv').toLowerCase() === 'json' ? 'json' : 'csv';
|
|
133
|
+
const rowsR = await db.getAllRows(req.params.table);
|
|
134
|
+
if (!rowsR.success)
|
|
135
|
+
return fail(res, rowsR.error ?? 'Export failed.', 500);
|
|
136
|
+
const rows = (rowsR.data ?? []).map(util_1.normalizeRow);
|
|
137
|
+
const columns = info.columns.map((c) => c.name);
|
|
138
|
+
const body = format === 'json' ? (0, csv_1.toJson)(rows) : (0, csv_1.toCsv)(rows, columns);
|
|
139
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
140
|
+
res.setHeader('Content-Type', format === 'json' ? 'application/json; charset=utf-8' : 'text/csv; charset=utf-8');
|
|
141
|
+
res.setHeader('Content-Disposition', `attachment; filename="${req.params.table}-${stamp}.${format}"`);
|
|
142
|
+
res.send(body);
|
|
143
|
+
}));
|
|
144
|
+
router.post('/api/tables/:table/rows/import', wrap(async (req, res) => {
|
|
145
|
+
const info = await requireTable(db, req.params.table);
|
|
146
|
+
if (!info)
|
|
147
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
148
|
+
const csvText = String(req.body?.csv ?? '');
|
|
149
|
+
if (!csvText.trim())
|
|
150
|
+
return fail(res, 'CSV content is required.');
|
|
151
|
+
const rows = (0, csv_1.parseCsv)(csvText);
|
|
152
|
+
if (rows.length < 2)
|
|
153
|
+
return fail(res, 'CSV must include a header row and at least one data row.');
|
|
154
|
+
const headers = rows[0].map((h) => String(h).trim());
|
|
155
|
+
if (!headers.length)
|
|
156
|
+
return fail(res, 'CSV header is empty.');
|
|
157
|
+
const colSet = new Set(info.columns.map((c) => c.name));
|
|
158
|
+
const unknown = headers.filter((h) => !colSet.has(h));
|
|
159
|
+
if (unknown.length)
|
|
160
|
+
return fail(res, `Unknown column(s) in CSV header: ${unknown.join(', ')}.`);
|
|
161
|
+
if (new Set(headers).size !== headers.length)
|
|
162
|
+
return fail(res, 'Duplicate column(s) in CSV header.');
|
|
163
|
+
const typeMap = new Map(info.columns.map((c) => [c.name, c.type]));
|
|
164
|
+
const dataRows = rows
|
|
165
|
+
.slice(1)
|
|
166
|
+
.filter((r) => r.length !== 1 || r[0] !== '')
|
|
167
|
+
.map((r) => {
|
|
168
|
+
const fields = [];
|
|
169
|
+
headers.forEach((h, i) => {
|
|
170
|
+
const val = (0, util_1.coerceFormValue)(r[i], typeMap.get(h));
|
|
171
|
+
if (val !== null)
|
|
172
|
+
fields.push({ column: h, value: val });
|
|
173
|
+
});
|
|
174
|
+
return fields;
|
|
175
|
+
});
|
|
176
|
+
if (!dataRows.length)
|
|
177
|
+
return fail(res, 'No data rows to import.');
|
|
178
|
+
const result = await db.insertRows(req.params.table, dataRows);
|
|
179
|
+
if (!result.success)
|
|
180
|
+
return fail(res, result.error ?? 'Import failed.', 400);
|
|
181
|
+
ok(res, {
|
|
182
|
+
message: `Imported ${result.data?.inserted} row(s).`,
|
|
183
|
+
inserted: result.data?.inserted,
|
|
184
|
+
skipped: result.data?.skipped,
|
|
185
|
+
}, 201);
|
|
186
|
+
}));
|
|
187
|
+
router.post('/api/query/export', wrap(async (req, res) => {
|
|
188
|
+
const sql = String(req.body?.sql ?? '').trim();
|
|
189
|
+
if (!sql)
|
|
190
|
+
return fail(res, 'SQL is required.');
|
|
191
|
+
const format = String(req.body?.format ?? 'csv').toLowerCase() === 'json' ? 'json' : 'csv';
|
|
192
|
+
const r = await db.all(sql);
|
|
193
|
+
if (!r.success)
|
|
194
|
+
return fail(res, r.error ?? 'Query failed.', 400);
|
|
195
|
+
const rows = (r.data ?? []).map(util_1.normalizeRow);
|
|
196
|
+
const columns = rows.length ? Object.keys(rows[0]) : [];
|
|
197
|
+
const body = format === 'json' ? (0, csv_1.toJson)(rows) : (0, csv_1.toCsv)(rows, columns);
|
|
198
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
199
|
+
res.setHeader('Content-Type', format === 'json' ? 'application/json; charset=utf-8' : 'text/csv; charset=utf-8');
|
|
200
|
+
res.setHeader('Content-Disposition', `attachment; filename="query-${stamp}.${format}"`);
|
|
201
|
+
res.send(body);
|
|
202
|
+
}));
|
|
203
|
+
router.get('/api/tables/:table/row/:id', wrap(async (req, res) => {
|
|
204
|
+
const info = await requireTable(db, req.params.table);
|
|
205
|
+
if (!info)
|
|
206
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
207
|
+
const where = pkWhere(info, req.params.id);
|
|
208
|
+
if (!where)
|
|
209
|
+
return fail(res, 'Invalid primary key.', 400);
|
|
210
|
+
const r = await db.getRow(req.params.table, where);
|
|
211
|
+
if (!r.success)
|
|
212
|
+
return fail(res, r.error ?? 'Failed to load row.', 500);
|
|
213
|
+
if (!r.data)
|
|
214
|
+
return fail(res, 'Row not found.', 404);
|
|
215
|
+
ok(res, (0, util_1.normalizeRow)(r.data));
|
|
216
|
+
}));
|
|
217
|
+
// ---- Row CRUD (run + generate variants) -------------------------------
|
|
218
|
+
router.post('/api/tables/:table/rows', wrap(async (req, res) => {
|
|
219
|
+
const info = await requireTable(db, req.params.table);
|
|
220
|
+
if (!info)
|
|
221
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
222
|
+
const values = (req.body?.values ?? {});
|
|
223
|
+
const fields = buildFields(info, values);
|
|
224
|
+
if (!fields.length)
|
|
225
|
+
return fail(res, 'No values provided.');
|
|
226
|
+
const sql = (0, generator_1.generateInsert)(req.params.table, fields);
|
|
227
|
+
const r = await db.insertRow(req.params.table, fields);
|
|
228
|
+
if (!r.success)
|
|
229
|
+
return fail(res, r.error ?? 'Insert failed.', 400);
|
|
230
|
+
ok(res, { message: 'Row inserted.', rowId: r.data?.lastInsertRowid, sql }, 201);
|
|
231
|
+
}));
|
|
232
|
+
router.post('/api/tables/:table/rows/generate', wrap(async (req, res) => {
|
|
233
|
+
const info = await requireTable(db, req.params.table);
|
|
234
|
+
if (!info)
|
|
235
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
236
|
+
const values = (req.body?.values ?? {});
|
|
237
|
+
const fields = buildFields(info, values);
|
|
238
|
+
if (!fields.length)
|
|
239
|
+
return fail(res, 'No values provided.');
|
|
240
|
+
ok(res, { sql: (0, generator_1.generateInsert)(req.params.table, fields) });
|
|
241
|
+
}));
|
|
242
|
+
router.put('/api/tables/:table/row/:id', wrap(async (req, res) => {
|
|
243
|
+
const info = await requireTable(db, req.params.table);
|
|
244
|
+
if (!info)
|
|
245
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
246
|
+
const where = pkWhere(info, req.params.id);
|
|
247
|
+
if (!where)
|
|
248
|
+
return fail(res, 'Invalid primary key.', 400);
|
|
249
|
+
const values = (req.body?.values ?? {});
|
|
250
|
+
const fields = buildFields(info, values, { excludePk: true });
|
|
251
|
+
if (!fields.length)
|
|
252
|
+
return fail(res, 'No fields to update.');
|
|
253
|
+
const sql = (0, generator_1.generateUpdate)(req.params.table, fields, where);
|
|
254
|
+
const r = await db.updateRow(req.params.table, fields, where);
|
|
255
|
+
if (!r.success)
|
|
256
|
+
return fail(res, r.error ?? 'Update failed.', 400);
|
|
257
|
+
ok(res, { message: 'Row updated.', sql });
|
|
258
|
+
}));
|
|
259
|
+
router.put('/api/tables/:table/row/:id/generate', wrap(async (req, res) => {
|
|
260
|
+
const info = await requireTable(db, req.params.table);
|
|
261
|
+
if (!info)
|
|
262
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
263
|
+
const where = pkWhere(info, req.params.id);
|
|
264
|
+
if (!where)
|
|
265
|
+
return fail(res, 'Invalid primary key.', 400);
|
|
266
|
+
const values = (req.body?.values ?? {});
|
|
267
|
+
const fields = buildFields(info, values, { excludePk: true });
|
|
268
|
+
if (!fields.length)
|
|
269
|
+
return fail(res, 'No fields to update.');
|
|
270
|
+
ok(res, { sql: (0, generator_1.generateUpdate)(req.params.table, fields, where) });
|
|
271
|
+
}));
|
|
272
|
+
router.delete('/api/tables/:table/row/:id', wrap(async (req, res) => {
|
|
273
|
+
const info = await requireTable(db, req.params.table);
|
|
274
|
+
if (!info)
|
|
275
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
276
|
+
const where = pkWhere(info, req.params.id);
|
|
277
|
+
if (!where)
|
|
278
|
+
return fail(res, 'Invalid primary key.', 400);
|
|
279
|
+
const r = await db.deleteRow(req.params.table, where);
|
|
280
|
+
if (!r.success)
|
|
281
|
+
return fail(res, r.error ?? 'Delete failed.', 400);
|
|
282
|
+
ok(res, { message: 'Row deleted.' });
|
|
283
|
+
}));
|
|
284
|
+
// ---- Create table (run + generate) ------------------------------------
|
|
285
|
+
router.post('/api/tables', wrap(async (req, res) => {
|
|
286
|
+
const { name, columns } = req.body ?? {};
|
|
287
|
+
if (!Array.isArray(columns))
|
|
288
|
+
return fail(res, 'Columns must be an array.');
|
|
289
|
+
let sql;
|
|
290
|
+
try {
|
|
291
|
+
sql = (0, generator_1.generateCreateTable)(name, columns);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
295
|
+
}
|
|
296
|
+
const r = await db.execResult(sql);
|
|
297
|
+
if (!r.success)
|
|
298
|
+
return fail(res, r.error ?? 'Create table failed.', 400);
|
|
299
|
+
ok(res, { message: `Table "${name}" created.`, sql }, 201);
|
|
300
|
+
}));
|
|
301
|
+
router.post('/api/tables/generate', wrap(async (req, res) => {
|
|
302
|
+
const { name, columns } = req.body ?? {};
|
|
303
|
+
if (!Array.isArray(columns))
|
|
304
|
+
return fail(res, 'Columns must be an array.');
|
|
305
|
+
try {
|
|
306
|
+
ok(res, { sql: (0, generator_1.generateCreateTable)(name, columns) });
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
fail(res, (0, util_1.errorMessage)(err));
|
|
310
|
+
}
|
|
311
|
+
}));
|
|
312
|
+
// ---- Schema editing ----------------------------------------------------
|
|
313
|
+
router.get('/api/tables/:table/schema', wrap(async (req, res) => {
|
|
314
|
+
const info = await requireTable(db, req.params.table);
|
|
315
|
+
if (!info)
|
|
316
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
317
|
+
const r = await db.getSchema(req.params.table);
|
|
318
|
+
if (!r.success)
|
|
319
|
+
return fail(res, r.error ?? 'Failed to load schema.', 500);
|
|
320
|
+
ok(res, r.data);
|
|
321
|
+
}));
|
|
322
|
+
router.post('/api/tables/:table/rename', wrap(async (req, res) => {
|
|
323
|
+
const info = await requireTable(db, req.params.table);
|
|
324
|
+
if (!info)
|
|
325
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
326
|
+
if (req.params.table.startsWith('_'))
|
|
327
|
+
return fail(res, 'Internal tables cannot be renamed.');
|
|
328
|
+
const name = String(req.body?.name ?? '').trim();
|
|
329
|
+
let sql;
|
|
330
|
+
try {
|
|
331
|
+
sql = (0, generator_1.generateRenameTable)(req.params.table, name);
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
335
|
+
}
|
|
336
|
+
const r = await db.renameTable(req.params.table, name);
|
|
337
|
+
if (!r.success)
|
|
338
|
+
return fail(res, r.error ?? 'Rename failed.', 400);
|
|
339
|
+
ok(res, { message: `Table renamed to "${name}".`, sql, table: name });
|
|
340
|
+
}));
|
|
341
|
+
router.post('/api/tables/:table/columns', wrap(async (req, res) => {
|
|
342
|
+
const info = await requireTable(db, req.params.table);
|
|
343
|
+
if (!info)
|
|
344
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
345
|
+
const col = (req.body?.column ?? null);
|
|
346
|
+
if (!col || typeof col !== 'object')
|
|
347
|
+
return fail(res, 'Column definition is required.');
|
|
348
|
+
let sql;
|
|
349
|
+
try {
|
|
350
|
+
sql = (0, generator_1.generateAddColumn)(req.params.table, col);
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
354
|
+
}
|
|
355
|
+
const r = await db.addColumn(req.params.table, col);
|
|
356
|
+
if (!r.success)
|
|
357
|
+
return fail(res, r.error ?? 'Failed to add column.', 400);
|
|
358
|
+
ok(res, { message: `Column "${String(col.name)}" added.`, sql }, 201);
|
|
359
|
+
}));
|
|
360
|
+
router.put('/api/tables/:table/columns/:column', wrap(async (req, res) => {
|
|
361
|
+
const info = await requireTable(db, req.params.table);
|
|
362
|
+
if (!info)
|
|
363
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
364
|
+
const newName = String(req.body?.name ?? '').trim();
|
|
365
|
+
let sql;
|
|
366
|
+
try {
|
|
367
|
+
sql = (0, generator_1.generateRenameColumn)(req.params.table, req.params.column, newName);
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
371
|
+
}
|
|
372
|
+
const r = await db.renameColumn(req.params.table, req.params.column, newName);
|
|
373
|
+
if (!r.success)
|
|
374
|
+
return fail(res, r.error ?? 'Failed to rename column.', 400);
|
|
375
|
+
ok(res, { message: `Column renamed to "${newName}".`, sql });
|
|
376
|
+
}));
|
|
377
|
+
router.delete('/api/tables/:table/columns/:column', wrap(async (req, res) => {
|
|
378
|
+
const info = await requireTable(db, req.params.table);
|
|
379
|
+
if (!info)
|
|
380
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
381
|
+
// Safety: refuse drops that would break constraints or relationships.
|
|
382
|
+
const schema = await db.getSchema(req.params.table);
|
|
383
|
+
if (schema.success) {
|
|
384
|
+
const col = schema.data?.columns.find((c) => c.name === req.params.column);
|
|
385
|
+
if (col && !col.canDrop) {
|
|
386
|
+
return fail(res, `Cannot drop column "${col.name}": ${col.dropBlockers.join('; ')}.`, 400);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
let sql;
|
|
390
|
+
try {
|
|
391
|
+
sql = (0, generator_1.generateDropColumn)(req.params.table, req.params.column);
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
395
|
+
}
|
|
396
|
+
const r = await db.dropColumn(req.params.table, req.params.column);
|
|
397
|
+
if (!r.success)
|
|
398
|
+
return fail(res, r.error ?? 'Failed to drop column.', 400);
|
|
399
|
+
ok(res, { message: `Column "${req.params.column}" dropped.`, sql });
|
|
400
|
+
}));
|
|
401
|
+
router.delete('/api/tables/:table', wrap(async (req, res) => {
|
|
402
|
+
const info = await requireTable(db, req.params.table);
|
|
403
|
+
if (!info)
|
|
404
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
405
|
+
if (req.params.table.startsWith('_'))
|
|
406
|
+
return fail(res, 'Internal tables cannot be dropped.');
|
|
407
|
+
const schema = await db.getSchema(req.params.table);
|
|
408
|
+
if (schema.success && schema.data && schema.data.references.length > 0) {
|
|
409
|
+
return fail(res, `Cannot drop "${req.params.table}": it is referenced by foreign keys in ${schema.data.references.map((r) => `"${r.table}"`).join(', ')}.`, 400);
|
|
410
|
+
}
|
|
411
|
+
let sql;
|
|
412
|
+
try {
|
|
413
|
+
sql = (0, generator_1.generateDropTable)(req.params.table);
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
417
|
+
}
|
|
418
|
+
const r = await db.dropTable(req.params.table);
|
|
419
|
+
if (!r.success)
|
|
420
|
+
return fail(res, r.error ?? 'Failed to drop table.', 400);
|
|
421
|
+
ok(res, { message: `Table "${req.params.table}" dropped.`, sql });
|
|
422
|
+
}));
|
|
423
|
+
// ---- Index management --------------------------------------------------
|
|
424
|
+
router.post('/api/tables/:table/indexes', wrap(async (req, res) => {
|
|
425
|
+
const info = await requireTable(db, req.params.table);
|
|
426
|
+
if (!info)
|
|
427
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
428
|
+
const { name, columns, unique } = (req.body ?? {});
|
|
429
|
+
const index = { name, columns: Array.isArray(columns) ? columns : [], unique: !!unique };
|
|
430
|
+
let sql;
|
|
431
|
+
try {
|
|
432
|
+
sql = (0, generator_1.generateCreateIndex)(req.params.table, index);
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
436
|
+
}
|
|
437
|
+
const r = await db.createIndex(req.params.table, index);
|
|
438
|
+
if (!r.success)
|
|
439
|
+
return fail(res, r.error ?? 'Failed to create index.', 400);
|
|
440
|
+
ok(res, { message: `Index created.`, sql }, 201);
|
|
441
|
+
}));
|
|
442
|
+
router.delete('/api/tables/:table/indexes/:index', wrap(async (req, res) => {
|
|
443
|
+
const info = await requireTable(db, req.params.table);
|
|
444
|
+
if (!info)
|
|
445
|
+
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
446
|
+
const schema = await db.getSchema(req.params.table);
|
|
447
|
+
const ix = schema.success
|
|
448
|
+
? schema.data?.indexes.find((i) => i.name === req.params.index)
|
|
449
|
+
: undefined;
|
|
450
|
+
if (ix && ix.origin !== 'c') {
|
|
451
|
+
return fail(res, `Cannot drop "${ix.name}": it is an automatic ${ix.origin === 'pk' ? 'primary-key' : 'unique'} index.`, 400);
|
|
452
|
+
}
|
|
453
|
+
let sql;
|
|
454
|
+
try {
|
|
455
|
+
sql = (0, generator_1.generateDropIndex)(req.params.index);
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
459
|
+
}
|
|
460
|
+
const r = await db.dropIndex(req.params.index);
|
|
461
|
+
if (!r.success)
|
|
462
|
+
return fail(res, r.error ?? 'Failed to drop index.', 400);
|
|
463
|
+
ok(res, { message: `Index "${req.params.index}" dropped.`, sql });
|
|
464
|
+
}));
|
|
465
|
+
// ---- Views & triggers --------------------------------------------------
|
|
466
|
+
router.get('/api/views', wrap(async (_req, res) => {
|
|
467
|
+
const r = await db.listViews();
|
|
468
|
+
if (!r.success)
|
|
469
|
+
return fail(res, r.error ?? 'Failed to list views.', 500);
|
|
470
|
+
ok(res, r.data);
|
|
471
|
+
}));
|
|
472
|
+
router.get('/api/views/:name/rows', wrap(async (req, res) => {
|
|
473
|
+
const r = await db.getViewRows(req.params.name);
|
|
474
|
+
if (!r.success)
|
|
475
|
+
return fail(res, r.error ?? 'Failed to read view.', 400);
|
|
476
|
+
ok(res, { rows: (r.data ?? []).map(util_1.normalizeRow) });
|
|
477
|
+
}));
|
|
478
|
+
router.post('/api/views/generate', wrap(async (req, res) => {
|
|
479
|
+
const name = String(req.body?.name ?? '').trim();
|
|
480
|
+
const sql = String(req.body?.sql ?? '').trim();
|
|
481
|
+
try {
|
|
482
|
+
ok(res, { sql: (0, generator_1.generateCreateView)(name, sql) });
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
fail(res, (0, util_1.errorMessage)(err));
|
|
486
|
+
}
|
|
487
|
+
}));
|
|
488
|
+
router.post('/api/views', wrap(async (req, res) => {
|
|
489
|
+
const name = String(req.body?.name ?? '').trim();
|
|
490
|
+
const sql = String(req.body?.sql ?? '').trim();
|
|
491
|
+
let genSql;
|
|
492
|
+
try {
|
|
493
|
+
genSql = (0, generator_1.generateCreateView)(name, sql);
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
497
|
+
}
|
|
498
|
+
const r = await db.createView(name, sql);
|
|
499
|
+
if (!r.success)
|
|
500
|
+
return fail(res, r.error ?? 'Failed to create view.', 400);
|
|
501
|
+
ok(res, { message: `View "${name}" created.`, sql: genSql }, 201);
|
|
502
|
+
}));
|
|
503
|
+
router.delete('/api/views/:name', wrap(async (req, res) => {
|
|
504
|
+
let genSql;
|
|
505
|
+
try {
|
|
506
|
+
genSql = (0, generator_1.generateDropView)(req.params.name);
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
510
|
+
}
|
|
511
|
+
const r = await db.dropView(req.params.name);
|
|
512
|
+
if (!r.success)
|
|
513
|
+
return fail(res, r.error ?? 'Failed to drop view.', 400);
|
|
514
|
+
ok(res, { message: `View "${req.params.name}" dropped.`, sql: genSql });
|
|
515
|
+
}));
|
|
516
|
+
router.get('/api/triggers', wrap(async (_req, res) => {
|
|
517
|
+
const r = await db.listTriggers();
|
|
518
|
+
if (!r.success)
|
|
519
|
+
return fail(res, r.error ?? 'Failed to list triggers.', 500);
|
|
520
|
+
ok(res, r.data);
|
|
521
|
+
}));
|
|
522
|
+
router.post('/api/triggers/generate', wrap(async (req, res) => {
|
|
523
|
+
try {
|
|
524
|
+
ok(res, { sql: (0, generator_1.generateCreateTrigger)((req.body ?? {})) });
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
fail(res, (0, util_1.errorMessage)(err));
|
|
528
|
+
}
|
|
529
|
+
}));
|
|
530
|
+
router.post('/api/triggers', wrap(async (req, res) => {
|
|
531
|
+
const def = (req.body ?? {});
|
|
532
|
+
let genSql;
|
|
533
|
+
try {
|
|
534
|
+
genSql = (0, generator_1.generateCreateTrigger)(def);
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
538
|
+
}
|
|
539
|
+
const r = await db.createTrigger(def);
|
|
540
|
+
if (!r.success)
|
|
541
|
+
return fail(res, r.error ?? 'Failed to create trigger.', 400);
|
|
542
|
+
ok(res, { message: `Trigger "${def.name}" created.`, sql: genSql }, 201);
|
|
543
|
+
}));
|
|
544
|
+
router.delete('/api/triggers/:name', wrap(async (req, res) => {
|
|
545
|
+
let genSql;
|
|
546
|
+
try {
|
|
547
|
+
genSql = (0, generator_1.generateDropTrigger)(req.params.name);
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
return fail(res, (0, util_1.errorMessage)(err));
|
|
551
|
+
}
|
|
552
|
+
const r = await db.dropTrigger(req.params.name);
|
|
553
|
+
if (!r.success)
|
|
554
|
+
return fail(res, r.error ?? 'Failed to drop trigger.', 400);
|
|
555
|
+
ok(res, { message: `Trigger "${req.params.name}" dropped.`, sql: genSql });
|
|
556
|
+
}));
|
|
557
|
+
// ---- Query runner ------------------------------------------------------
|
|
558
|
+
router.post('/api/query', wrap(async (req, res) => {
|
|
559
|
+
const sql = String(req.body?.sql ?? '').trim();
|
|
560
|
+
if (!sql)
|
|
561
|
+
return fail(res, 'SQL is required.');
|
|
562
|
+
const { kind } = (0, classifier_1.classifySql)(sql);
|
|
563
|
+
if (kind === 'count') {
|
|
564
|
+
const r = await db.all(sql);
|
|
565
|
+
if (!r.success)
|
|
566
|
+
return fail(res, r.error ?? 'Query failed.', 400);
|
|
567
|
+
const rows = (r.data ?? []);
|
|
568
|
+
const first = rows[0];
|
|
569
|
+
const val = first ? Object.values(first)[0] : 0;
|
|
570
|
+
ok(res, { kind: 'count', count: Number(val ?? 0), message: `Total: ${val ?? 0}` });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (kind === 'select' || kind === 'read') {
|
|
574
|
+
const r = await db.all(sql);
|
|
575
|
+
if (!r.success)
|
|
576
|
+
return fail(res, r.error ?? 'Query failed.', 400);
|
|
577
|
+
const rows = (r.data ?? []).map(util_1.normalizeRow);
|
|
578
|
+
const columns = rows.length ? Object.keys(rows[0]) : [];
|
|
579
|
+
ok(res, { kind: 'select', columns, rows });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// write
|
|
583
|
+
if (db.isReadOnly) {
|
|
584
|
+
return fail(res, 'Database is open in read-only mode — write statements are disabled.', 403);
|
|
585
|
+
}
|
|
586
|
+
const r = await db.runWrite(sql);
|
|
587
|
+
if (!r.success)
|
|
588
|
+
return fail(res, r.error ?? 'Query failed.', 400);
|
|
589
|
+
const changes = r.data?.changes;
|
|
590
|
+
ok(res, {
|
|
591
|
+
kind: 'write',
|
|
592
|
+
changes: changes ?? null,
|
|
593
|
+
message: changes != null ? `${changes} row(s) affected.` : 'Statement executed successfully.',
|
|
594
|
+
});
|
|
595
|
+
}));
|
|
596
|
+
// ---- Saved queries -----------------------------------------------------
|
|
597
|
+
router.get('/api/queries', wrap(async (_req, res) => {
|
|
598
|
+
const r = await db.listSavedQueries();
|
|
599
|
+
if (!r.success)
|
|
600
|
+
return fail(res, r.error ?? 'Failed to load queries.', 500);
|
|
601
|
+
ok(res, r.data);
|
|
602
|
+
}));
|
|
603
|
+
router.post('/api/queries', wrap(async (req, res) => {
|
|
604
|
+
const name = String(req.body?.name ?? '').trim();
|
|
605
|
+
const sql = String(req.body?.sql ?? '').trim();
|
|
606
|
+
if (!name)
|
|
607
|
+
return fail(res, 'Query name is required.');
|
|
608
|
+
if (!sql)
|
|
609
|
+
return fail(res, 'Query SQL is required.');
|
|
610
|
+
const r = await db.saveQuery(name, sql);
|
|
611
|
+
if (!r.success)
|
|
612
|
+
return fail(res, r.error ?? 'Failed to save query.', 400);
|
|
613
|
+
ok(res, { message: 'Query saved.' }, 201);
|
|
614
|
+
}));
|
|
615
|
+
router.delete('/api/queries/:id', wrap(async (req, res) => {
|
|
616
|
+
const r = await db.deleteSavedQuery(req.params.id);
|
|
617
|
+
if (!r.success)
|
|
618
|
+
return fail(res, r.error ?? 'Failed to delete query.', 400);
|
|
619
|
+
ok(res, { message: 'Query deleted.' });
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
622
|
+
//# sourceMappingURL=api.js.map
|