admindb 1.2.0 → 2.0.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/README.md +250 -352
- package/dist/app.d.ts +25 -4
- package/dist/app.js +79 -15
- package/dist/app.js.map +1 -1
- package/dist/auth/config.d.ts +19 -0
- package/dist/auth/config.js +147 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/crypto.d.ts +27 -0
- package/dist/auth/crypto.js +133 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.js +22 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +14 -0
- package/dist/auth/middleware.js +100 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/routes.d.ts +7 -0
- package/dist/auth/routes.js +78 -0
- package/dist/auth/routes.js.map +1 -0
- package/dist/auth/types.d.ts +35 -0
- package/dist/auth/types.js +11 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli/args.d.ts +45 -0
- package/dist/cli/args.js +328 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/config.d.ts +56 -0
- package/dist/cli/config.js +133 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/runner.d.ts +1 -0
- package/dist/cli/runner.js +222 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli.js +2 -127
- package/dist/cli.js.map +1 -1
- package/dist/data/datasets.d.ts +17 -0
- package/dist/data/datasets.js +133 -0
- package/dist/data/datasets.js.map +1 -0
- package/dist/data/detector.d.ts +30 -0
- package/dist/data/detector.js +472 -0
- package/dist/data/detector.js.map +1 -0
- package/dist/data/engine.d.ts +10 -0
- package/dist/data/engine.js +211 -0
- package/dist/data/engine.js.map +1 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.js +22 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/strategies.d.ts +30 -0
- package/dist/data/strategies.js +343 -0
- package/dist/data/strategies.js.map +1 -0
- package/dist/data/types.d.ts +73 -0
- package/dist/data/types.js +8 -0
- package/dist/data/types.js.map +1 -0
- package/dist/db/database.d.ts +14 -142
- package/dist/db/database.js +209 -435
- package/dist/db/database.js.map +1 -1
- package/dist/db/export.d.ts +2 -2
- package/dist/db/export.js +7 -2
- package/dist/db/export.js.map +1 -1
- package/dist/db/filters.d.ts +22 -0
- package/dist/db/filters.js +158 -0
- package/dist/db/filters.js.map +1 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +25 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/introspection.d.ts +6 -0
- package/dist/db/introspection.js +172 -0
- package/dist/db/introspection.js.map +1 -0
- package/dist/db/manager.d.ts +32 -19
- package/dist/db/manager.js +203 -44
- package/dist/db/manager.js.map +1 -1
- package/dist/db/migrations.d.ts +5 -0
- package/dist/db/migrations.js +110 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/postgres.d.ts +85 -0
- package/dist/db/postgres.js +745 -0
- package/dist/db/postgres.js.map +1 -0
- package/dist/db/types.d.ts +206 -0
- package/dist/db/types.js +7 -0
- package/dist/db/types.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +46 -4
- package/dist/index.js.map +1 -1
- package/dist/public/css/app.css +1 -1
- package/dist/public/css/input.css +83 -13
- package/dist/public/js/app.js +142 -0
- package/dist/public/js/browse.js +591 -104
- package/dist/public/js/databases.js +105 -13
- package/dist/public/js/designer.js +40 -2
- package/dist/public/js/forms.js +610 -28
- package/dist/public/js/icons.js +51 -0
- package/dist/public/js/inspector.js +638 -0
- package/dist/public/js/query.js +93 -3
- package/dist/public/js/schema.js +170 -52
- package/dist/public/js/seed.js +1090 -0
- package/dist/routes/api/helpers.d.ts +42 -0
- package/dist/routes/api/helpers.js +133 -0
- package/dist/routes/api/helpers.js.map +1 -0
- package/dist/routes/api/import-export.d.ts +3 -0
- package/dist/routes/api/import-export.js +85 -0
- package/dist/routes/api/import-export.js.map +1 -0
- package/dist/routes/api/index.d.ts +4 -0
- package/dist/routes/api/index.js +37 -0
- package/dist/routes/api/index.js.map +1 -0
- package/dist/routes/api/query.d.ts +3 -0
- package/dist/routes/api/query.js +76 -0
- package/dist/routes/api/query.js.map +1 -0
- package/dist/routes/api/rows.d.ts +3 -0
- package/dist/routes/api/rows.js +373 -0
- package/dist/routes/api/rows.js.map +1 -0
- package/dist/routes/api/seed.d.ts +3 -0
- package/dist/routes/api/seed.js +82 -0
- package/dist/routes/api/seed.js.map +1 -0
- package/dist/routes/api/tables.d.ts +3 -0
- package/dist/routes/api/tables.js +160 -0
- package/dist/routes/api/tables.js.map +1 -0
- package/dist/routes/databases.d.ts +8 -11
- package/dist/routes/databases.js +100 -66
- package/dist/routes/databases.js.map +1 -1
- package/dist/routes/index.d.ts +3 -0
- package/dist/routes/index.js +20 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/pages.d.ts +3 -3
- package/dist/routes/pages.js +163 -54
- package/dist/routes/pages.js.map +1 -1
- package/dist/serverless.d.ts +40 -0
- package/dist/serverless.js +210 -0
- package/dist/serverless.js.map +1 -0
- package/dist/sql/generator.d.ts +4 -3
- package/dist/sql/generator.js +103 -6
- package/dist/sql/generator.js.map +1 -1
- package/dist/sql/index.d.ts +2 -0
- package/dist/sql/index.js +19 -0
- package/dist/sql/index.js.map +1 -0
- package/dist/types/api.d.ts +412 -0
- package/dist/types/api.js +9 -0
- package/dist/types/api.js.map +1 -0
- package/dist/utils/colors.d.ts +40 -0
- package/dist/utils/colors.js +64 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/{util.d.ts → utils/common.d.ts} +10 -6
- package/dist/{util.js → utils/common.js} +80 -13
- package/dist/utils/common.js.map +1 -0
- package/dist/utils/csv.js.map +1 -0
- package/dist/utils/datatype.d.ts +49 -0
- package/dist/utils/datatype.js +222 -0
- package/dist/utils/datatype.js.map +1 -0
- package/dist/utils/icons.d.ts +11 -0
- package/dist/utils/icons.js +72 -0
- package/dist/utils/icons.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/{logger.d.ts → utils/logger.d.ts} +1 -1
- package/dist/utils/logger.js +40 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/views/layouts/main.hbs +33 -11
- package/dist/views/pages/databases.hbs +182 -74
- package/dist/views/pages/designer.hbs +9 -6
- package/dist/views/pages/error.hbs +3 -3
- package/dist/views/pages/form.hbs +5 -3
- package/dist/views/pages/home.hbs +22 -13
- package/dist/views/pages/login.hbs +111 -0
- package/dist/views/pages/query.hbs +10 -8
- package/dist/views/pages/schema.hbs +41 -13
- package/dist/views/pages/seed.hbs +232 -0
- package/dist/views/pages/table.hbs +155 -70
- package/dist/views/partials/icon.hbs +2 -0
- package/dist/views/partials/navbar.hbs +70 -31
- package/dist/views/partials/sidebar.hbs +108 -99
- package/package.json +12 -5
- package/dist/args.d.ts +0 -28
- package/dist/args.js +0 -187
- package/dist/args.js.map +0 -1
- package/dist/config.d.ts +0 -17
- package/dist/config.js +0 -23
- package/dist/config.js.map +0 -1
- package/dist/csv.js.map +0 -1
- package/dist/logger.js +0 -25
- package/dist/logger.js.map +0 -1
- package/dist/routes/api.d.ts +0 -9
- package/dist/routes/api.js +0 -786
- package/dist/routes/api.js.map +0 -1
- package/dist/test/classifier.test.d.ts +0 -1
- package/dist/test/classifier.test.js +0 -44
- package/dist/test/classifier.test.js.map +0 -1
- package/dist/test/csv.test.d.ts +0 -1
- package/dist/test/csv.test.js +0 -64
- package/dist/test/csv.test.js.map +0 -1
- package/dist/test/database.test.d.ts +0 -1
- package/dist/test/database.test.js +0 -402
- package/dist/test/database.test.js.map +0 -1
- package/dist/test/generator.test.d.ts +0 -1
- package/dist/test/generator.test.js +0 -132
- package/dist/test/generator.test.js.map +0 -1
- package/dist/test/manager.test.d.ts +0 -1
- package/dist/test/manager.test.js +0 -146
- package/dist/test/manager.test.js.map +0 -1
- package/dist/util.js.map +0 -1
- /package/dist/{csv.d.ts → utils/csv.d.ts} +0 -0
- /package/dist/{csv.js → utils/csv.js} +0 -0
package/dist/routes/api.js
DELETED
|
@@ -1,786 +0,0 @@
|
|
|
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
|
-
/**
|
|
47
|
-
* Build the SET fields for an UPDATE from form values plus an explicit list of
|
|
48
|
-
* columns to null out (the "clear to NULL" affordance of inline editing).
|
|
49
|
-
*/
|
|
50
|
-
function buildUpdateFields(info, values, nulls) {
|
|
51
|
-
const fields = buildFields(info, values, { excludePk: true });
|
|
52
|
-
const typeMap = new Map(info.columns.map((c) => [c.name, c.type]));
|
|
53
|
-
const pkSet = new Set(info.primaryKey);
|
|
54
|
-
for (const col of nulls) {
|
|
55
|
-
if (pkSet.has(col) || !typeMap.has(col))
|
|
56
|
-
continue;
|
|
57
|
-
if (fields.some((f) => f.column === col))
|
|
58
|
-
continue;
|
|
59
|
-
fields.push({ column: col, value: null });
|
|
60
|
-
}
|
|
61
|
-
return fields;
|
|
62
|
-
}
|
|
63
|
-
function pkWhere(info, encodedId) {
|
|
64
|
-
if (!info.primaryKey.length)
|
|
65
|
-
return null;
|
|
66
|
-
const vals = (0, util_1.decodePk)(encodedId);
|
|
67
|
-
if (vals.length !== info.primaryKey.length)
|
|
68
|
-
return null;
|
|
69
|
-
const typeMap = new Map(info.columns.map((c) => [c.name, c.type]));
|
|
70
|
-
return info.primaryKey.map((col, i) => ({ column: col, value: (0, util_1.coerceFormValue)(vals[i], typeMap.get(col)) }));
|
|
71
|
-
}
|
|
72
|
-
/** Hard cap on how many rows can be selected for a bulk operation at once. */
|
|
73
|
-
const MAX_BULK_ROWS = 1000;
|
|
74
|
-
/**
|
|
75
|
-
* Turn a list of encoded primary-key ids (from the grid's row checkboxes) into
|
|
76
|
-
* the WHERE clauses needed to address each row. Returns null when the table has
|
|
77
|
-
* no usable primary key or when none of the ids resolve.
|
|
78
|
-
*/
|
|
79
|
-
function resolvePkRows(info, ids) {
|
|
80
|
-
if (!info.primaryKey.length)
|
|
81
|
-
return null;
|
|
82
|
-
const list = Array.isArray(ids) ? ids : ids == null ? [] : [ids];
|
|
83
|
-
const out = [];
|
|
84
|
-
for (const id of list) {
|
|
85
|
-
const where = pkWhere(info, String(id));
|
|
86
|
-
if (where)
|
|
87
|
-
out.push(where);
|
|
88
|
-
}
|
|
89
|
-
return out.length ? out : null;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* How many rows in OTHER tables reference any of the selected rows (via foreign
|
|
93
|
-
* keys pointing at the selected table). Used to warn before a bulk delete —
|
|
94
|
-
* those rows may be cascaded away or orphaned depending on the FK action.
|
|
95
|
-
*/
|
|
96
|
-
async function computeBulkImpact(db, info, wheres) {
|
|
97
|
-
const rowsR = await db.getRowsByPks(info.table, wheres);
|
|
98
|
-
const rows = (rowsR.data ?? []);
|
|
99
|
-
const refsR = await db.getReferencingTables(info.table);
|
|
100
|
-
const references = [];
|
|
101
|
-
const toColumn = (to) => to || info.primaryKey[0] || info.columns[0]?.name || null;
|
|
102
|
-
for (const rt of refsR.data ?? []) {
|
|
103
|
-
if (rt.table.startsWith('_'))
|
|
104
|
-
continue;
|
|
105
|
-
for (const ref of rt.refs) {
|
|
106
|
-
const col = toColumn(ref.to);
|
|
107
|
-
if (!col)
|
|
108
|
-
continue;
|
|
109
|
-
// Deduplicate the referenced values (preserving their native type).
|
|
110
|
-
const byStr = new Map();
|
|
111
|
-
for (const row of rows) {
|
|
112
|
-
const v = row[col];
|
|
113
|
-
if (v === null || v === undefined)
|
|
114
|
-
continue;
|
|
115
|
-
byStr.set(String((0, util_1.normalizeCell)(v)), v);
|
|
116
|
-
}
|
|
117
|
-
if (!byStr.size)
|
|
118
|
-
continue;
|
|
119
|
-
const values = [...byStr.values()];
|
|
120
|
-
const placeholders = values.map(() => '?').join(', ');
|
|
121
|
-
const cR = await db.all(`SELECT COUNT(*) AS c FROM ${(0, generator_1.quoteIdentifier)(rt.table)} WHERE ${(0, generator_1.quoteIdentifier)(ref.from)} IN (${placeholders})`, values);
|
|
122
|
-
const count = Number((cR.data ?? [])[0]?.c ?? 0);
|
|
123
|
-
if (count > 0)
|
|
124
|
-
references.push({ table: rt.table, from: ref.from, to: ref.to, count });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return { references, total: references.reduce((n, r) => n + r.count, 0) };
|
|
128
|
-
}
|
|
129
|
-
function registerApi(router, ctx) {
|
|
130
|
-
const { db, logger } = ctx;
|
|
131
|
-
// In read-only mode every mutating request is rejected (GET, the generate-only
|
|
132
|
-
// preview endpoints and the query runner stay available; the query runner
|
|
133
|
-
// rejects write statements itself).
|
|
134
|
-
router.use((req, res, next) => {
|
|
135
|
-
if (!db.isReadOnly || req.method === 'GET')
|
|
136
|
-
return next();
|
|
137
|
-
const p = req.path;
|
|
138
|
-
if (p.endsWith('/generate') ||
|
|
139
|
-
p.endsWith('/api/query/export') ||
|
|
140
|
-
p.endsWith('/api/query') ||
|
|
141
|
-
p.endsWith('/rows/bulk-impact') ||
|
|
142
|
-
p.endsWith('/rows/bulk-export')) {
|
|
143
|
-
return next();
|
|
144
|
-
}
|
|
145
|
-
return res.status(403).json({ success: false, error: 'Database is open in read-only mode — write operations are disabled.' });
|
|
146
|
-
});
|
|
147
|
-
// ---- Tables ------------------------------------------------------------
|
|
148
|
-
router.get('/api/tables', wrap(async (_req, res) => {
|
|
149
|
-
const r = await db.listTables();
|
|
150
|
-
if (!r.success)
|
|
151
|
-
return fail(res, r.error ?? 'Failed to list tables.', 500);
|
|
152
|
-
ok(res, r.data);
|
|
153
|
-
}));
|
|
154
|
-
router.get('/api/tables/:table/info', wrap(async (req, res) => {
|
|
155
|
-
const info = await requireTable(db, req.params.table);
|
|
156
|
-
if (!info)
|
|
157
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
158
|
-
ok(res, info);
|
|
159
|
-
}));
|
|
160
|
-
router.get('/api/tables/:table/fk-options', wrap(async (req, res) => {
|
|
161
|
-
const info = await requireTable(db, req.params.table);
|
|
162
|
-
if (!info)
|
|
163
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
164
|
-
const options = {};
|
|
165
|
-
for (const fk of info.foreignKeys) {
|
|
166
|
-
try {
|
|
167
|
-
const refInfo = await requireTable(db, fk.table);
|
|
168
|
-
if (!refInfo)
|
|
169
|
-
continue;
|
|
170
|
-
// The FK may reference a non-PK unique column (e.g. Users.UserName),
|
|
171
|
-
// so the option value must be the referenced column's value, not the
|
|
172
|
-
// referenced table's primary key.
|
|
173
|
-
const refCol = fk.to || refInfo.primaryKey[0] || refInfo.columns[0]?.name;
|
|
174
|
-
const pk = refInfo.primaryKey[0] ?? refInfo.columns[0]?.name;
|
|
175
|
-
const labelCols = refInfo.columns.filter((c) => c.name !== pk).slice(0, 2).map((c) => c.name);
|
|
176
|
-
const rowsR = await db.getAllRows(fk.table);
|
|
177
|
-
const rows = (rowsR.data ?? []).slice(0, 500);
|
|
178
|
-
options[fk.from] = rows.map((row) => ({
|
|
179
|
-
value: row[refCol],
|
|
180
|
-
label: labelCols.length ? labelCols.map((c) => String(row[c] ?? '')).join(' · ') : String(row[refCol] ?? ''),
|
|
181
|
-
}));
|
|
182
|
-
}
|
|
183
|
-
catch (err) {
|
|
184
|
-
logger.warn(`fk-options for ${fk.from}: ${(0, util_1.errorMessage)(err)}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
ok(res, options);
|
|
188
|
-
}));
|
|
189
|
-
router.get('/api/tables/:table/rows', wrap(async (req, res) => {
|
|
190
|
-
const info = await requireTable(db, req.params.table);
|
|
191
|
-
if (!info)
|
|
192
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
193
|
-
const page = Math.max(1, Number.parseInt(String(req.query.page ?? '1'), 10) || 1);
|
|
194
|
-
const limit = Math.max(1, Math.min(1000, Number.parseInt(String(req.query.limit ?? '50'), 10) || 50));
|
|
195
|
-
const orderBy = req.query.orderBy ? String(req.query.orderBy) : undefined;
|
|
196
|
-
const orderDir = String(req.query.orderDir ?? 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc';
|
|
197
|
-
const filters = (0, util_1.parseFilters)(req.query.f);
|
|
198
|
-
const rowsR = await db.getRows(req.params.table, { page, limit, orderBy, orderDir, filters });
|
|
199
|
-
if (!rowsR.success)
|
|
200
|
-
return fail(res, rowsR.error ?? 'Failed to load rows.', 500);
|
|
201
|
-
const countR = await db.getRowCount(req.params.table, filters);
|
|
202
|
-
ok(res, {
|
|
203
|
-
rows: (rowsR.data ?? []).map(util_1.normalizeRow),
|
|
204
|
-
count: countR.success ? countR.data : 0,
|
|
205
|
-
page,
|
|
206
|
-
limit,
|
|
207
|
-
filters,
|
|
208
|
-
});
|
|
209
|
-
}));
|
|
210
|
-
// Related rows: rows in other tables whose foreign keys point at this row.
|
|
211
|
-
router.get('/api/tables/:table/rows/:id/references', wrap(async (req, res) => {
|
|
212
|
-
const info = await requireTable(db, req.params.table);
|
|
213
|
-
if (!info)
|
|
214
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
215
|
-
if (!info.primaryKey.length)
|
|
216
|
-
return fail(res, `Table "${req.params.table}" has no primary key, so related rows cannot be resolved.`, 400);
|
|
217
|
-
const pk = (0, util_1.decodePk)(req.params.id);
|
|
218
|
-
if (pk.length !== info.primaryKey.length)
|
|
219
|
-
return fail(res, 'Invalid row id.', 400);
|
|
220
|
-
const typeMap = new Map(info.columns.map((c) => [c.name, c.type]));
|
|
221
|
-
const where = info.primaryKey.map((col, i) => ({ column: col, value: (0, util_1.coerceFormValue)(pk[i], typeMap.get(col)) }));
|
|
222
|
-
const rowR = await db.getRow(req.params.table, where);
|
|
223
|
-
if (!rowR.success || !rowR.data)
|
|
224
|
-
return fail(res, 'Row not found.', 404);
|
|
225
|
-
const row = rowR.data;
|
|
226
|
-
const refsR = await db.getReferencingTables(req.params.table);
|
|
227
|
-
if (!refsR.success)
|
|
228
|
-
return fail(res, refsR.error ?? 'Failed to load references.', 500);
|
|
229
|
-
const references = [];
|
|
230
|
-
for (const rt of refsR.data ?? []) {
|
|
231
|
-
if (rt.table.startsWith('_'))
|
|
232
|
-
continue;
|
|
233
|
-
const refInfo = await requireTable(db, rt.table);
|
|
234
|
-
if (!refInfo)
|
|
235
|
-
continue;
|
|
236
|
-
for (const ref of rt.refs) {
|
|
237
|
-
const value = row[ref.to];
|
|
238
|
-
if (value === null || value === undefined)
|
|
239
|
-
continue;
|
|
240
|
-
const r = await db.getRowsByFk(rt.table, ref.from, value, 50);
|
|
241
|
-
if (!r.success)
|
|
242
|
-
continue;
|
|
243
|
-
references.push({
|
|
244
|
-
table: rt.table,
|
|
245
|
-
from: ref.from,
|
|
246
|
-
to: ref.to,
|
|
247
|
-
value: (0, util_1.normalizeCell)(value),
|
|
248
|
-
count: r.data.total,
|
|
249
|
-
columns: refInfo.columns.map((c) => c.name),
|
|
250
|
-
rows: r.data.rows.map(util_1.normalizeRow),
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
ok(res, { references });
|
|
255
|
-
}));
|
|
256
|
-
// ---- Export / import ---------------------------------------------------
|
|
257
|
-
router.get('/api/tables/:table/export', wrap(async (req, res) => {
|
|
258
|
-
const info = await requireTable(db, req.params.table);
|
|
259
|
-
if (!info)
|
|
260
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
261
|
-
const format = String(req.query.format ?? 'csv').toLowerCase() === 'json' ? 'json' : 'csv';
|
|
262
|
-
const rowsR = await db.getAllRows(req.params.table);
|
|
263
|
-
if (!rowsR.success)
|
|
264
|
-
return fail(res, rowsR.error ?? 'Export failed.', 500);
|
|
265
|
-
const rows = (rowsR.data ?? []).map(util_1.normalizeRow);
|
|
266
|
-
const columns = info.columns.map((c) => c.name);
|
|
267
|
-
const body = format === 'json' ? (0, csv_1.toJson)(rows) : (0, csv_1.toCsv)(rows, columns);
|
|
268
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
269
|
-
res.setHeader('Content-Type', format === 'json' ? 'application/json; charset=utf-8' : 'text/csv; charset=utf-8');
|
|
270
|
-
res.setHeader('Content-Disposition', `attachment; filename="${req.params.table}-${stamp}.${format}"`);
|
|
271
|
-
res.send(body);
|
|
272
|
-
}));
|
|
273
|
-
router.post('/api/tables/:table/rows/import', wrap(async (req, res) => {
|
|
274
|
-
const info = await requireTable(db, req.params.table);
|
|
275
|
-
if (!info)
|
|
276
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
277
|
-
const csvText = String(req.body?.csv ?? '');
|
|
278
|
-
if (!csvText.trim())
|
|
279
|
-
return fail(res, 'CSV content is required.');
|
|
280
|
-
const rows = (0, csv_1.parseCsv)(csvText);
|
|
281
|
-
if (rows.length < 2)
|
|
282
|
-
return fail(res, 'CSV must include a header row and at least one data row.');
|
|
283
|
-
const headers = rows[0].map((h) => String(h).trim());
|
|
284
|
-
if (!headers.length)
|
|
285
|
-
return fail(res, 'CSV header is empty.');
|
|
286
|
-
const colSet = new Set(info.columns.map((c) => c.name));
|
|
287
|
-
const unknown = headers.filter((h) => !colSet.has(h));
|
|
288
|
-
if (unknown.length)
|
|
289
|
-
return fail(res, `Unknown column(s) in CSV header: ${unknown.join(', ')}.`);
|
|
290
|
-
if (new Set(headers).size !== headers.length)
|
|
291
|
-
return fail(res, 'Duplicate column(s) in CSV header.');
|
|
292
|
-
const typeMap = new Map(info.columns.map((c) => [c.name, c.type]));
|
|
293
|
-
const dataRows = rows
|
|
294
|
-
.slice(1)
|
|
295
|
-
.filter((r) => r.length !== 1 || r[0] !== '')
|
|
296
|
-
.map((r) => {
|
|
297
|
-
const fields = [];
|
|
298
|
-
headers.forEach((h, i) => {
|
|
299
|
-
const val = (0, util_1.coerceFormValue)(r[i], typeMap.get(h));
|
|
300
|
-
if (val !== null)
|
|
301
|
-
fields.push({ column: h, value: val });
|
|
302
|
-
});
|
|
303
|
-
return fields;
|
|
304
|
-
});
|
|
305
|
-
if (!dataRows.length)
|
|
306
|
-
return fail(res, 'No data rows to import.');
|
|
307
|
-
const result = await db.insertRows(req.params.table, dataRows);
|
|
308
|
-
if (!result.success)
|
|
309
|
-
return fail(res, result.error ?? 'Import failed.', 400);
|
|
310
|
-
ok(res, {
|
|
311
|
-
message: `Imported ${result.data?.inserted} row(s).`,
|
|
312
|
-
inserted: result.data?.inserted,
|
|
313
|
-
skipped: result.data?.skipped,
|
|
314
|
-
}, 201);
|
|
315
|
-
}));
|
|
316
|
-
router.post('/api/query/export', wrap(async (req, res) => {
|
|
317
|
-
const sql = String(req.body?.sql ?? '').trim();
|
|
318
|
-
if (!sql)
|
|
319
|
-
return fail(res, 'SQL is required.');
|
|
320
|
-
const format = String(req.body?.format ?? 'csv').toLowerCase() === 'json' ? 'json' : 'csv';
|
|
321
|
-
const r = await db.all(sql);
|
|
322
|
-
if (!r.success)
|
|
323
|
-
return fail(res, r.error ?? 'Query failed.', 400);
|
|
324
|
-
const rows = (r.data ?? []).map(util_1.normalizeRow);
|
|
325
|
-
const columns = rows.length ? Object.keys(rows[0]) : [];
|
|
326
|
-
const body = format === 'json' ? (0, csv_1.toJson)(rows) : (0, csv_1.toCsv)(rows, columns);
|
|
327
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
328
|
-
res.setHeader('Content-Type', format === 'json' ? 'application/json; charset=utf-8' : 'text/csv; charset=utf-8');
|
|
329
|
-
res.setHeader('Content-Disposition', `attachment; filename="query-${stamp}.${format}"`);
|
|
330
|
-
res.send(body);
|
|
331
|
-
}));
|
|
332
|
-
router.get('/api/tables/:table/row/:id', wrap(async (req, res) => {
|
|
333
|
-
const info = await requireTable(db, req.params.table);
|
|
334
|
-
if (!info)
|
|
335
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
336
|
-
const where = pkWhere(info, req.params.id);
|
|
337
|
-
if (!where)
|
|
338
|
-
return fail(res, 'Invalid primary key.', 400);
|
|
339
|
-
const r = await db.getRow(req.params.table, where);
|
|
340
|
-
if (!r.success)
|
|
341
|
-
return fail(res, r.error ?? 'Failed to load row.', 500);
|
|
342
|
-
if (!r.data)
|
|
343
|
-
return fail(res, 'Row not found.', 404);
|
|
344
|
-
ok(res, (0, util_1.normalizeRow)(r.data));
|
|
345
|
-
}));
|
|
346
|
-
// ---- Row CRUD (run + generate variants) -------------------------------
|
|
347
|
-
router.post('/api/tables/:table/rows', wrap(async (req, res) => {
|
|
348
|
-
const info = await requireTable(db, req.params.table);
|
|
349
|
-
if (!info)
|
|
350
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
351
|
-
const values = (req.body?.values ?? {});
|
|
352
|
-
const fields = buildFields(info, values);
|
|
353
|
-
if (!fields.length)
|
|
354
|
-
return fail(res, 'No values provided.');
|
|
355
|
-
const sql = (0, generator_1.generateInsert)(req.params.table, fields);
|
|
356
|
-
const r = await db.insertRow(req.params.table, fields);
|
|
357
|
-
if (!r.success)
|
|
358
|
-
return fail(res, r.error ?? 'Insert failed.', 400);
|
|
359
|
-
ok(res, { message: 'Row inserted.', rowId: r.data?.lastInsertRowid, sql }, 201);
|
|
360
|
-
}));
|
|
361
|
-
router.post('/api/tables/:table/rows/generate', wrap(async (req, res) => {
|
|
362
|
-
const info = await requireTable(db, req.params.table);
|
|
363
|
-
if (!info)
|
|
364
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
365
|
-
const values = (req.body?.values ?? {});
|
|
366
|
-
const fields = buildFields(info, values);
|
|
367
|
-
if (!fields.length)
|
|
368
|
-
return fail(res, 'No values provided.');
|
|
369
|
-
ok(res, { sql: (0, generator_1.generateInsert)(req.params.table, fields) });
|
|
370
|
-
}));
|
|
371
|
-
router.put('/api/tables/:table/row/:id', wrap(async (req, res) => {
|
|
372
|
-
const info = await requireTable(db, req.params.table);
|
|
373
|
-
if (!info)
|
|
374
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
375
|
-
const where = pkWhere(info, req.params.id);
|
|
376
|
-
if (!where)
|
|
377
|
-
return fail(res, 'Invalid primary key.', 400);
|
|
378
|
-
const values = (req.body?.values ?? {});
|
|
379
|
-
const nulls = Array.isArray(req.body?.nulls) ? req.body.nulls.map(String) : [];
|
|
380
|
-
const fields = buildUpdateFields(info, values, nulls);
|
|
381
|
-
if (!fields.length)
|
|
382
|
-
return fail(res, 'No fields to update.');
|
|
383
|
-
const sql = (0, generator_1.generateUpdate)(req.params.table, fields, where);
|
|
384
|
-
const r = await db.updateRow(req.params.table, fields, where);
|
|
385
|
-
if (!r.success)
|
|
386
|
-
return fail(res, r.error ?? 'Update failed.', 400);
|
|
387
|
-
ok(res, { message: 'Row updated.', sql });
|
|
388
|
-
}));
|
|
389
|
-
router.put('/api/tables/:table/row/:id/generate', wrap(async (req, res) => {
|
|
390
|
-
const info = await requireTable(db, req.params.table);
|
|
391
|
-
if (!info)
|
|
392
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
393
|
-
const where = pkWhere(info, req.params.id);
|
|
394
|
-
if (!where)
|
|
395
|
-
return fail(res, 'Invalid primary key.', 400);
|
|
396
|
-
const values = (req.body?.values ?? {});
|
|
397
|
-
const nulls = Array.isArray(req.body?.nulls) ? req.body.nulls.map(String) : [];
|
|
398
|
-
const fields = buildUpdateFields(info, values, nulls);
|
|
399
|
-
if (!fields.length)
|
|
400
|
-
return fail(res, 'No fields to update.');
|
|
401
|
-
ok(res, { sql: (0, generator_1.generateUpdate)(req.params.table, fields, where) });
|
|
402
|
-
}));
|
|
403
|
-
router.delete('/api/tables/:table/row/:id', wrap(async (req, res) => {
|
|
404
|
-
const info = await requireTable(db, req.params.table);
|
|
405
|
-
if (!info)
|
|
406
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
407
|
-
const where = pkWhere(info, req.params.id);
|
|
408
|
-
if (!where)
|
|
409
|
-
return fail(res, 'Invalid primary key.', 400);
|
|
410
|
-
const r = await db.deleteRow(req.params.table, where);
|
|
411
|
-
if (!r.success)
|
|
412
|
-
return fail(res, r.error ?? 'Delete failed.', 400);
|
|
413
|
-
ok(res, { message: 'Row deleted.' });
|
|
414
|
-
}));
|
|
415
|
-
// ---- Bulk row operations -----------------------------------------------
|
|
416
|
-
/**
|
|
417
|
-
* FK-impact preview: how many rows in other tables reference the selected
|
|
418
|
-
* rows. Shown to the user before a bulk delete so cascades/orphaning are
|
|
419
|
-
* understood up front.
|
|
420
|
-
*/
|
|
421
|
-
router.post('/api/tables/:table/rows/bulk-impact', wrap(async (req, res) => {
|
|
422
|
-
const info = await requireTable(db, req.params.table);
|
|
423
|
-
if (!info)
|
|
424
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
425
|
-
const wheres = resolvePkRows(info, (req.body ?? {}).ids);
|
|
426
|
-
if (!wheres || !wheres.length)
|
|
427
|
-
return fail(res, 'No valid rows selected.', 400);
|
|
428
|
-
if (wheres.length > MAX_BULK_ROWS)
|
|
429
|
-
return fail(res, `Select at most ${MAX_BULK_ROWS} rows at a time.`, 400);
|
|
430
|
-
const impact = await computeBulkImpact(db, info, wheres);
|
|
431
|
-
ok(res, { selected: wheres.length, references: impact.references, total: impact.total });
|
|
432
|
-
}));
|
|
433
|
-
/**
|
|
434
|
-
* Bulk delete the selected rows inside a single transaction. When other
|
|
435
|
-
* tables reference them, the client must acknowledge the impact
|
|
436
|
-
* (`confirmImpact: true`) — this is the safety net behind the UI warning.
|
|
437
|
-
*/
|
|
438
|
-
router.post('/api/tables/:table/rows/bulk-delete', wrap(async (req, res) => {
|
|
439
|
-
const info = await requireTable(db, req.params.table);
|
|
440
|
-
if (!info)
|
|
441
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
442
|
-
const body = (req.body ?? {});
|
|
443
|
-
const wheres = resolvePkRows(info, body.ids);
|
|
444
|
-
if (!wheres || !wheres.length)
|
|
445
|
-
return fail(res, 'No valid rows selected.', 400);
|
|
446
|
-
if (wheres.length > MAX_BULK_ROWS)
|
|
447
|
-
return fail(res, `Select at most ${MAX_BULK_ROWS} rows at a time.`, 400);
|
|
448
|
-
const impact = await computeBulkImpact(db, info, wheres);
|
|
449
|
-
if (impact.total > 0 && body.confirmImpact !== true) {
|
|
450
|
-
return res.status(409).json({
|
|
451
|
-
success: false,
|
|
452
|
-
error: 'These rows are referenced by other tables. Confirm the impact before deleting.',
|
|
453
|
-
data: { references: impact.references, total: impact.total },
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
const r = await db.deleteRows(req.params.table, wheres);
|
|
457
|
-
if (!r.success)
|
|
458
|
-
return fail(res, r.error ?? 'Bulk delete failed.', 400);
|
|
459
|
-
ok(res, { message: `Deleted ${r.data?.deleted ?? 0} row(s).`, deleted: r.data?.deleted ?? 0 });
|
|
460
|
-
}));
|
|
461
|
-
/**
|
|
462
|
-
* Export only the selected rows (CSV or JSON). The content is returned in the
|
|
463
|
-
* JSON envelope so the client can trigger a download without a full page reload.
|
|
464
|
-
*/
|
|
465
|
-
router.post('/api/tables/:table/rows/bulk-export', wrap(async (req, res) => {
|
|
466
|
-
const info = await requireTable(db, req.params.table);
|
|
467
|
-
if (!info)
|
|
468
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
469
|
-
const body = (req.body ?? {});
|
|
470
|
-
const wheres = resolvePkRows(info, body.ids);
|
|
471
|
-
if (!wheres || !wheres.length)
|
|
472
|
-
return fail(res, 'No valid rows selected.', 400);
|
|
473
|
-
if (wheres.length > MAX_BULK_ROWS)
|
|
474
|
-
return fail(res, `Select at most ${MAX_BULK_ROWS} rows at a time.`, 400);
|
|
475
|
-
const format = String(body.format ?? 'csv').toLowerCase() === 'json' ? 'json' : 'csv';
|
|
476
|
-
const rowsR = await db.getRowsByPks(req.params.table, wheres);
|
|
477
|
-
if (!rowsR.success)
|
|
478
|
-
return fail(res, rowsR.error ?? 'Export failed.', 500);
|
|
479
|
-
const rows = (rowsR.data ?? []).map(util_1.normalizeRow);
|
|
480
|
-
const columns = info.columns.map((c) => c.name);
|
|
481
|
-
const content = format === 'json' ? (0, csv_1.toJson)(rows) : (0, csv_1.toCsv)(rows, columns);
|
|
482
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
483
|
-
ok(res, {
|
|
484
|
-
format,
|
|
485
|
-
filename: `${req.params.table}-selected-${stamp}.${format}`,
|
|
486
|
-
content,
|
|
487
|
-
rowCount: rows.length,
|
|
488
|
-
});
|
|
489
|
-
}));
|
|
490
|
-
/**
|
|
491
|
-
* Apply staged inline-grid edits: update many cells across many rows inside a
|
|
492
|
-
* single transaction. Each entry is `{ id, values?, nulls? }` where `id` is
|
|
493
|
-
* the encoded primary key. This is the "Apply" step of the Neon-style
|
|
494
|
-
* spreadsheet editing flow (edits are buffered client-side until this runs).
|
|
495
|
-
*/
|
|
496
|
-
router.post('/api/tables/:table/rows/bulk-update', wrap(async (req, res) => {
|
|
497
|
-
const info = await requireTable(db, req.params.table);
|
|
498
|
-
if (!info)
|
|
499
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
500
|
-
if (!info.primaryKey.length) {
|
|
501
|
-
return fail(res, `Table "${req.params.table}" has no primary key, so inline edits cannot be applied.`, 400);
|
|
502
|
-
}
|
|
503
|
-
const updates = Array.isArray(req.body?.updates) ? req.body.updates : [];
|
|
504
|
-
if (!updates.length)
|
|
505
|
-
return fail(res, 'No changes to apply.', 400);
|
|
506
|
-
if (updates.length > MAX_BULK_ROWS)
|
|
507
|
-
return fail(res, `Select at most ${MAX_BULK_ROWS} rows at a time.`, 400);
|
|
508
|
-
const rows = [];
|
|
509
|
-
for (const u of updates) {
|
|
510
|
-
const entry = (u ?? {});
|
|
511
|
-
const where = pkWhere(info, String(entry.id ?? ''));
|
|
512
|
-
if (!where)
|
|
513
|
-
continue;
|
|
514
|
-
const fields = buildUpdateFields(info, (entry.values ?? {}), Array.isArray(entry.nulls) ? entry.nulls.map(String) : []);
|
|
515
|
-
if (fields.length)
|
|
516
|
-
rows.push({ fields, where });
|
|
517
|
-
}
|
|
518
|
-
if (!rows.length)
|
|
519
|
-
return fail(res, 'No fields to update.', 400);
|
|
520
|
-
const r = await db.updateRows(req.params.table, rows);
|
|
521
|
-
if (!r.success)
|
|
522
|
-
return fail(res, r.error ?? 'Update failed.', 400);
|
|
523
|
-
ok(res, {
|
|
524
|
-
message: `Applied ${r.data?.updated ?? 0} row change(s).`,
|
|
525
|
-
updated: r.data?.updated ?? 0,
|
|
526
|
-
});
|
|
527
|
-
}));
|
|
528
|
-
// ---- Create table (run + generate) ------------------------------------
|
|
529
|
-
router.post('/api/tables', wrap(async (req, res) => {
|
|
530
|
-
const { name, columns } = req.body ?? {};
|
|
531
|
-
if (!Array.isArray(columns))
|
|
532
|
-
return fail(res, 'Columns must be an array.');
|
|
533
|
-
let sql;
|
|
534
|
-
try {
|
|
535
|
-
sql = (0, generator_1.generateCreateTable)(name, columns);
|
|
536
|
-
}
|
|
537
|
-
catch (err) {
|
|
538
|
-
return fail(res, (0, util_1.errorMessage)(err));
|
|
539
|
-
}
|
|
540
|
-
const r = await db.execResult(sql);
|
|
541
|
-
if (!r.success)
|
|
542
|
-
return fail(res, r.error ?? 'Create table failed.', 400);
|
|
543
|
-
ok(res, { message: `Table "${name}" created.`, sql }, 201);
|
|
544
|
-
}));
|
|
545
|
-
router.post('/api/tables/generate', wrap(async (req, res) => {
|
|
546
|
-
const { name, columns } = req.body ?? {};
|
|
547
|
-
if (!Array.isArray(columns))
|
|
548
|
-
return fail(res, 'Columns must be an array.');
|
|
549
|
-
try {
|
|
550
|
-
ok(res, { sql: (0, generator_1.generateCreateTable)(name, columns) });
|
|
551
|
-
}
|
|
552
|
-
catch (err) {
|
|
553
|
-
fail(res, (0, util_1.errorMessage)(err));
|
|
554
|
-
}
|
|
555
|
-
}));
|
|
556
|
-
// ---- Schema editing ----------------------------------------------------
|
|
557
|
-
router.get('/api/tables/:table/schema', wrap(async (req, res) => {
|
|
558
|
-
const info = await requireTable(db, req.params.table);
|
|
559
|
-
if (!info)
|
|
560
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
561
|
-
const r = await db.getSchema(req.params.table);
|
|
562
|
-
if (!r.success)
|
|
563
|
-
return fail(res, r.error ?? 'Failed to load schema.', 500);
|
|
564
|
-
ok(res, r.data);
|
|
565
|
-
}));
|
|
566
|
-
router.post('/api/tables/:table/rename', wrap(async (req, res) => {
|
|
567
|
-
const info = await requireTable(db, req.params.table);
|
|
568
|
-
if (!info)
|
|
569
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
570
|
-
if (req.params.table.startsWith('_'))
|
|
571
|
-
return fail(res, 'Internal tables cannot be renamed.');
|
|
572
|
-
const name = String(req.body?.name ?? '').trim();
|
|
573
|
-
let sql;
|
|
574
|
-
try {
|
|
575
|
-
sql = (0, generator_1.generateRenameTable)(req.params.table, name);
|
|
576
|
-
}
|
|
577
|
-
catch (err) {
|
|
578
|
-
return fail(res, (0, util_1.errorMessage)(err));
|
|
579
|
-
}
|
|
580
|
-
const r = await db.renameTable(req.params.table, name);
|
|
581
|
-
if (!r.success)
|
|
582
|
-
return fail(res, r.error ?? 'Rename failed.', 400);
|
|
583
|
-
ok(res, { message: `Table renamed to "${name}".`, sql, table: name });
|
|
584
|
-
}));
|
|
585
|
-
router.post('/api/tables/:table/columns', wrap(async (req, res) => {
|
|
586
|
-
const info = await requireTable(db, req.params.table);
|
|
587
|
-
if (!info)
|
|
588
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
589
|
-
const col = (req.body?.column ?? null);
|
|
590
|
-
if (!col || typeof col !== 'object')
|
|
591
|
-
return fail(res, 'Column definition is required.');
|
|
592
|
-
let sql;
|
|
593
|
-
try {
|
|
594
|
-
sql = (0, generator_1.generateAddColumn)(req.params.table, col);
|
|
595
|
-
}
|
|
596
|
-
catch (err) {
|
|
597
|
-
return fail(res, (0, util_1.errorMessage)(err));
|
|
598
|
-
}
|
|
599
|
-
const r = await db.addColumn(req.params.table, col);
|
|
600
|
-
if (!r.success)
|
|
601
|
-
return fail(res, r.error ?? 'Failed to add column.', 400);
|
|
602
|
-
ok(res, { message: `Column "${String(col.name)}" added.`, sql }, 201);
|
|
603
|
-
}));
|
|
604
|
-
router.put('/api/tables/:table/columns/:column', wrap(async (req, res) => {
|
|
605
|
-
const info = await requireTable(db, req.params.table);
|
|
606
|
-
if (!info)
|
|
607
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
608
|
-
const newName = String(req.body?.name ?? '').trim();
|
|
609
|
-
let sql;
|
|
610
|
-
try {
|
|
611
|
-
sql = (0, generator_1.generateRenameColumn)(req.params.table, req.params.column, newName);
|
|
612
|
-
}
|
|
613
|
-
catch (err) {
|
|
614
|
-
return fail(res, (0, util_1.errorMessage)(err));
|
|
615
|
-
}
|
|
616
|
-
const r = await db.renameColumn(req.params.table, req.params.column, newName);
|
|
617
|
-
if (!r.success)
|
|
618
|
-
return fail(res, r.error ?? 'Failed to rename column.', 400);
|
|
619
|
-
ok(res, { message: `Column renamed to "${newName}".`, sql });
|
|
620
|
-
}));
|
|
621
|
-
router.delete('/api/tables/:table/columns/:column', wrap(async (req, res) => {
|
|
622
|
-
const info = await requireTable(db, req.params.table);
|
|
623
|
-
if (!info)
|
|
624
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
625
|
-
// Safety: refuse drops that would break constraints or relationships.
|
|
626
|
-
const schema = await db.getSchema(req.params.table);
|
|
627
|
-
if (schema.success) {
|
|
628
|
-
const col = schema.data?.columns.find((c) => c.name === req.params.column);
|
|
629
|
-
if (col && !col.canDrop) {
|
|
630
|
-
return fail(res, `Cannot drop column "${col.name}": ${col.dropBlockers.join('; ')}.`, 400);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
let sql;
|
|
634
|
-
try {
|
|
635
|
-
sql = (0, generator_1.generateDropColumn)(req.params.table, req.params.column);
|
|
636
|
-
}
|
|
637
|
-
catch (err) {
|
|
638
|
-
return fail(res, (0, util_1.errorMessage)(err));
|
|
639
|
-
}
|
|
640
|
-
const r = await db.dropColumn(req.params.table, req.params.column);
|
|
641
|
-
if (!r.success)
|
|
642
|
-
return fail(res, r.error ?? 'Failed to drop column.', 400);
|
|
643
|
-
ok(res, { message: `Column "${req.params.column}" dropped.`, sql });
|
|
644
|
-
}));
|
|
645
|
-
router.delete('/api/tables/:table', wrap(async (req, res) => {
|
|
646
|
-
const info = await requireTable(db, req.params.table);
|
|
647
|
-
if (!info)
|
|
648
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
649
|
-
if (req.params.table.startsWith('_'))
|
|
650
|
-
return fail(res, 'Internal tables cannot be dropped.');
|
|
651
|
-
const schema = await db.getSchema(req.params.table);
|
|
652
|
-
if (schema.success && schema.data && schema.data.references.length > 0) {
|
|
653
|
-
return fail(res, `Cannot drop "${req.params.table}": it is referenced by foreign keys in ${schema.data.references.map((r) => `"${r.table}"`).join(', ')}.`, 400);
|
|
654
|
-
}
|
|
655
|
-
let sql;
|
|
656
|
-
try {
|
|
657
|
-
sql = (0, generator_1.generateDropTable)(req.params.table);
|
|
658
|
-
}
|
|
659
|
-
catch (err) {
|
|
660
|
-
return fail(res, (0, util_1.errorMessage)(err));
|
|
661
|
-
}
|
|
662
|
-
const r = await db.dropTable(req.params.table);
|
|
663
|
-
if (!r.success)
|
|
664
|
-
return fail(res, r.error ?? 'Failed to drop table.', 400);
|
|
665
|
-
ok(res, { message: `Table "${req.params.table}" dropped.`, sql });
|
|
666
|
-
}));
|
|
667
|
-
// ---- Index management --------------------------------------------------
|
|
668
|
-
router.post('/api/tables/:table/indexes', wrap(async (req, res) => {
|
|
669
|
-
const info = await requireTable(db, req.params.table);
|
|
670
|
-
if (!info)
|
|
671
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
672
|
-
const { name, columns, unique } = (req.body ?? {});
|
|
673
|
-
const index = { name, columns: Array.isArray(columns) ? columns : [], unique: !!unique };
|
|
674
|
-
let sql;
|
|
675
|
-
try {
|
|
676
|
-
sql = (0, generator_1.generateCreateIndex)(req.params.table, index);
|
|
677
|
-
}
|
|
678
|
-
catch (err) {
|
|
679
|
-
return fail(res, (0, util_1.errorMessage)(err));
|
|
680
|
-
}
|
|
681
|
-
const r = await db.createIndex(req.params.table, index);
|
|
682
|
-
if (!r.success)
|
|
683
|
-
return fail(res, r.error ?? 'Failed to create index.', 400);
|
|
684
|
-
ok(res, { message: `Index created.`, sql }, 201);
|
|
685
|
-
}));
|
|
686
|
-
router.delete('/api/tables/:table/indexes/:index', wrap(async (req, res) => {
|
|
687
|
-
const info = await requireTable(db, req.params.table);
|
|
688
|
-
if (!info)
|
|
689
|
-
return fail(res, `Table "${req.params.table}" does not exist.`, 404);
|
|
690
|
-
const schema = await db.getSchema(req.params.table);
|
|
691
|
-
const ix = schema.success
|
|
692
|
-
? schema.data?.indexes.find((i) => i.name === req.params.index)
|
|
693
|
-
: undefined;
|
|
694
|
-
if (ix && ix.origin !== 'c') {
|
|
695
|
-
return fail(res, `Cannot drop "${ix.name}": it is an automatic ${ix.origin === 'pk' ? 'primary-key' : 'unique'} index.`, 400);
|
|
696
|
-
}
|
|
697
|
-
let sql;
|
|
698
|
-
try {
|
|
699
|
-
sql = (0, generator_1.generateDropIndex)(req.params.index);
|
|
700
|
-
}
|
|
701
|
-
catch (err) {
|
|
702
|
-
return fail(res, (0, util_1.errorMessage)(err));
|
|
703
|
-
}
|
|
704
|
-
const r = await db.dropIndex(req.params.index);
|
|
705
|
-
if (!r.success)
|
|
706
|
-
return fail(res, r.error ?? 'Failed to drop index.', 400);
|
|
707
|
-
ok(res, { message: `Index "${req.params.index}" dropped.`, sql });
|
|
708
|
-
}));
|
|
709
|
-
// ---- Query runner ------------------------------------------------------
|
|
710
|
-
router.post('/api/query', wrap(async (req, res) => {
|
|
711
|
-
const sql = String(req.body?.sql ?? '').trim();
|
|
712
|
-
if (!sql)
|
|
713
|
-
return fail(res, 'SQL is required.');
|
|
714
|
-
// Scripts (more than one statement, e.g. a schema/migration script) cannot
|
|
715
|
-
// be represented as a single prepared statement, so run them via `exec`.
|
|
716
|
-
if (db.hasMultipleStatements(sql)) {
|
|
717
|
-
if (db.isReadOnly) {
|
|
718
|
-
return fail(res, 'Database is open in read-only mode — write statements are disabled.', 403);
|
|
719
|
-
}
|
|
720
|
-
const r = await db.execResult(sql);
|
|
721
|
-
if (!r.success)
|
|
722
|
-
return fail(res, r.error ?? 'Query failed.', 400);
|
|
723
|
-
ok(res, { kind: 'write', changes: null, message: 'Statement(s) executed successfully.' });
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
const { kind } = (0, classifier_1.classifySql)(sql);
|
|
727
|
-
if (kind === 'count') {
|
|
728
|
-
const r = await db.all(sql);
|
|
729
|
-
if (!r.success)
|
|
730
|
-
return fail(res, r.error ?? 'Query failed.', 400);
|
|
731
|
-
const rows = (r.data ?? []);
|
|
732
|
-
const first = rows[0];
|
|
733
|
-
const val = first ? Object.values(first)[0] : 0;
|
|
734
|
-
ok(res, { kind: 'count', count: Number(val ?? 0), message: `Total: ${val ?? 0}` });
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
if (kind === 'select' || kind === 'read') {
|
|
738
|
-
const r = await db.all(sql);
|
|
739
|
-
if (!r.success)
|
|
740
|
-
return fail(res, r.error ?? 'Query failed.', 400);
|
|
741
|
-
const rows = (r.data ?? []).map(util_1.normalizeRow);
|
|
742
|
-
const columns = rows.length ? Object.keys(rows[0]) : [];
|
|
743
|
-
ok(res, { kind: 'select', columns, rows });
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
// write
|
|
747
|
-
if (db.isReadOnly) {
|
|
748
|
-
return fail(res, 'Database is open in read-only mode — write statements are disabled.', 403);
|
|
749
|
-
}
|
|
750
|
-
const r = await db.runWrite(sql);
|
|
751
|
-
if (!r.success)
|
|
752
|
-
return fail(res, r.error ?? 'Query failed.', 400);
|
|
753
|
-
const changes = r.data?.changes;
|
|
754
|
-
ok(res, {
|
|
755
|
-
kind: 'write',
|
|
756
|
-
changes: changes ?? null,
|
|
757
|
-
message: changes != null ? `${changes} row(s) affected.` : 'Statement executed successfully.',
|
|
758
|
-
});
|
|
759
|
-
}));
|
|
760
|
-
// ---- Saved queries -----------------------------------------------------
|
|
761
|
-
router.get('/api/queries', wrap(async (_req, res) => {
|
|
762
|
-
const r = await db.listSavedQueries();
|
|
763
|
-
if (!r.success)
|
|
764
|
-
return fail(res, r.error ?? 'Failed to load queries.', 500);
|
|
765
|
-
ok(res, r.data);
|
|
766
|
-
}));
|
|
767
|
-
router.post('/api/queries', wrap(async (req, res) => {
|
|
768
|
-
const name = String(req.body?.name ?? '').trim();
|
|
769
|
-
const sql = String(req.body?.sql ?? '').trim();
|
|
770
|
-
if (!name)
|
|
771
|
-
return fail(res, 'Query name is required.');
|
|
772
|
-
if (!sql)
|
|
773
|
-
return fail(res, 'Query SQL is required.');
|
|
774
|
-
const r = await db.saveQuery(name, sql);
|
|
775
|
-
if (!r.success)
|
|
776
|
-
return fail(res, r.error ?? 'Failed to save query.', 400);
|
|
777
|
-
ok(res, { message: 'Query saved.' }, 201);
|
|
778
|
-
}));
|
|
779
|
-
router.delete('/api/queries/:id', wrap(async (req, res) => {
|
|
780
|
-
const r = await db.deleteSavedQuery(req.params.id);
|
|
781
|
-
if (!r.success)
|
|
782
|
-
return fail(res, r.error ?? 'Failed to delete query.', 400);
|
|
783
|
-
ok(res, { message: 'Query deleted.' });
|
|
784
|
-
}));
|
|
785
|
-
}
|
|
786
|
-
//# sourceMappingURL=api.js.map
|