admindb 2.1.1 → 2.3.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 +368 -335
- package/dist/app.js +227 -1
- package/dist/auth/config.d.ts +1 -1
- package/dist/auth/config.js +109 -1
- package/dist/auth/crypto.js +98 -1
- package/dist/auth/index.js +21 -1
- package/dist/auth/middleware.js +78 -1
- package/dist/auth/routes.js +64 -1
- package/dist/auth/types.js +7 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +309 -1
- package/dist/cli/config.js +118 -1
- package/dist/cli/index.js +20 -1
- package/dist/cli/runner.js +208 -1
- package/dist/cli.js +4 -1
- package/dist/data/chain.d.ts +1 -0
- package/dist/data/chain.js +130 -0
- package/dist/data/datasets.d.ts +1 -1
- package/dist/data/datasets.js +171 -1
- package/dist/data/detector.d.ts +1 -1
- package/dist/data/detector.js +535 -1
- package/dist/data/engine.d.ts +1 -1
- package/dist/data/engine.js +586 -1
- package/dist/data/index.d.ts +1 -1
- package/dist/data/index.js +30 -1
- package/dist/data/prng.d.ts +1 -0
- package/dist/data/prng.js +73 -0
- package/dist/data/registry.d.ts +1 -0
- package/dist/data/registry.js +805 -0
- package/dist/data/schema-graph.d.ts +1 -0
- package/dist/data/schema-graph.js +210 -0
- package/dist/data/strategies.d.ts +1 -1
- package/dist/data/strategies.js +100 -1
- package/dist/data/templates.d.ts +1 -0
- package/dist/data/templates.js +134 -0
- package/dist/data/types.d.ts +1 -1
- package/dist/data/types.js +5 -1
- package/dist/data/validator.d.ts +1 -0
- package/dist/data/validator.js +160 -0
- package/dist/db/database.d.ts +1 -1
- package/dist/db/database.js +472 -1
- package/dist/db/export.d.ts +1 -1
- package/dist/db/export.js +67 -1
- package/dist/db/filters.d.ts +1 -1
- package/dist/db/filters.js +119 -1
- package/dist/db/index.js +24 -1
- package/dist/db/introspection.js +164 -1
- package/dist/db/manager.js +327 -1
- package/dist/db/migrations.js +100 -1
- package/dist/db/postgres.d.ts +1 -1
- package/dist/db/postgres.js +687 -1
- package/dist/db/types.d.ts +1 -1
- package/dist/db/types.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +52 -1
- package/dist/public/css/app.css +1 -1
- package/dist/public/js/api.js +1 -1
- package/dist/public/js/browse.js +1 -1
- package/dist/public/js/common.js +1 -0
- package/dist/public/js/databases.js +1 -1
- package/dist/public/js/designer.js +1 -1
- package/dist/public/js/forms.js +1 -1
- package/dist/public/js/home.js +1 -0
- package/dist/public/js/inspector.js +1 -1
- package/dist/public/js/query.js +1 -1
- package/dist/public/js/schema.js +1 -1
- package/dist/public/js/seed.js +1 -1
- package/dist/routes/api/erd.d.ts +1 -0
- package/dist/routes/api/erd.js +45 -0
- package/dist/routes/api/helpers.d.ts +1 -1
- package/dist/routes/api/helpers.js +213 -1
- package/dist/routes/api/import-export.js +82 -1
- package/dist/routes/api/index.js +48 -1
- package/dist/routes/api/query.js +74 -1
- package/dist/routes/api/rows.js +358 -1
- package/dist/routes/api/seed.js +167 -1
- package/dist/routes/api/tables.js +216 -1
- package/dist/routes/databases.js +210 -1
- package/dist/routes/index.js +19 -1
- package/dist/routes/pages.js +446 -1
- package/dist/serverless.js +149 -1
- package/dist/sql/classifier.js +24 -1
- package/dist/sql/error-analyzer.d.ts +1 -0
- package/dist/sql/error-analyzer.js +550 -0
- package/dist/sql/generator.d.ts +1 -1
- package/dist/sql/generator.js +245 -1
- package/dist/sql/index.js +18 -1
- package/dist/types/api.d.ts +1 -1
- package/dist/types/api.js +2 -1
- package/dist/utils/colors.js +54 -1
- package/dist/utils/common.d.ts +1 -1
- package/dist/utils/common.js +185 -1
- package/dist/utils/csv.d.ts +1 -1
- package/dist/utils/csv.js +61 -1
- package/dist/utils/datatype.js +167 -1
- package/dist/utils/icons.js +81 -1
- package/dist/utils/index.js +20 -1
- package/dist/utils/logger.js +36 -1
- package/dist/views/layouts/main.hbs +1 -74
- package/dist/views/pages/databases.hbs +1 -233
- package/dist/views/pages/designer.hbs +1 -89
- package/dist/views/pages/erd.hbs +1 -0
- package/dist/views/pages/error.hbs +1 -10
- package/dist/views/pages/form.hbs +1 -59
- package/dist/views/pages/home.hbs +1 -124
- package/dist/views/pages/info.hbs +1 -0
- package/dist/views/pages/login.hbs +1 -98
- package/dist/views/pages/query.hbs +1 -87
- package/dist/views/pages/schema.hbs +1 -298
- package/dist/views/pages/seed-select.hbs +251 -0
- package/dist/views/pages/seed.hbs +273 -167
- package/dist/views/pages/table.hbs +1 -327
- package/dist/views/partials/navbar.hbs +1 -83
- package/dist/views/partials/sidebar.hbs +1 -117
- package/docs/API.md +149 -144
- package/docs/EXAMPLES.md +579 -576
- package/docs/SECURITY.md +265 -265
- package/package.json +79 -78
- package/dist/public/js/icons.js +0 -1
package/dist/routes/pages.js
CHANGED
|
@@ -1 +1,446 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerPages = registerPages;
|
|
4
|
+
const export_1 = require("../db/export");
|
|
5
|
+
const generator_1 = require("../sql/generator");
|
|
6
|
+
const common_1 = require("../utils/common");
|
|
7
|
+
const datatype_1 = require("../utils/datatype");
|
|
8
|
+
const index_1 = require("../data/index");
|
|
9
|
+
function buildDisplayRows(rawRows, info, table, basePath = '') {
|
|
10
|
+
const pkCols = info.primaryKey.length ? info.primaryKey : ['_rowid_'];
|
|
11
|
+
return rawRows.map((row) => {
|
|
12
|
+
const pkEncoded = (0, common_1.encodePk)(pkCols.map((c) => row[c]));
|
|
13
|
+
return {
|
|
14
|
+
cells: info.columns.map((c) => {
|
|
15
|
+
const raw = row[c.name];
|
|
16
|
+
const v = (0, common_1.normalizeCell)(raw);
|
|
17
|
+
const isNull = v == null;
|
|
18
|
+
const typeUpper = (c.type || '').toUpperCase();
|
|
19
|
+
const isBlob = typeUpper.includes('BLOB') || typeUpper.includes('BYTEA') || Buffer.isBuffer(raw) || raw instanceof Uint8Array || (typeof v === 'string' && (/^0x[0-9a-f]{8,}$/i.test(v) || /^\\x[0-9a-f]{8,}$/i.test(v)));
|
|
20
|
+
let isJson = false;
|
|
21
|
+
let isImage = false;
|
|
22
|
+
let isUrl = false;
|
|
23
|
+
let isColor = false;
|
|
24
|
+
let isAudio = false;
|
|
25
|
+
let isVideo = false;
|
|
26
|
+
let isPdf = false;
|
|
27
|
+
let blobSize = '';
|
|
28
|
+
let blobMime = '';
|
|
29
|
+
let blobExt = '';
|
|
30
|
+
let blobUrl = '';
|
|
31
|
+
if (!isNull) {
|
|
32
|
+
const str = String(v);
|
|
33
|
+
if (isBlob) {
|
|
34
|
+
let buf;
|
|
35
|
+
if (Buffer.isBuffer(raw) || raw instanceof Uint8Array) {
|
|
36
|
+
buf = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
37
|
+
}
|
|
38
|
+
else if (typeof v === 'string' && (/^0x[0-9a-f]*$/i.test(v) || /^\\x[0-9a-f]*$/i.test(v))) {
|
|
39
|
+
buf = Buffer.from(v.slice(2), 'hex');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
buf = Buffer.from(str, 'utf8');
|
|
43
|
+
}
|
|
44
|
+
const mimeInfo = (0, datatype_1.sniffMimeType)(buf);
|
|
45
|
+
blobSize = (0, common_1.formatBytes)(buf.length);
|
|
46
|
+
blobMime = mimeInfo.mime;
|
|
47
|
+
blobExt = mimeInfo.ext;
|
|
48
|
+
isImage = mimeInfo.isImage;
|
|
49
|
+
isAudio = mimeInfo.isAudio;
|
|
50
|
+
isVideo = mimeInfo.isVideo;
|
|
51
|
+
isPdf = mimeInfo.isPdf;
|
|
52
|
+
if (table && pkEncoded) {
|
|
53
|
+
blobUrl = `${basePath}/api/tables/${encodeURIComponent(table)}/row/${encodeURIComponent(pkEncoded)}/blob/${encodeURIComponent(c.name)}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if ((0, datatype_1.isJsonString)(v) || typeUpper.includes('JSON')) {
|
|
57
|
+
isJson = (0, datatype_1.isJsonString)(v);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
if (/^https?:\/\/[^\s$.?#].[^\s]*$/i.test(str))
|
|
61
|
+
isUrl = true;
|
|
62
|
+
else if (/^#(?:[0-9a-fA-F]{3}){1,2}$|^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+/i.test(str))
|
|
63
|
+
isColor = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
name: c.name,
|
|
68
|
+
value: v,
|
|
69
|
+
isNull,
|
|
70
|
+
display: isNull ? '' : String(v),
|
|
71
|
+
type: c.type,
|
|
72
|
+
isBlob,
|
|
73
|
+
isJson,
|
|
74
|
+
isImage,
|
|
75
|
+
isAudio,
|
|
76
|
+
isVideo,
|
|
77
|
+
isPdf,
|
|
78
|
+
isUrl,
|
|
79
|
+
isColor,
|
|
80
|
+
blobSize,
|
|
81
|
+
blobMime,
|
|
82
|
+
blobExt,
|
|
83
|
+
blobUrl,
|
|
84
|
+
};
|
|
85
|
+
}),
|
|
86
|
+
pkEncoded,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function registerPages(router, ctx) {
|
|
91
|
+
const { db } = ctx;
|
|
92
|
+
const notFound = (res, message) => {
|
|
93
|
+
res.status(404).render('pages/error', { title: 'Not found', status: 404, error: message });
|
|
94
|
+
};
|
|
95
|
+
router.get('/', async (_req, res, next) => {
|
|
96
|
+
try {
|
|
97
|
+
const names = res.locals.tables ?? [];
|
|
98
|
+
const internalNames = res.locals.internalTables ?? [];
|
|
99
|
+
const colCountsMap = {};
|
|
100
|
+
if (db.dialect === 'postgres') {
|
|
101
|
+
const colRes = await db.all(`SELECT table_name, count(*)::int AS cols FROM information_schema.columns WHERE table_schema = 'public' GROUP BY table_name;`);
|
|
102
|
+
if (colRes.success && colRes.data) {
|
|
103
|
+
for (const row of colRes.data) {
|
|
104
|
+
if (row && typeof row.table_name === 'string') {
|
|
105
|
+
colCountsMap[row.table_name] = Number(row.cols ?? 0);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const [stats, saved] = await Promise.all([
|
|
111
|
+
Promise.all(names.map(async (name) => {
|
|
112
|
+
const countR = await db.getRowCount(name);
|
|
113
|
+
const cols = colCountsMap[name] !== undefined
|
|
114
|
+
? colCountsMap[name]
|
|
115
|
+
: (await db.getTableInfo(name)).data?.columns.length ?? 0;
|
|
116
|
+
return {
|
|
117
|
+
name,
|
|
118
|
+
count: countR.success ? countR.data : 0,
|
|
119
|
+
cols,
|
|
120
|
+
};
|
|
121
|
+
})),
|
|
122
|
+
db.listSavedQueries(),
|
|
123
|
+
]);
|
|
124
|
+
const counts = {};
|
|
125
|
+
const cols = {};
|
|
126
|
+
let totalRows = 0;
|
|
127
|
+
for (const s of stats) {
|
|
128
|
+
counts[s.name] = s.count;
|
|
129
|
+
cols[s.name] = s.cols;
|
|
130
|
+
totalRows += s.count;
|
|
131
|
+
}
|
|
132
|
+
res.render('pages/home', {
|
|
133
|
+
title: 'Home',
|
|
134
|
+
tables: names,
|
|
135
|
+
counts,
|
|
136
|
+
cols,
|
|
137
|
+
totalRows,
|
|
138
|
+
savedQueries: saved.success ? (saved.data?.length ?? 0) : 0,
|
|
139
|
+
internalCount: internalNames.length,
|
|
140
|
+
dbPath: db.path,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
next(err);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
router.get('/tables/:table', async (req, res, next) => {
|
|
148
|
+
try {
|
|
149
|
+
const table = req.params.table;
|
|
150
|
+
const info = await db.getTableInfo(table);
|
|
151
|
+
if (!info.success || !info.data || info.data.columns.length === 0) {
|
|
152
|
+
return notFound(res, `Table "${table}" does not exist.`);
|
|
153
|
+
}
|
|
154
|
+
const page = Math.max(1, Number.parseInt(String(req.query.page ?? '1'), 10) || 1);
|
|
155
|
+
const pageSize = Math.max(1, Number.parseInt(String(req.query.size ?? '50'), 10) || 50);
|
|
156
|
+
const orderBy = req.query.orderBy ? String(req.query.orderBy) : undefined;
|
|
157
|
+
const orderDir = String(req.query.orderDir ?? 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc';
|
|
158
|
+
const filters = (0, common_1.parseFilters)(req.query.f);
|
|
159
|
+
const filterQS = (0, common_1.filtersToQS)(filters);
|
|
160
|
+
const [countR, rowsR, refsR] = await Promise.all([
|
|
161
|
+
db.getRowCount(table, filters),
|
|
162
|
+
db.getRows(table, { page, limit: pageSize, orderBy, orderDir, filters }),
|
|
163
|
+
db.getReferencingTables(table),
|
|
164
|
+
]);
|
|
165
|
+
const count = countR.success ? countR.data : 0;
|
|
166
|
+
const rawRows = (rowsR.data ?? []);
|
|
167
|
+
const pages = Math.max(1, Math.ceil(count / pageSize));
|
|
168
|
+
const basePath = String(res.locals.basePath ?? '');
|
|
169
|
+
const sizes = [25, 50, 100, 250];
|
|
170
|
+
const colHeaders = info.data.columns.map((c) => {
|
|
171
|
+
const active = c.name === orderBy;
|
|
172
|
+
const nextDir = active ? (orderDir === 'asc' ? 'desc' : 'asc') : 'asc';
|
|
173
|
+
const qs = `page=1&size=${pageSize}&orderBy=${encodeURIComponent(c.name)}&orderDir=${nextDir}`;
|
|
174
|
+
return {
|
|
175
|
+
name: c.name,
|
|
176
|
+
pk: c.pk,
|
|
177
|
+
type: c.type,
|
|
178
|
+
active,
|
|
179
|
+
dir: active ? orderDir : null,
|
|
180
|
+
href: `${basePath}/tables/${encodeURIComponent(table)}?${qs}${filterQS ? `&${filterQS}` : ''}`,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
const refColumns = (refsR.data ?? [])
|
|
184
|
+
.filter((r) => !r.table.startsWith('_'))
|
|
185
|
+
.map((rt) => ({ table: rt.table, from: rt.refs[0]?.from ?? '', to: rt.refs[0]?.to ?? '' }));
|
|
186
|
+
const countEntries = await Promise.all(refColumns.map(async (col) => {
|
|
187
|
+
const values = rawRows
|
|
188
|
+
.map((r) => r[col.to])
|
|
189
|
+
.filter((v) => v !== null && v !== undefined);
|
|
190
|
+
if (!values.length)
|
|
191
|
+
return [col.table, new Map()];
|
|
192
|
+
const placeholders = values.map(() => '?').join(', ');
|
|
193
|
+
const cR = await db.all(`SELECT ${(0, generator_1.quoteIdentifier)(col.from)} AS fk, COUNT(*) AS c FROM ${(0, generator_1.quoteIdentifier)(col.table)} WHERE ${(0, generator_1.quoteIdentifier)(col.from)} IN (${placeholders}) GROUP BY ${(0, generator_1.quoteIdentifier)(col.from)}`, values);
|
|
194
|
+
const map = new Map();
|
|
195
|
+
for (const row of (cR.data ?? []))
|
|
196
|
+
map.set(String(row.fk), Number(row.c));
|
|
197
|
+
return [col.table, map];
|
|
198
|
+
}));
|
|
199
|
+
const countMap = new Map(countEntries);
|
|
200
|
+
const rows = buildDisplayRows(rawRows, info.data, table, basePath).map((dr, i) => {
|
|
201
|
+
const raw = rawRows[i];
|
|
202
|
+
const refs = refColumns.map((col) => {
|
|
203
|
+
const v = raw[col.to];
|
|
204
|
+
return {
|
|
205
|
+
table: col.table,
|
|
206
|
+
from: col.from,
|
|
207
|
+
to: col.to,
|
|
208
|
+
value: v == null ? '' : String(v),
|
|
209
|
+
count: v == null ? 0 : (countMap.get(col.table)?.get(String(v)) ?? 0),
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
return { ...dr, refs };
|
|
213
|
+
});
|
|
214
|
+
res.locals.currentTable = table;
|
|
215
|
+
res.render('pages/table', {
|
|
216
|
+
title: table,
|
|
217
|
+
table,
|
|
218
|
+
info: info.data,
|
|
219
|
+
rows,
|
|
220
|
+
count,
|
|
221
|
+
page,
|
|
222
|
+
pageSize,
|
|
223
|
+
pages,
|
|
224
|
+
pkCols: info.data.primaryKey,
|
|
225
|
+
colNames: info.data.columns.map((c) => c.name),
|
|
226
|
+
colHeaders,
|
|
227
|
+
refColumns,
|
|
228
|
+
hasPk: true,
|
|
229
|
+
orderBy: orderBy ?? '',
|
|
230
|
+
orderDir,
|
|
231
|
+
sizes,
|
|
232
|
+
filters,
|
|
233
|
+
filterQS,
|
|
234
|
+
firstRow: count === 0 ? 0 : (page - 1) * pageSize + 1,
|
|
235
|
+
lastRow: Math.min(page * pageSize, count),
|
|
236
|
+
browseConfig: {
|
|
237
|
+
table,
|
|
238
|
+
filters,
|
|
239
|
+
pkCols: info.data.primaryKey.length ? info.data.primaryKey : ['_rowid_'],
|
|
240
|
+
hasPk: true,
|
|
241
|
+
readonly: db.isReadOnly,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
next(err);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
router.get('/tables/:table/schema', async (req, res, next) => {
|
|
250
|
+
try {
|
|
251
|
+
const table = req.params.table;
|
|
252
|
+
const [info, schema] = await Promise.all([db.getTableInfo(table), db.getSchema(table)]);
|
|
253
|
+
if (!info.success || !info.data || info.data.columns.length === 0) {
|
|
254
|
+
return notFound(res, `Table "${table}" does not exist.`);
|
|
255
|
+
}
|
|
256
|
+
if (!schema.success || !schema.data) {
|
|
257
|
+
return notFound(res, schema.error ?? 'Failed to load schema.');
|
|
258
|
+
}
|
|
259
|
+
res.locals.currentTable = table;
|
|
260
|
+
res.render('pages/schema', {
|
|
261
|
+
title: `Schema · ${table}`,
|
|
262
|
+
table,
|
|
263
|
+
schema: schema.data,
|
|
264
|
+
isInternal: table.startsWith('_'),
|
|
265
|
+
dialect: db.dialect,
|
|
266
|
+
isSqlite: db.dialect === 'sqlite',
|
|
267
|
+
isPostgres: db.dialect === 'postgres',
|
|
268
|
+
config: { table, isInternal: table.startsWith('_'), dialect: db.dialect },
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
next(err);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
router.get('/seed', async (req, res, next) => {
|
|
276
|
+
try {
|
|
277
|
+
const tableNames = res.locals.tables ?? [];
|
|
278
|
+
if (tableNames.length === 0) {
|
|
279
|
+
return res.render('pages/seed-select', {
|
|
280
|
+
title: 'Seed Data Generator',
|
|
281
|
+
tables: [],
|
|
282
|
+
hasTables: false,
|
|
283
|
+
dialect: db.dialect,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const tableStats = await Promise.all(tableNames.map(async (name) => {
|
|
287
|
+
const [info, countRes] = await Promise.all([db.getTableInfo(name), db.getRowCount(name)]);
|
|
288
|
+
const cols = info.success && info.data ? info.data.columns.length : 0;
|
|
289
|
+
const fks = info.success && info.data ? info.data.foreignKeys.length : 0;
|
|
290
|
+
const rowCount = countRes.success ? countRes.data : 0;
|
|
291
|
+
return {
|
|
292
|
+
name,
|
|
293
|
+
cols,
|
|
294
|
+
fks,
|
|
295
|
+
rowCount,
|
|
296
|
+
};
|
|
297
|
+
}));
|
|
298
|
+
const defaultTable = req.query.table ? String(req.query.table) : tableStats[0]?.name;
|
|
299
|
+
const defaultMode = req.query.mode === 'chain' ? 'chain' : 'single';
|
|
300
|
+
res.render('pages/seed-select', {
|
|
301
|
+
title: 'Seed Data Generator · Choose Table & Mode',
|
|
302
|
+
tables: tableStats,
|
|
303
|
+
hasTables: true,
|
|
304
|
+
defaultTable,
|
|
305
|
+
defaultMode,
|
|
306
|
+
dialect: db.dialect,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
next(err);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
router.get('/tables/:table/seed', (req, res) => {
|
|
314
|
+
const table = req.params.table;
|
|
315
|
+
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
|
|
316
|
+
const bp = res.locals.basePath || '';
|
|
317
|
+
return res.redirect(301, `${bp}/seed/${encodeURIComponent(table)}${qs}`);
|
|
318
|
+
});
|
|
319
|
+
router.get('/seed/:table', async (req, res, next) => {
|
|
320
|
+
try {
|
|
321
|
+
const table = req.params.table;
|
|
322
|
+
const allTables = res.locals.tables ?? [];
|
|
323
|
+
const initialMode = req.query.mode === 'chain' ? 'chain' : 'single';
|
|
324
|
+
const [info, countRes] = await Promise.all([db.getTableInfo(table), db.getRowCount(table)]);
|
|
325
|
+
if (!info.success || !info.data || info.data.columns.length === 0) {
|
|
326
|
+
return notFound(res, `Table "${table}" does not exist.`);
|
|
327
|
+
}
|
|
328
|
+
const rowCount = countRes.success ? countRes.data : 0;
|
|
329
|
+
const columns = (0, index_1.buildColumnConfigs)(info.data);
|
|
330
|
+
res.locals.currentTable = table;
|
|
331
|
+
res.render('pages/seed', {
|
|
332
|
+
title: `Seed data · ${table}`,
|
|
333
|
+
table,
|
|
334
|
+
allTables,
|
|
335
|
+
mode: initialMode,
|
|
336
|
+
isChainMode: initialMode === 'chain',
|
|
337
|
+
rowCount,
|
|
338
|
+
colCount: info.data.columns.length,
|
|
339
|
+
maxRows: index_1.MAX_SEED_ROWS,
|
|
340
|
+
quickCounts: [10, 50, 100, 500, 1000, index_1.MAX_SEED_ROWS],
|
|
341
|
+
seedConfig: { table, columns, rowCount, maxRows: index_1.MAX_SEED_ROWS, mode: initialMode },
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
next(err);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
router.get('/tables/:table/rows/new', async (req, res, next) => {
|
|
349
|
+
try {
|
|
350
|
+
const table = req.params.table;
|
|
351
|
+
const info = await db.getTableInfo(table);
|
|
352
|
+
if (!info.success || !info.data || info.data.columns.length === 0) {
|
|
353
|
+
return notFound(res, `Table "${table}" does not exist.`);
|
|
354
|
+
}
|
|
355
|
+
const pkColumns = info.data.primaryKey;
|
|
356
|
+
res.locals.currentTable = table;
|
|
357
|
+
res.render('pages/form', {
|
|
358
|
+
title: `New row · ${table}`,
|
|
359
|
+
table,
|
|
360
|
+
mode: 'insert',
|
|
361
|
+
pk: null,
|
|
362
|
+
pkColumns,
|
|
363
|
+
infoSummary: `${info.data.columns.length} column(s)` + (pkColumns.length ? ` · PK: ${pkColumns.join(', ')}` : ''),
|
|
364
|
+
config: { table, mode: 'insert', pk: null, pkColumns },
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
next(err);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
router.get('/tables/:table/rows/:id/edit', async (req, res, next) => {
|
|
372
|
+
try {
|
|
373
|
+
const table = req.params.table;
|
|
374
|
+
const info = await db.getTableInfo(table);
|
|
375
|
+
if (!info.success || !info.data || info.data.columns.length === 0) {
|
|
376
|
+
return notFound(res, `Table "${table}" does not exist.`);
|
|
377
|
+
}
|
|
378
|
+
const pkColumns = info.data.primaryKey.length ? info.data.primaryKey : ['_rowid_'];
|
|
379
|
+
const pk = (0, common_1.decodePk)(req.params.id);
|
|
380
|
+
if (pk.length !== pkColumns.length) {
|
|
381
|
+
return notFound(res, 'Invalid row identifier.');
|
|
382
|
+
}
|
|
383
|
+
res.locals.currentTable = table;
|
|
384
|
+
res.render('pages/form', {
|
|
385
|
+
title: `Edit row · ${table}`,
|
|
386
|
+
table,
|
|
387
|
+
mode: 'edit',
|
|
388
|
+
pk: req.params.id,
|
|
389
|
+
pkColumns,
|
|
390
|
+
infoSummary: `${info.data.columns.length} column(s)` + (info.data.primaryKey.length ? ` · PK: ${info.data.primaryKey.join(', ')}` : ''),
|
|
391
|
+
config: { table, mode: 'edit', pk: req.params.id, pkColumns },
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
next(err);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
router.get('/erd', (_req, res) => {
|
|
399
|
+
res.render('pages/erd', { title: 'ER Diagram' });
|
|
400
|
+
});
|
|
401
|
+
router.get('/designer', (_req, res) => {
|
|
402
|
+
res.render('pages/designer', {
|
|
403
|
+
title: 'New table',
|
|
404
|
+
dialect: db.dialect,
|
|
405
|
+
isSqlite: db.dialect === 'sqlite',
|
|
406
|
+
isPostgres: db.dialect === 'postgres',
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
router.get('/query', async (_req, res, next) => {
|
|
410
|
+
try {
|
|
411
|
+
const queries = await db.listSavedQueries();
|
|
412
|
+
res.render('pages/query', { title: 'Query editor', savedQueries: queries.success ? queries.data : [] });
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
next(err);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
router.get('/export', async (_req, res, next) => {
|
|
419
|
+
try {
|
|
420
|
+
const dump = await (0, export_1.generateSqlDump)(db);
|
|
421
|
+
if (!dump.success || !dump.data)
|
|
422
|
+
throw new Error(dump.error ?? 'Failed to generate dump.');
|
|
423
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
424
|
+
res.setHeader('Content-Type', 'application/sql; charset=utf-8');
|
|
425
|
+
res.setHeader('Content-Disposition', `attachment; filename="admindb-${stamp}.sql"`);
|
|
426
|
+
res.send(dump.data);
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
next(err);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
router.get('/info', async (_req, res, next) => {
|
|
433
|
+
try {
|
|
434
|
+
const settingsRes = await db.getSettings();
|
|
435
|
+
res.render('pages/info', {
|
|
436
|
+
title: 'Database Info',
|
|
437
|
+
settings: settingsRes.success ? settingsRes.data : {},
|
|
438
|
+
dialect: db.dialect,
|
|
439
|
+
dbPath: db.path,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
next(err);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
package/dist/serverless.js
CHANGED
|
@@ -1 +1,149 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isServerlessEnvironment = isServerlessEnvironment;
|
|
4
|
+
exports.createServerlessHandler = createServerlessHandler;
|
|
5
|
+
exports.createLambdaHandler = createLambdaHandler;
|
|
6
|
+
const node_http_1 = require("node:http");
|
|
7
|
+
const node_net_1 = require("node:net");
|
|
8
|
+
const app_1 = require("./app");
|
|
9
|
+
const common_1 = require("./utils/common");
|
|
10
|
+
function isServerlessEnvironment(env = process.env) {
|
|
11
|
+
if (['1', 'true', 'yes', 'on'].includes(String(env.ADMINDB_SERVERLESS ?? env.SERVERLESS ?? env.IS_SERVERLESS ?? '').trim().toLowerCase())) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return Boolean(env.VERCEL || env.VERCEL_ENV || env.NOW_REGION ||
|
|
15
|
+
env.AWS_LAMBDA_FUNCTION_NAME || env.LAMBDA_TASK_ROOT || env.AWS_EXECUTION_ENV ||
|
|
16
|
+
env.NETLIFY || env.NETLIFY_LOCAL ||
|
|
17
|
+
env.FUNCTION_TARGET || env.FUNCTION_NAME || env.K_SERVICE ||
|
|
18
|
+
env.FUNCTIONS_WORKER_RUNTIME || env.WEBSITE_INSTANCE_ID ||
|
|
19
|
+
env.CF_PAGES);
|
|
20
|
+
}
|
|
21
|
+
function createServerlessHandler(options = {}) {
|
|
22
|
+
const rawTarget = options.connection || options.dbPath || '';
|
|
23
|
+
const isPg = Boolean(options.pgOptions) ||
|
|
24
|
+
(0, common_1.isPostgresConnectionString)(rawTarget) ||
|
|
25
|
+
options.db?.dialect === 'postgres';
|
|
26
|
+
const defaultReadonly = !isPg;
|
|
27
|
+
const app = (0, app_1.createRouter)({
|
|
28
|
+
...options,
|
|
29
|
+
serverless: options.serverless ?? true,
|
|
30
|
+
readonly: options.readonly ?? defaultReadonly,
|
|
31
|
+
});
|
|
32
|
+
return (req, res) => {
|
|
33
|
+
app(req, res);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createLambdaHandler(options = {}) {
|
|
37
|
+
const rawTarget = options.connection || options.dbPath || '';
|
|
38
|
+
const isPg = Boolean(options.pgOptions) ||
|
|
39
|
+
(0, common_1.isPostgresConnectionString)(rawTarget) ||
|
|
40
|
+
options.db?.dialect === 'postgres';
|
|
41
|
+
const defaultReadonly = !isPg;
|
|
42
|
+
const app = (0, app_1.createRouter)({
|
|
43
|
+
...options,
|
|
44
|
+
serverless: options.serverless ?? true,
|
|
45
|
+
readonly: options.readonly ?? defaultReadonly,
|
|
46
|
+
});
|
|
47
|
+
return async (event, _context) => {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
try {
|
|
50
|
+
const isV2 = Boolean(event.version === '2.0' || event.rawPath);
|
|
51
|
+
const method = (isV2 ? event.requestContext?.http?.method : event.httpMethod) || 'GET';
|
|
52
|
+
const rawPath = isV2 ? (event.rawPath || '/') : (event.path || '/');
|
|
53
|
+
const qs = isV2
|
|
54
|
+
? (event.rawQueryString || '')
|
|
55
|
+
: (event.queryStringParameters
|
|
56
|
+
? Object.entries(event.queryStringParameters)
|
|
57
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
58
|
+
.join('&')
|
|
59
|
+
: '');
|
|
60
|
+
const url = qs ? `${rawPath}?${qs}` : rawPath;
|
|
61
|
+
const rawHeaders = event.headers || {};
|
|
62
|
+
const headers = {};
|
|
63
|
+
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
64
|
+
if (v !== undefined && v !== null)
|
|
65
|
+
headers[k.toLowerCase()] = String(v);
|
|
66
|
+
}
|
|
67
|
+
if (event.cookies && Array.isArray(event.cookies) && !headers['cookie']) {
|
|
68
|
+
headers['cookie'] = event.cookies.join('; ');
|
|
69
|
+
}
|
|
70
|
+
let bodyBuffer = null;
|
|
71
|
+
if (event.body) {
|
|
72
|
+
bodyBuffer = event.isBase64Encoded ? Buffer.from(event.body, 'base64') : Buffer.from(event.body, 'utf8');
|
|
73
|
+
if (!headers['content-length'])
|
|
74
|
+
headers['content-length'] = String(bodyBuffer.length);
|
|
75
|
+
}
|
|
76
|
+
const socket = new node_net_1.Socket();
|
|
77
|
+
const req = new node_http_1.IncomingMessage(socket);
|
|
78
|
+
req.url = url;
|
|
79
|
+
req.method = method;
|
|
80
|
+
req.headers = headers;
|
|
81
|
+
req.httpVersion = '1.1';
|
|
82
|
+
req.httpVersionMajor = 1;
|
|
83
|
+
req.httpVersionMinor = 1;
|
|
84
|
+
if (bodyBuffer)
|
|
85
|
+
req.push(bodyBuffer);
|
|
86
|
+
req.push(null);
|
|
87
|
+
const res = new node_http_1.ServerResponse(req);
|
|
88
|
+
const chunks = [];
|
|
89
|
+
res.write = function (chunk, ...args) {
|
|
90
|
+
if (chunk)
|
|
91
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
92
|
+
const cb = typeof args[0] === 'function' ? args[0] : typeof args[1] === 'function' ? args[1] : undefined;
|
|
93
|
+
if (cb)
|
|
94
|
+
cb();
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
97
|
+
res.end = function (chunk, ...args) {
|
|
98
|
+
if (chunk && typeof chunk !== 'function') {
|
|
99
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
100
|
+
}
|
|
101
|
+
const cb = typeof chunk === 'function'
|
|
102
|
+
? chunk
|
|
103
|
+
: typeof args[0] === 'function'
|
|
104
|
+
? args[0]
|
|
105
|
+
: typeof args[1] === 'function'
|
|
106
|
+
? args[1]
|
|
107
|
+
: undefined;
|
|
108
|
+
if (cb)
|
|
109
|
+
cb();
|
|
110
|
+
const responseHeaders = {};
|
|
111
|
+
const multiValueHeaders = {};
|
|
112
|
+
for (const [k, v] of Object.entries(res.getHeaders())) {
|
|
113
|
+
if (Array.isArray(v)) {
|
|
114
|
+
multiValueHeaders[k] = v.map(String);
|
|
115
|
+
responseHeaders[k] = v.map(String).join(', ');
|
|
116
|
+
}
|
|
117
|
+
else if (v !== undefined) {
|
|
118
|
+
responseHeaders[k] = String(v);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const fullBody = Buffer.concat(chunks);
|
|
122
|
+
const contentType = String(responseHeaders['content-type'] || '');
|
|
123
|
+
const isBinary = !contentType.startsWith('text/') &&
|
|
124
|
+
!contentType.includes('application/json') &&
|
|
125
|
+
!contentType.includes('application/javascript') &&
|
|
126
|
+
!contentType.includes('application/xml') &&
|
|
127
|
+
!contentType.includes('image/svg+xml');
|
|
128
|
+
resolve({
|
|
129
|
+
statusCode: res.statusCode || 200,
|
|
130
|
+
headers: responseHeaders,
|
|
131
|
+
multiValueHeaders: Object.keys(multiValueHeaders).length > 0 ? multiValueHeaders : undefined,
|
|
132
|
+
body: isBinary ? fullBody.toString('base64') : fullBody.toString('utf8'),
|
|
133
|
+
isBase64Encoded: isBinary,
|
|
134
|
+
});
|
|
135
|
+
return res;
|
|
136
|
+
};
|
|
137
|
+
app(req, res);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
resolve({
|
|
141
|
+
statusCode: 500,
|
|
142
|
+
headers: { 'content-type': 'application/json' },
|
|
143
|
+
body: JSON.stringify({ success: false, error: err?.message || 'Server error' }),
|
|
144
|
+
isBase64Encoded: false,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
}
|
package/dist/sql/classifier.js
CHANGED
|
@@ -1 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.firstKeyword = firstKeyword;
|
|
4
|
+
exports.classifySql = classifySql;
|
|
5
|
+
const LEADING_NOISE = /^\s*((\/\*[\s\S]*?\*\/)|(--[^\n]*\n))*\s*/;
|
|
6
|
+
function firstKeyword(sql) {
|
|
7
|
+
const cleaned = String(sql).replace(LEADING_NOISE, '').trim();
|
|
8
|
+
const m = cleaned.match(/^([a-zA-Z]+)/);
|
|
9
|
+
return m ? m[1].toUpperCase() : '';
|
|
10
|
+
}
|
|
11
|
+
function classifySql(sql) {
|
|
12
|
+
const keyword = firstKeyword(sql);
|
|
13
|
+
if (keyword === 'SELECT' || keyword === 'WITH') {
|
|
14
|
+
const body = String(sql).replace(LEADING_NOISE, '').trim();
|
|
15
|
+
const isCount = /^select\b[\s\S]*?\bcount\s*\(/i.test(body) &&
|
|
16
|
+
!/\bgroup\s+by\b/i.test(body) &&
|
|
17
|
+
!/\bunion\b/i.test(body);
|
|
18
|
+
return { kind: isCount ? 'count' : 'select', keyword };
|
|
19
|
+
}
|
|
20
|
+
if (['INSERT', 'UPDATE', 'DELETE', 'REPLACE', 'CREATE', 'DROP', 'ALTER'].includes(keyword)) {
|
|
21
|
+
return { kind: 'write', keyword };
|
|
22
|
+
}
|
|
23
|
+
return { kind: 'read', keyword: keyword || 'UNKNOWN' };
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type {IDatabase} from '../db/types';export interface SqlErrorDetails{message:string;line?:number;column?:number;token?:string;snippet?:string;cause?:string;suggestion?:string;raw?:string;}export declare function buildErrorSnippet(sql:string,line:number,column:number):string;export declare function offsetToLineCol(sql:string,offset:number):{line:number;column:number;};export declare function maskCommentsAndStrings(sql:string):string;export declare function findTokenPosition(sql:string,token:string):{line:number;column:number;}|null;export declare function analyzeSqlError(sql:string,rawError:unknown,db?:IDatabase):Promise<SqlErrorDetails>;
|