admindb 1.2.1 → 2.1.0

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