admindb 2.3.0 → 2.4.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 +17 -17
- package/dist/app.js +123 -163
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +5 -3
- package/dist/cli/config.d.ts +1 -1
- package/dist/cli/config.js +9 -2
- package/dist/cli/runner.js +9 -6
- package/dist/db/manager.d.ts +1 -1
- package/dist/db/manager.js +21 -2
- package/dist/public/css/app.css +1 -1
- package/dist/public/js/browse.js +1 -1
- 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 -1
- 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.js +2 -2
- package/dist/routes/api/helpers.d.ts +1 -1
- package/dist/routes/api/import-export.js +8 -6
- package/dist/routes/api/index.js +16 -9
- package/dist/routes/api/query.js +8 -5
- package/dist/routes/api/rows.js +36 -21
- package/dist/routes/api/seed.js +29 -16
- package/dist/routes/api/tables.js +32 -18
- package/dist/routes/databases.js +2 -0
- package/dist/routes/pages.d.ts +1 -1
- package/dist/routes/pages.js +137 -43
- package/dist/serverless.js +16 -4
- package/dist/views/layouts/main.hbs +1 -1
- package/dist/views/pages/databases.hbs +1 -1
- package/dist/views/pages/erd.hbs +1 -1
- package/dist/views/pages/form.hbs +1 -1
- package/dist/views/pages/home.hbs +1 -1
- package/dist/views/pages/info.hbs +1 -1
- package/dist/views/pages/schema.hbs +1 -1
- package/dist/views/pages/seed-select.hbs +6 -5
- package/dist/views/pages/seed.hbs +3 -3
- package/dist/views/pages/table.hbs +1 -1
- package/dist/views/partials/navbar.hbs +1 -1
- package/dist/views/partials/sidebar.hbs +1 -1
- package/docs/API.md +60 -55
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
6
|
[](https://www.npmjs.com/package/admindb)
|
|
7
|
-
[](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/checkLatestVersion.yml)
|
|
8
8
|
|
|
9
9
|
**A modern, browser-based SQLite and PostgreSQL database administration tool.** Manage SQLite and PostgreSQL databases entirely from your browser — browse and edit rows, run arbitrary SQL queries, design schemas visually, seed realistic test data, inspect complex data types, and import/export CSV/JSON — with zero frontend build step.
|
|
10
10
|
|
|
@@ -309,22 +309,22 @@ ADMINDB_AUTH=false npx admindb
|
|
|
309
309
|
AdminDB exposes a comprehensive REST API under `basePath` returning `{ success, data?, error? }`:
|
|
310
310
|
|
|
311
311
|
```text
|
|
312
|
-
GET /api/tables
|
|
313
|
-
GET /api/tables/:table/rows
|
|
314
|
-
GET /api/tables/:table/row/:id
|
|
315
|
-
GET /api/tables/:table/row/:id/blob/:column
|
|
316
|
-
GET /api/tables/:table/row/:id/blob/:col/meta
|
|
317
|
-
PUT /api/tables/:table/row/:id/blob/:column
|
|
318
|
-
POST /api/tables/:table/rows
|
|
319
|
-
PUT /api/tables/:table/row/:id
|
|
320
|
-
DELETE /api/tables/:table/row/:id
|
|
321
|
-
POST /api/tables/:table/rows/bulk-update
|
|
322
|
-
POST /api/tables/:table/rows/bulk-delete
|
|
323
|
-
POST /api/tables/:table/seed
|
|
324
|
-
POST /api/tables
|
|
325
|
-
GET /api/tables/:table/schema
|
|
326
|
-
GET /api/tables/:table/ddl
|
|
327
|
-
POST /api/query
|
|
312
|
+
GET /api/tables/:db List tables
|
|
313
|
+
GET /api/tables/:db/:table/rows Paginated rows (with filtering & sorting)
|
|
314
|
+
GET /api/tables/:db/:table/row/:id Get a single row
|
|
315
|
+
GET /api/tables/:db/:table/row/:id/blob/:column Stream raw BLOB / BYTEA binary data
|
|
316
|
+
GET /api/tables/:db/:table/row/:id/blob/:col/meta BLOB / BYTEA metadata, MIME analysis & hex dump
|
|
317
|
+
PUT /api/tables/:db/:table/row/:id/blob/:column Upload / update binary content
|
|
318
|
+
POST /api/tables/:db/:table/rows Insert row (single or batch)
|
|
319
|
+
PUT /api/tables/:db/:table/row/:id Update row
|
|
320
|
+
DELETE /api/tables/:db/:table/row/:id Delete row
|
|
321
|
+
POST /api/tables/:db/:table/rows/bulk-update Apply staged inline edits atomically
|
|
322
|
+
POST /api/tables/:db/:table/rows/bulk-delete Delete selected rows atomically
|
|
323
|
+
POST /api/tables/:db/:table/seed Generate & insert realistic seed rows
|
|
324
|
+
POST /api/tables/:db Create a new table
|
|
325
|
+
GET /api/tables/:db/:table/schema Inspect table schema & constraints
|
|
326
|
+
GET /api/tables/:db/:table/ddl Get table CREATE SQL & indexes
|
|
327
|
+
POST /api/query/:db Execute arbitrary SQL
|
|
328
328
|
GET /api/databases List managed database connections & files
|
|
329
329
|
```
|
|
330
330
|
|
package/dist/app.js
CHANGED
|
@@ -20,7 +20,120 @@ const serverless_1 = require("./serverless");
|
|
|
20
20
|
const auth_1 = require("./auth");
|
|
21
21
|
const APP_VERSION = (0, args_1.getPackageVersion)();
|
|
22
22
|
function createRouter(options = {}) {
|
|
23
|
-
|
|
23
|
+
const isServerless = options.serverless !== undefined ? options.serverless : (0, serverless_1.isServerlessEnvironment)();
|
|
24
|
+
const basePath = (options.basePath ?? '').replace(/\/+$/, '');
|
|
25
|
+
const logger = options.logger ?? (0, logger_1.createLogger)(options.logLevel ?? 'info', 'admindb');
|
|
26
|
+
const authConfig = (0, auth_1.resolveAuthConfig)(options.auth);
|
|
27
|
+
const envTarget = process.env.ADMINDB_CONNECTION ||
|
|
28
|
+
process.env.DATABASE_URL ||
|
|
29
|
+
process.env.POSTGRES_URL ||
|
|
30
|
+
process.env.PG_CONNECTION ||
|
|
31
|
+
'';
|
|
32
|
+
const rawTarget = options.connection || options.dbPath || ((0, common_1.isPostgresConnectionString)(envTarget) ? envTarget : '');
|
|
33
|
+
const isPg = Boolean(options.pgOptions) ||
|
|
34
|
+
(0, common_1.isPostgresConnectionString)(rawTarget) ||
|
|
35
|
+
options.db?.dialect === 'postgres';
|
|
36
|
+
const explicitReadonly = options.readonly !== undefined ? Boolean(options.readonly) : undefined;
|
|
37
|
+
const globalReadonly = explicitReadonly !== undefined
|
|
38
|
+
? explicitReadonly
|
|
39
|
+
: options.manager
|
|
40
|
+
? options.manager.isReadOnly
|
|
41
|
+
: isServerless
|
|
42
|
+
? !isPg
|
|
43
|
+
: false;
|
|
44
|
+
const manager = options.manager;
|
|
45
|
+
let singleDb;
|
|
46
|
+
let defaultDbId = options.dbId;
|
|
47
|
+
if (!manager) {
|
|
48
|
+
if (options.db) {
|
|
49
|
+
singleDb = options.db;
|
|
50
|
+
if (options.readonly !== undefined && singleDb.isReadOnly !== Boolean(options.readonly)) {
|
|
51
|
+
try {
|
|
52
|
+
Object.defineProperty(singleDb, 'isReadOnly', { value: Boolean(options.readonly), configurable: true });
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (isPg) {
|
|
58
|
+
singleDb = new postgres_1.PostgresDatabase(options.pgOptions || rawTarget, logger, { readonly: globalReadonly });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
singleDb = new database_1.SqliteDatabase(options.dbPath ?? 'admindb.db', logger, { readonly: globalReadonly });
|
|
62
|
+
}
|
|
63
|
+
if (!defaultDbId) {
|
|
64
|
+
defaultDbId = singleDb.dialect === 'postgres' ? 'PostgreSQL' : (node_path_1.default.basename(singleDb.path) || 'SQLite');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const app = (0, express_1.default)();
|
|
68
|
+
app.disable('x-powered-by');
|
|
69
|
+
app.use(express_1.default.static(node_path_1.default.join(__dirname, 'public')));
|
|
70
|
+
setupViewEngine(app, logger);
|
|
71
|
+
app.use(express_1.default.json({ limit: '10mb' }));
|
|
72
|
+
app.use(express_1.default.urlencoded({ extended: false }));
|
|
73
|
+
(0, auth_1.registerAuthRoutes)(app, authConfig, basePath, logger);
|
|
74
|
+
app.use((0, auth_1.createAuthMiddleware)(authConfig, basePath, logger));
|
|
75
|
+
app.use(async (req, res, next) => {
|
|
76
|
+
try {
|
|
77
|
+
res.locals.basePath = basePath;
|
|
78
|
+
res.locals.currentPath = req.path;
|
|
79
|
+
res.locals.currentTable = null;
|
|
80
|
+
res.locals.dbPath = manager ? manager.directory : singleDb?.path;
|
|
81
|
+
res.locals.dbId = defaultDbId ?? null;
|
|
82
|
+
res.locals.databasesUrl = manager ? `${basePath}/` : (options.databasesUrl ?? null);
|
|
83
|
+
res.locals.databasesMode = false;
|
|
84
|
+
res.locals.readonly = globalReadonly;
|
|
85
|
+
res.locals.serverless = isServerless;
|
|
86
|
+
res.locals.version = APP_VERSION;
|
|
87
|
+
res.locals.authEnabled = authConfig.enabled;
|
|
88
|
+
res.locals.isDefaultPassword = authConfig.isDefaultPassword;
|
|
89
|
+
res.locals.icons = icons_1.ICONS;
|
|
90
|
+
res.locals.tables = [];
|
|
91
|
+
res.locals.internalTables = [];
|
|
92
|
+
next();
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
next(err);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
const getDb = (req) => {
|
|
99
|
+
if (manager) {
|
|
100
|
+
const dbId = String(req.params.db || req.params.dbId || req.params.id || '');
|
|
101
|
+
if (!manager.has(dbId)) {
|
|
102
|
+
const err = new Error(`Database "${dbId}" does not exist.`);
|
|
103
|
+
err.status = 404;
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
const rawRo = req.query.readonly;
|
|
107
|
+
if (rawRo !== undefined) {
|
|
108
|
+
const explicitRo = rawRo === '1' || rawRo === 'true';
|
|
109
|
+
if (!globalReadonly || explicitRo) {
|
|
110
|
+
manager.setReadonly(dbId, explicitRo);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const effectiveRo = globalReadonly ? true : manager.isDbReadOnly(dbId);
|
|
114
|
+
return manager.open(dbId, effectiveRo);
|
|
115
|
+
}
|
|
116
|
+
return singleDb;
|
|
117
|
+
};
|
|
118
|
+
if (manager) {
|
|
119
|
+
(0, databases_1.registerDatabasesRoutes)(app, {
|
|
120
|
+
manager,
|
|
121
|
+
logger,
|
|
122
|
+
basePath,
|
|
123
|
+
readonly: globalReadonly,
|
|
124
|
+
allowBrowse: isServerless ? false : options.allowBrowse,
|
|
125
|
+
browseRoot: options.browseRoot,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
app.get('/', (_req, res) => {
|
|
130
|
+
res.redirect(302, `${basePath}/home/${encodeURIComponent(defaultDbId)}`);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
(0, pages_1.registerPages)(app, { getDb, logger });
|
|
134
|
+
(0, index_1.registerApi)(app, { getDb, logger });
|
|
135
|
+
addErrorHandlers(app, logger);
|
|
136
|
+
return app;
|
|
24
137
|
}
|
|
25
138
|
function setupViewEngine(app, _logger) {
|
|
26
139
|
app.engine('hbs', (0, express_handlebars_1.engine)({
|
|
@@ -42,6 +155,9 @@ function setupViewEngine(app, _logger) {
|
|
|
42
155
|
gt: (a, b) => Number(a) > Number(b),
|
|
43
156
|
lt: (a, b) => Number(a) < Number(b),
|
|
44
157
|
join: (arr, sep) => (Array.isArray(arr) ? arr.join(String(sep ?? ',')) : String(arr ?? '')),
|
|
158
|
+
encodeUri: (str) => encodeURIComponent(String(str ?? '')),
|
|
159
|
+
or: (a, b) => a || b,
|
|
160
|
+
tableName: (t) => (typeof t === 'object' && t !== null && 'name' in t ? String(t.name ?? '') : String(t ?? '')),
|
|
45
161
|
icon: (name, options) => {
|
|
46
162
|
const customClass = (options && options.hash && options.hash.class) || undefined;
|
|
47
163
|
return (0, icons_1.renderIcon)(String(name ?? ''), customClass);
|
|
@@ -59,169 +175,13 @@ function addErrorHandlers(app, logger) {
|
|
|
59
175
|
res.status(404).render('pages/error', { title: 'Not found', status: 404, error: 'The page you requested does not exist.' });
|
|
60
176
|
});
|
|
61
177
|
app.use((err, req, res, _next) => {
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
178
|
+
const status = typeof err?.status === 'number' ? err.status : 500;
|
|
179
|
+
if (status >= 500) {
|
|
180
|
+
logger.error('Unhandled error', err);
|
|
65
181
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
function createSingleDbApp(options) {
|
|
70
|
-
const isServerless = options.serverless !== undefined ? options.serverless : (0, serverless_1.isServerlessEnvironment)();
|
|
71
|
-
const rawTarget = options.connection || options.dbPath || '';
|
|
72
|
-
const isPg = Boolean(options.pgOptions) ||
|
|
73
|
-
(0, common_1.isPostgresConnectionString)(rawTarget) ||
|
|
74
|
-
options.db?.dialect === 'postgres';
|
|
75
|
-
const readonly = options.readonly !== undefined
|
|
76
|
-
? Boolean(options.readonly)
|
|
77
|
-
: (isServerless && !isPg);
|
|
78
|
-
const basePath = (options.basePath ?? '').replace(/\/+$/, '');
|
|
79
|
-
const logger = options.logger ?? (0, logger_1.createLogger)(options.logLevel ?? 'info', 'admindb');
|
|
80
|
-
let db;
|
|
81
|
-
if (options.db) {
|
|
82
|
-
db = options.db;
|
|
83
|
-
}
|
|
84
|
-
else if (isPg) {
|
|
85
|
-
db = new postgres_1.PostgresDatabase(options.pgOptions || rawTarget, logger, { readonly });
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
db = new database_1.SqliteDatabase(options.dbPath ?? 'admindb.db', logger, { readonly });
|
|
89
|
-
}
|
|
90
|
-
const databasesUrl = options.databasesUrl;
|
|
91
|
-
const authConfig = (0, auth_1.resolveAuthConfig)(options.auth);
|
|
92
|
-
const router = (0, express_1.default)();
|
|
93
|
-
router.disable('x-powered-by');
|
|
94
|
-
router.use(express_1.default.static(node_path_1.default.join(__dirname, 'public')));
|
|
95
|
-
setupViewEngine(router, logger);
|
|
96
|
-
router.use(express_1.default.json({ limit: '10mb' }));
|
|
97
|
-
router.use(express_1.default.urlencoded({ extended: false }));
|
|
98
|
-
(0, auth_1.registerAuthRoutes)(router, authConfig, basePath, logger);
|
|
99
|
-
router.use((0, auth_1.createAuthMiddleware)(authConfig, basePath, logger));
|
|
100
|
-
router.use(async (req, res, next) => {
|
|
101
|
-
try {
|
|
102
|
-
res.locals.basePath = basePath;
|
|
103
|
-
res.locals.currentPath = req.path;
|
|
104
|
-
res.locals.currentTable = null;
|
|
105
|
-
res.locals.dbPath = db.path;
|
|
106
|
-
res.locals.dbId = options.dbId ?? (db.dialect === 'postgres' ? 'PostgreSQL' : (node_path_1.default.basename(db.path) || 'SQLite'));
|
|
107
|
-
res.locals.dialect = db.dialect;
|
|
108
|
-
res.locals.isPostgres = db.dialect === 'postgres';
|
|
109
|
-
res.locals.isSqlite = db.dialect === 'sqlite';
|
|
110
|
-
res.locals.dialectName = db.dialect === 'postgres' ? 'PostgreSQL' : 'SQLite';
|
|
111
|
-
res.locals.databasesUrl = databasesUrl ?? null;
|
|
112
|
-
res.locals.databasesMode = false;
|
|
113
|
-
res.locals.readonly = db.isReadOnly;
|
|
114
|
-
res.locals.serverless = isServerless;
|
|
115
|
-
res.locals.version = APP_VERSION;
|
|
116
|
-
res.locals.authEnabled = authConfig.enabled;
|
|
117
|
-
res.locals.isDefaultPassword = authConfig.isDefaultPassword;
|
|
118
|
-
res.locals.icons = icons_1.ICONS;
|
|
119
|
-
if (!req.path.startsWith('/api/')) {
|
|
120
|
-
const all = await db.listTables();
|
|
121
|
-
const names = (all.data ?? []).map((t) => t.name);
|
|
122
|
-
res.locals.tables = names.filter((n) => !n.startsWith('_'));
|
|
123
|
-
res.locals.internalTables = names.filter((n) => n.startsWith('_'));
|
|
124
|
-
}
|
|
125
|
-
next();
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
next(err);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
(0, pages_1.registerPages)(router, { db, logger });
|
|
132
|
-
(0, index_1.registerApi)(router, { db, logger });
|
|
133
|
-
addErrorHandlers(router, logger);
|
|
134
|
-
return router;
|
|
135
|
-
}
|
|
136
|
-
function createManagerApp(options) {
|
|
137
|
-
const isServerless = options.serverless !== undefined ? options.serverless : (0, serverless_1.isServerlessEnvironment)();
|
|
138
|
-
const readonly = isServerless || Boolean(options.readonly);
|
|
139
|
-
const manager = options.manager;
|
|
140
|
-
const basePath = (options.basePath ?? '').replace(/\/+$/, '');
|
|
141
|
-
const logger = options.logger ?? (0, logger_1.createLogger)(options.logLevel ?? 'info', 'admindb');
|
|
142
|
-
const authConfig = (0, auth_1.resolveAuthConfig)(options.auth);
|
|
143
|
-
const app = (0, express_1.default)();
|
|
144
|
-
app.disable('x-powered-by');
|
|
145
|
-
app.use(express_1.default.static(node_path_1.default.join(__dirname, 'public')));
|
|
146
|
-
setupViewEngine(app, logger);
|
|
147
|
-
app.use(express_1.default.json({ limit: '10mb' }));
|
|
148
|
-
app.use(express_1.default.urlencoded({ extended: false }));
|
|
149
|
-
(0, auth_1.registerAuthRoutes)(app, authConfig, basePath, logger);
|
|
150
|
-
app.use((0, auth_1.createAuthMiddleware)(authConfig, basePath, logger));
|
|
151
|
-
app.use(async (req, res, next) => {
|
|
152
|
-
try {
|
|
153
|
-
res.locals.basePath = basePath;
|
|
154
|
-
res.locals.currentPath = req.path;
|
|
155
|
-
res.locals.currentTable = null;
|
|
156
|
-
res.locals.dbPath = manager.directory;
|
|
157
|
-
res.locals.dbId = null;
|
|
158
|
-
res.locals.databasesUrl = null;
|
|
159
|
-
res.locals.databasesMode = true;
|
|
160
|
-
res.locals.readonly = readonly;
|
|
161
|
-
res.locals.serverless = isServerless;
|
|
162
|
-
res.locals.version = APP_VERSION;
|
|
163
|
-
res.locals.authEnabled = authConfig.enabled;
|
|
164
|
-
res.locals.isDefaultPassword = authConfig.isDefaultPassword;
|
|
165
|
-
res.locals.icons = icons_1.ICONS;
|
|
166
|
-
if (!req.path.startsWith('/api/')) {
|
|
167
|
-
res.locals.tables = [];
|
|
168
|
-
res.locals.internalTables = [];
|
|
169
|
-
}
|
|
170
|
-
next();
|
|
171
|
-
}
|
|
172
|
-
catch (err) {
|
|
173
|
-
next(err);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
const subApps = new Map();
|
|
177
|
-
(0, databases_1.registerDatabasesRoutes)(app, {
|
|
178
|
-
manager,
|
|
179
|
-
logger,
|
|
180
|
-
basePath,
|
|
181
|
-
readonly,
|
|
182
|
-
allowBrowse: isServerless ? false : options.allowBrowse,
|
|
183
|
-
browseRoot: options.browseRoot,
|
|
184
|
-
invalidate: (id) => subApps.delete(id),
|
|
185
|
-
});
|
|
186
|
-
app.use('/:dbId', (req, res, next) => {
|
|
187
|
-
const dbId = String(req.params.dbId);
|
|
188
|
-
if (!manager.has(dbId)) {
|
|
189
|
-
if (req.path.startsWith('/api/')) {
|
|
190
|
-
return res.status(404).json({ success: false, error: `Database "${dbId}" does not exist.` });
|
|
191
|
-
}
|
|
192
|
-
return res.status(404).render('pages/error', {
|
|
193
|
-
title: 'Not found',
|
|
194
|
-
status: 404,
|
|
195
|
-
error: `Database "${dbId}" does not exist.`,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
const rawRo = req.query.readonly;
|
|
199
|
-
if (rawRo !== undefined) {
|
|
200
|
-
const explicitRo = rawRo === '1' || rawRo === 'true';
|
|
201
|
-
if (!readonly || explicitRo) {
|
|
202
|
-
manager.setReadonly(dbId, explicitRo);
|
|
203
|
-
subApps.delete(dbId);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
let sub = subApps.get(dbId);
|
|
207
|
-
if (!sub) {
|
|
208
|
-
const subBase = `${basePath || ''}/${encodeURIComponent(dbId)}`;
|
|
209
|
-
const effectiveRo = readonly || manager.isDbReadOnly(dbId);
|
|
210
|
-
sub = createSingleDbApp({
|
|
211
|
-
db: manager.open(dbId, effectiveRo),
|
|
212
|
-
basePath: subBase,
|
|
213
|
-
logger,
|
|
214
|
-
logLevel: options.logLevel,
|
|
215
|
-
dbId,
|
|
216
|
-
databasesUrl: `${basePath || ''}/`,
|
|
217
|
-
readonly: effectiveRo,
|
|
218
|
-
serverless: isServerless,
|
|
219
|
-
auth: authConfig,
|
|
220
|
-
});
|
|
221
|
-
subApps.set(dbId, sub);
|
|
182
|
+
if (req.path.startsWith('/api/')) {
|
|
183
|
+
return res.status(status).json({ success: false, error: (0, common_1.errorMessage)(err) });
|
|
222
184
|
}
|
|
223
|
-
|
|
185
|
+
res.status(status).render('pages/error', { title: status === 404 ? 'Not found' : 'Error', status, error: (0, common_1.errorMessage)(err) });
|
|
224
186
|
});
|
|
225
|
-
addErrorHandlers(app, logger);
|
|
226
|
-
return app;
|
|
227
187
|
}
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import type {LogLevel} from '../utils/logger';export interface CliArgs{help:boolean;version:boolean;host?:string;port?:number;dbPath?:string;connection?:string;connections?:Record<string,string>;configPath?:string;dbDir?:string;dbFiles?:string[];basePath?:string;logLevel?:LogLevel;readonly :boolean;serverless?:boolean;auth?:boolean;authUsername?:string;authPassword?:string;authSecret?:string;}export declare function helpText():string;export declare const getPackageVersion:()=>string;export declare const versionText:()=>string;export declare function parseArgs(argv:string[]):{args:CliArgs;error?:string;};
|
|
1
|
+
import type {LogLevel} from '../utils/logger';export interface CliArgs{help:boolean;version:boolean;host?:string;port?:number;dbPath?:string;connection?:string;connections?:Record<string,string>;configPath?:string;dbDir?:string;dbFiles?:string[];basePath?:string;logLevel?:LogLevel;readonly :boolean;explicitReadonly?:boolean;serverless?:boolean;auth?:boolean;authUsername?:string;authPassword?:string;authSecret?:string;}export declare function helpText():string;export declare const getPackageVersion:()=>string;export declare const versionText:()=>string;export declare function parseArgs(argv:string[]):{args:CliArgs;error?:string;};
|
package/dist/cli/args.js
CHANGED
|
@@ -38,7 +38,7 @@ function helpText() {
|
|
|
38
38
|
' --files, --db-files <list> Comma-separated database file paths [DB_FILES / ADMINDB_DB_FILES]',
|
|
39
39
|
' -b, --base-path, --base <p> URL prefix to serve under (default: /) [BASE_PATH / ADMINDB_BASE_PATH]',
|
|
40
40
|
' -r, --readonly, --read-only Open databases read-only (writes disabled) [READONLY / ADMINDB_READONLY]',
|
|
41
|
-
' --serverless Enable serverless mode (
|
|
41
|
+
' --serverless Enable serverless mode (SQLite defaults to read-only; PostgreSQL is editable unless --readonly is specified) [SERVERLESS / ADMINDB_SERVERLESS]',
|
|
42
42
|
' --auth Enable authentication (default: on) [ADMINDB_AUTH=true]',
|
|
43
43
|
' --no-auth, --disable-auth Disable authentication completely [ADMINDB_NO_AUTH=1 / ADMINDB_AUTH=false]',
|
|
44
44
|
' -u, --username, --user <user> Admin username (default: admin) [ADMINDB_USERNAME / ADMINDB_USER]',
|
|
@@ -122,8 +122,10 @@ function parseArgs(argv) {
|
|
|
122
122
|
args.host = fileConf.host;
|
|
123
123
|
if (fileConf.basePath !== undefined)
|
|
124
124
|
args.basePath = fileConf.basePath;
|
|
125
|
-
if (fileConf.readonly !== undefined)
|
|
125
|
+
if (fileConf.readonly !== undefined) {
|
|
126
126
|
args.readonly = fileConf.readonly;
|
|
127
|
+
args.explicitReadonly = fileConf.readonly;
|
|
128
|
+
}
|
|
127
129
|
if (fileConf.serverless !== undefined)
|
|
128
130
|
args.serverless = fileConf.serverless;
|
|
129
131
|
if (fileConf.logLevel !== undefined)
|
|
@@ -204,10 +206,10 @@ function parseArgs(argv) {
|
|
|
204
206
|
}
|
|
205
207
|
else if (tok === '-r' || tok === '--readonly' || tok === '--read-only') {
|
|
206
208
|
args.readonly = true;
|
|
209
|
+
args.explicitReadonly = true;
|
|
207
210
|
}
|
|
208
211
|
else if (tok === '--serverless') {
|
|
209
212
|
args.serverless = true;
|
|
210
|
-
args.readonly = true;
|
|
211
213
|
}
|
|
212
214
|
else if (tok === '--no-auth' || tok === '--disable-auth') {
|
|
213
215
|
args.auth = false;
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import type {LogLevel} from '../utils/logger';export interface DatabaseConfigFile{connections?:Record<string,string>;connection?:string;port?:number;host?:string;basePath?:string;readonly ?:boolean;serverless?:boolean;auth?:boolean|{username?:string;password?:string;secret?:string;};logLevel?:LogLevel;}export declare function parseConfigFile(filePath:string):DatabaseConfigFile|null;export interface Config{host:string;port:number;dbPath:string;connection?:string;connections?:Record<string,string>;dbDir?:string;dbFiles?:string[];basePath:string;logLevel:LogLevel;readonly :boolean;serverless:boolean;auth:boolean;authUsername?:string;authPassword?:string;authSecret?:string;}export declare function loadConfig(env?:NodeJS.ProcessEnv):Config;
|
|
1
|
+
import type {LogLevel} from '../utils/logger';export interface DatabaseConfigFile{connections?:Record<string,string>;connection?:string;port?:number;host?:string;basePath?:string;readonly ?:boolean;serverless?:boolean;auth?:boolean|{username?:string;password?:string;secret?:string;};logLevel?:LogLevel;}export declare function parseConfigFile(filePath:string):DatabaseConfigFile|null;export interface Config{host:string;port:number;dbPath:string;connection?:string;connections?:Record<string,string>;dbDir?:string;dbFiles?:string[];basePath:string;logLevel:LogLevel;readonly :boolean;explicitReadonly?:boolean;serverless:boolean;auth:boolean;authUsername?:string;authPassword?:string;authSecret?:string;}export declare function loadConfig(env?:NodeJS.ProcessEnv):Config;
|
package/dist/cli/config.js
CHANGED
|
@@ -79,8 +79,14 @@ function loadConfig(env = process.env) {
|
|
|
79
79
|
const serverlessRaw = env.ADMINDB_SERVERLESS ?? env.SERVERLESS ?? env.IS_SERVERLESS ?? '';
|
|
80
80
|
const serverless = ['1', 'true', 'yes', 'on'].includes(String(serverlessRaw).trim().toLowerCase()) ||
|
|
81
81
|
(0, serverless_1.isServerlessEnvironment)(env);
|
|
82
|
-
const readonlyRaw = env.ADMINDB_READONLY ?? env.READONLY
|
|
83
|
-
const
|
|
82
|
+
const readonlyRaw = env.ADMINDB_READONLY ?? env.READONLY;
|
|
83
|
+
const explicitReadonly = readonlyRaw !== undefined && readonlyRaw !== ''
|
|
84
|
+
? ['1', 'true', 'yes', 'on'].includes(String(readonlyRaw).trim().toLowerCase())
|
|
85
|
+
: undefined;
|
|
86
|
+
const isPg = Boolean((connectionRaw && (0, common_1.isPostgresConnectionString)(connectionRaw)) ||
|
|
87
|
+
(dbPathRaw && (0, common_1.isPostgresConnectionString)(dbPathRaw)));
|
|
88
|
+
const defaultReadonly = serverless ? !isPg : false;
|
|
89
|
+
const readonly = explicitReadonly !== undefined ? explicitReadonly : defaultReadonly;
|
|
84
90
|
let connections;
|
|
85
91
|
const configJsonRaw = env.ADMINDB_CONFIG_JSON ?? env.ADMINDB_CONNECTIONS;
|
|
86
92
|
if (configJsonRaw) {
|
|
@@ -109,6 +115,7 @@ function loadConfig(env = process.env) {
|
|
|
109
115
|
basePath: String(basePathRaw).replace(/\/+$/, ''),
|
|
110
116
|
logLevel: logLevelRaw,
|
|
111
117
|
readonly,
|
|
118
|
+
explicitReadonly,
|
|
112
119
|
serverless,
|
|
113
120
|
auth: !authDisabled,
|
|
114
121
|
authUsername: (env.ADMINDB_USERNAME ?? env.ADMINDB_USER) ? String(env.ADMINDB_USERNAME ?? env.ADMINDB_USER).trim() : undefined,
|
package/dist/cli/runner.js
CHANGED
|
@@ -38,9 +38,13 @@ function runCli() {
|
|
|
38
38
|
const authPassword = args.authPassword ?? env.authPassword;
|
|
39
39
|
const authSecret = args.authSecret ?? env.authSecret;
|
|
40
40
|
const serverless = args.serverless !== undefined ? args.serverless : env.serverless;
|
|
41
|
-
const
|
|
41
|
+
const explicitReadonly = args.explicitReadonly !== undefined ? args.explicitReadonly : env.explicitReadonly;
|
|
42
42
|
const connections = args.connections ?? env.connections;
|
|
43
43
|
const hasConnections = Boolean(connections && Object.keys(connections).length > 0);
|
|
44
|
+
const explicitFile = args.connection || args.dbPath || env.connection || env.dbPath;
|
|
45
|
+
const isPg = Boolean(explicitFile && (0, common_1.isPostgresConnectionString)(explicitFile));
|
|
46
|
+
const defaultReadonly = serverless ? !isPg : false;
|
|
47
|
+
const readonly = explicitReadonly !== undefined ? explicitReadonly : defaultReadonly;
|
|
44
48
|
const config = {
|
|
45
49
|
host: args.host ?? env.host,
|
|
46
50
|
port: args.port ?? env.port,
|
|
@@ -52,6 +56,7 @@ function runCli() {
|
|
|
52
56
|
basePath: (args.basePath ?? env.basePath).replace(/\/+$/, ''),
|
|
53
57
|
logLevel: args.logLevel ?? env.logLevel,
|
|
54
58
|
readonly,
|
|
59
|
+
explicitReadonly,
|
|
55
60
|
serverless,
|
|
56
61
|
auth: authEnabled
|
|
57
62
|
? {
|
|
@@ -67,8 +72,6 @@ function runCli() {
|
|
|
67
72
|
app.disable('x-powered-by');
|
|
68
73
|
const hasDir = Boolean(config.dbDir);
|
|
69
74
|
const hasFiles = Boolean(config.dbFiles?.length);
|
|
70
|
-
const explicitFile = args.connection || args.dbPath || config.connection || config.dbPath;
|
|
71
|
-
const isPg = Boolean(explicitFile && (0, common_1.isPostgresConnectionString)(explicitFile));
|
|
72
75
|
const singleFileOnly = !hasConnections && (Boolean(explicitFile) || isPg) && !hasDir && !hasFiles;
|
|
73
76
|
const useManager = !singleFileOnly;
|
|
74
77
|
const managerFiles = [...(config.dbFiles ?? [])];
|
|
@@ -79,10 +82,10 @@ function runCli() {
|
|
|
79
82
|
const browseRoot = hasDir ? node_path_1.default.resolve(config.dbDir) : undefined;
|
|
80
83
|
const router = useManager
|
|
81
84
|
? (0, app_1.createRouter)({
|
|
82
|
-
manager: new manager_1.DbManager({ dir: managerDir, files: managerFiles, connections, readonly: config.
|
|
85
|
+
manager: new manager_1.DbManager({ dir: managerDir, files: managerFiles, connections, readonly: explicitReadonly, serverless: config.serverless }, logger),
|
|
83
86
|
basePath: config.basePath,
|
|
84
87
|
logger,
|
|
85
|
-
readonly:
|
|
88
|
+
readonly: explicitReadonly,
|
|
86
89
|
serverless: config.serverless,
|
|
87
90
|
allowBrowse,
|
|
88
91
|
browseRoot,
|
|
@@ -93,7 +96,7 @@ function runCli() {
|
|
|
93
96
|
connection: explicitFile,
|
|
94
97
|
basePath: config.basePath,
|
|
95
98
|
logger,
|
|
96
|
-
readonly: config.
|
|
99
|
+
readonly: explicitReadonly !== undefined ? explicitReadonly : (!isPg && config.serverless),
|
|
97
100
|
serverless: config.serverless,
|
|
98
101
|
auth: config.auth,
|
|
99
102
|
});
|
package/dist/db/manager.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import type {IDatabase} from './types';import{type Logger} from '../utils/logger';export interface DbManagerOptions{dir?:string;files?:string[];connections?:Record<string,string>;readonly ?:boolean;}export interface DatabaseEntry{id:string;name:string;path:string;dialect:'sqlite'|'postgres';size:number;modified:string;readonly :boolean;}export declare class DbManager{private logger;private dir?;private createDir;private readonly readonlyMode;private files;private idByPath;private pathById;private nameById;private readonlyById;private openDbs;constructor(options:DbManagerOptions|string,logger?:Logger);get directory():string;get isReadOnly():boolean;isDbFile(name:string):boolean;private registerConnections;private registerDirFiles;private registerExplicitFiles;private addNamedTarget;private addFile;list():DatabaseEntry[];has(id:string):boolean;isDbReadOnly(id:string):boolean;setReadonly(id:string,readonly :boolean):void;open(id:string,readonlyOverride?:boolean):IDatabase;openFile(absPath:string,readonly ?:boolean):string;get(id:string):IDatabase|undefined;countTables(id:string):Promise<number>;create(name:string):string;addConnection(name:string,connectionString:string,readonly ?:boolean):string;remove(id:string):void;closeAll():void;}
|
|
1
|
+
import type {IDatabase} from './types';import{type Logger} from '../utils/logger';export interface DbManagerOptions{dir?:string;files?:string[];connections?:Record<string,string>;readonly ?:boolean;serverless?:boolean;}export interface DatabaseEntry{id:string;name:string;path:string;dialect:'sqlite'|'postgres';size:number;modified:string;readonly :boolean;}export declare class DbManager{private logger;private dir?;private createDir;private readonly readonlyMode;private readonly isServerless;private files;private idByPath;private pathById;private nameById;private readonlyById;private openDbs;constructor(options:DbManagerOptions|string,logger?:Logger);get directory():string;get isReadOnly():boolean;isDbFile(name:string):boolean;private registerConnections;private registerDirFiles;private registerExplicitFiles;private addNamedTarget;private addFile;list():DatabaseEntry[];has(id:string):boolean;isDbReadOnly(id:string):boolean;setReadonly(id:string,readonly :boolean):void;open(id:string,readonlyOverride?:boolean):IDatabase;openFile(absPath:string,readonly ?:boolean):string;get(id:string):IDatabase|undefined;countTables(id:string):Promise<number>;create(name:string):string;addConnection(name:string,connectionString:string,readonly ?:boolean):string;remove(id:string):void;closeAll():void;}
|
package/dist/db/manager.js
CHANGED
|
@@ -10,12 +10,14 @@ const database_1 = require("./database");
|
|
|
10
10
|
const postgres_1 = require("./postgres");
|
|
11
11
|
const logger_1 = require("../utils/logger");
|
|
12
12
|
const common_1 = require("../utils/common");
|
|
13
|
+
const serverless_1 = require("../serverless");
|
|
13
14
|
const DB_EXTENSIONS = ['.db', '.sqlite', '.sqlite3'];
|
|
14
15
|
class DbManager {
|
|
15
16
|
logger;
|
|
16
17
|
dir;
|
|
17
18
|
createDir;
|
|
18
19
|
readonlyMode;
|
|
20
|
+
isServerless;
|
|
19
21
|
files = [];
|
|
20
22
|
idByPath = new Map();
|
|
21
23
|
pathById = new Map();
|
|
@@ -28,6 +30,7 @@ class DbManager {
|
|
|
28
30
|
this.dir = opts.dir ? node_path_1.default.resolve(opts.dir) : undefined;
|
|
29
31
|
this.createDir = this.dir ?? process.cwd();
|
|
30
32
|
this.readonlyMode = Boolean(opts.readonly);
|
|
33
|
+
this.isServerless = opts.serverless !== undefined ? opts.serverless : (0, serverless_1.isServerlessEnvironment)();
|
|
31
34
|
if (this.dir)
|
|
32
35
|
(0, node_fs_1.mkdirSync)(this.dir, { recursive: true });
|
|
33
36
|
if (opts.connections)
|
|
@@ -157,12 +160,28 @@ class DbManager {
|
|
|
157
160
|
return this.pathById.has(id);
|
|
158
161
|
}
|
|
159
162
|
isDbReadOnly(id) {
|
|
160
|
-
|
|
163
|
+
if (this.readonlyMode)
|
|
164
|
+
return true;
|
|
165
|
+
if (this.readonlyById.has(id))
|
|
166
|
+
return Boolean(this.readonlyById.get(id));
|
|
167
|
+
if (this.isServerless) {
|
|
168
|
+
const target = this.pathById.get(id);
|
|
169
|
+
const isPg = target ? (0, common_1.isPostgresConnectionString)(target) : false;
|
|
170
|
+
return !isPg;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
161
173
|
}
|
|
162
174
|
setReadonly(id, readonly) {
|
|
163
175
|
if (this.readonlyMode && !readonly) {
|
|
164
176
|
throw new Error('Manager is in global read-only mode — databases cannot be made writable.');
|
|
165
177
|
}
|
|
178
|
+
if (this.isServerless && !readonly) {
|
|
179
|
+
const target = this.pathById.get(id);
|
|
180
|
+
const isPg = target ? (0, common_1.isPostgresConnectionString)(target) : false;
|
|
181
|
+
if (!isPg) {
|
|
182
|
+
throw new Error('SQLite database cannot be made writable in a serverless environment.');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
166
185
|
if (this.readonlyById.get(id) === readonly)
|
|
167
186
|
return;
|
|
168
187
|
this.readonlyById.set(id, readonly);
|
|
@@ -179,7 +198,7 @@ class DbManager {
|
|
|
179
198
|
const target = this.pathById.get(id);
|
|
180
199
|
if (!target)
|
|
181
200
|
throw new Error(`Database "${id}" is not registered.`);
|
|
182
|
-
const isRo =
|
|
201
|
+
const isRo = readonlyOverride !== undefined ? readonlyOverride : this.isDbReadOnly(id);
|
|
183
202
|
let db = this.openDbs.get(id);
|
|
184
203
|
if (db && db.isReadOnly !== isRo) {
|
|
185
204
|
try {
|