admindb 2.3.1 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 List tables
313
- GET /api/tables/:table/rows Paginated rows (with filtering & sorting)
314
- GET /api/tables/:table/row/:id Get a single row
315
- GET /api/tables/:table/row/:id/blob/:column Stream raw BLOB / BYTEA binary data
316
- GET /api/tables/:table/row/:id/blob/:col/meta BLOB / BYTEA metadata, MIME analysis & hex dump
317
- PUT /api/tables/:table/row/:id/blob/:column Upload / update binary content
318
- POST /api/tables/:table/rows Insert row (single or batch)
319
- PUT /api/tables/:table/row/:id Update row
320
- DELETE /api/tables/:table/row/:id Delete row
321
- POST /api/tables/:table/rows/bulk-update Apply staged inline edits atomically
322
- POST /api/tables/:table/rows/bulk-delete Delete selected rows atomically
323
- POST /api/tables/:table/seed Generate & insert realistic seed rows
324
- POST /api/tables Create a new table
325
- GET /api/tables/:table/schema Inspect table schema & constraints
326
- GET /api/tables/:table/ddl Get table CREATE SQL & indexes
327
- POST /api/query Execute arbitrary SQL
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,54 +20,10 @@ 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
- return options.manager ? createManagerApp(options) : createSingleDbApp(options);
24
- }
25
- function setupViewEngine(app, _logger) {
26
- app.engine('hbs', (0, express_handlebars_1.engine)({
27
- extname: '.hbs',
28
- defaultLayout: 'main',
29
- layoutsDir: node_path_1.default.join(__dirname, 'views', 'layouts'),
30
- partialsDir: node_path_1.default.join(__dirname, 'views', 'partials'),
31
- helpers: {
32
- eq: (a, b) => a === b,
33
- startsWith: (a, b) => String(a ?? '').startsWith(String(b ?? '')),
34
- isNull: (v) => v === null || v === undefined,
35
- truncate: (v, n) => {
36
- const s = v == null ? '' : String(v);
37
- return s.length > n ? `${s.slice(0, n)}…` : s;
38
- },
39
- json: (v) => (JSON.stringify(v ?? null) ?? 'null').replace(/</g, '\\u003c'),
40
- add: (a, b) => Number(a) + Number(b),
41
- sub: (a, b) => Number(a) - Number(b),
42
- gt: (a, b) => Number(a) > Number(b),
43
- lt: (a, b) => Number(a) < Number(b),
44
- join: (arr, sep) => (Array.isArray(arr) ? arr.join(String(sep ?? ',')) : String(arr ?? '')),
45
- icon: (name, options) => {
46
- const customClass = (options && options.hash && options.hash.class) || undefined;
47
- return (0, icons_1.renderIcon)(String(name ?? ''), customClass);
48
- },
49
- },
50
- }));
51
- app.set('view engine', 'hbs');
52
- app.set('views', node_path_1.default.join(__dirname, 'views'));
53
- }
54
- function addErrorHandlers(app, logger) {
55
- app.use((req, res) => {
56
- if (req.path.startsWith('/api/')) {
57
- return res.status(404).json({ success: false, error: 'Not found.' });
58
- }
59
- res.status(404).render('pages/error', { title: 'Not found', status: 404, error: 'The page you requested does not exist.' });
60
- });
61
- app.use((err, req, res, _next) => {
62
- logger.error('Unhandled error', err);
63
- if (req.path.startsWith('/api/')) {
64
- return res.status(500).json({ success: false, error: (0, common_1.errorMessage)(err) });
65
- }
66
- res.status(500).render('pages/error', { title: 'Error', status: 500, error: (0, common_1.errorMessage)(err) });
67
- });
68
- }
69
- function createSingleDbApp(options) {
70
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);
71
27
  const envTarget = process.env.ADMINDB_CONNECTION ||
72
28
  process.env.DATABASE_URL ||
73
29
  process.env.POSTGRES_URL ||
@@ -77,81 +33,37 @@ function createSingleDbApp(options) {
77
33
  const isPg = Boolean(options.pgOptions) ||
78
34
  (0, common_1.isPostgresConnectionString)(rawTarget) ||
79
35
  options.db?.dialect === 'postgres';
80
- const readonly = options.readonly !== undefined
81
- ? Boolean(options.readonly)
82
- : (isServerless ? !isPg : false);
83
- const basePath = (options.basePath ?? '').replace(/\/+$/, '');
84
- const logger = options.logger ?? (0, logger_1.createLogger)(options.logLevel ?? 'info', 'admindb');
85
- let db;
86
- if (options.db) {
87
- db = options.db;
88
- if (options.readonly !== undefined && db.isReadOnly !== Boolean(options.readonly)) {
89
- try {
90
- Object.defineProperty(db, 'isReadOnly', { value: Boolean(options.readonly), configurable: true });
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 { }
91
55
  }
92
- catch { }
93
56
  }
94
- }
95
- else if (isPg) {
96
- db = new postgres_1.PostgresDatabase(options.pgOptions || rawTarget, logger, { readonly });
97
- }
98
- else {
99
- db = new database_1.SqliteDatabase(options.dbPath ?? 'admindb.db', logger, { readonly });
100
- }
101
- const databasesUrl = options.databasesUrl;
102
- const authConfig = (0, auth_1.resolveAuthConfig)(options.auth);
103
- const router = (0, express_1.default)();
104
- router.disable('x-powered-by');
105
- router.use(express_1.default.static(node_path_1.default.join(__dirname, 'public')));
106
- setupViewEngine(router, logger);
107
- router.use(express_1.default.json({ limit: '10mb' }));
108
- router.use(express_1.default.urlencoded({ extended: false }));
109
- (0, auth_1.registerAuthRoutes)(router, authConfig, basePath, logger);
110
- router.use((0, auth_1.createAuthMiddleware)(authConfig, basePath, logger));
111
- router.use(async (req, res, next) => {
112
- try {
113
- res.locals.basePath = basePath;
114
- res.locals.currentPath = req.path;
115
- res.locals.currentTable = null;
116
- res.locals.dbPath = db.path;
117
- res.locals.dbId = options.dbId ?? (db.dialect === 'postgres' ? 'PostgreSQL' : (node_path_1.default.basename(db.path) || 'SQLite'));
118
- res.locals.dialect = db.dialect;
119
- res.locals.isPostgres = db.dialect === 'postgres';
120
- res.locals.isSqlite = db.dialect === 'sqlite';
121
- res.locals.dialectName = db.dialect === 'postgres' ? 'PostgreSQL' : 'SQLite';
122
- res.locals.databasesUrl = databasesUrl ?? null;
123
- res.locals.databasesMode = false;
124
- res.locals.readonly = db.isReadOnly;
125
- res.locals.serverless = isServerless;
126
- res.locals.version = APP_VERSION;
127
- res.locals.authEnabled = authConfig.enabled;
128
- res.locals.isDefaultPassword = authConfig.isDefaultPassword;
129
- res.locals.icons = icons_1.ICONS;
130
- if (!req.path.startsWith('/api/')) {
131
- const all = await db.listTables();
132
- const names = (all.data ?? []).map((t) => t.name);
133
- res.locals.tables = names.filter((n) => !n.startsWith('_'));
134
- res.locals.internalTables = names.filter((n) => n.startsWith('_'));
135
- }
136
- next();
57
+ else if (isPg) {
58
+ singleDb = new postgres_1.PostgresDatabase(options.pgOptions || rawTarget, logger, { readonly: globalReadonly });
137
59
  }
138
- catch (err) {
139
- next(err);
60
+ else {
61
+ singleDb = new database_1.SqliteDatabase(options.dbPath ?? 'admindb.db', logger, { readonly: globalReadonly });
140
62
  }
141
- });
142
- (0, pages_1.registerPages)(router, { db, logger });
143
- (0, index_1.registerApi)(router, { db, logger });
144
- addErrorHandlers(router, logger);
145
- return router;
146
- }
147
- function createManagerApp(options) {
148
- const isServerless = options.serverless !== undefined ? options.serverless : (0, serverless_1.isServerlessEnvironment)();
149
- const explicitReadonly = options.readonly !== undefined ? Boolean(options.readonly) : undefined;
150
- const manager = options.manager;
151
- const globalReadonly = explicitReadonly ?? manager.isReadOnly;
152
- const basePath = (options.basePath ?? '').replace(/\/+$/, '');
153
- const logger = options.logger ?? (0, logger_1.createLogger)(options.logLevel ?? 'info', 'admindb');
154
- const authConfig = (0, auth_1.resolveAuthConfig)(options.auth);
63
+ if (!defaultDbId) {
64
+ defaultDbId = singleDb.dialect === 'postgres' ? 'PostgreSQL' : (node_path_1.default.basename(singleDb.path) || 'SQLite');
65
+ }
66
+ }
155
67
  const app = (0, express_1.default)();
156
68
  app.disable('x-powered-by');
157
69
  app.use(express_1.default.static(node_path_1.default.join(__dirname, 'public')));
@@ -165,75 +77,111 @@ function createManagerApp(options) {
165
77
  res.locals.basePath = basePath;
166
78
  res.locals.currentPath = req.path;
167
79
  res.locals.currentTable = null;
168
- res.locals.dbPath = manager.directory;
169
- res.locals.dbId = null;
170
- res.locals.databasesUrl = null;
171
- res.locals.databasesMode = true;
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;
172
84
  res.locals.readonly = globalReadonly;
173
85
  res.locals.serverless = isServerless;
174
86
  res.locals.version = APP_VERSION;
175
87
  res.locals.authEnabled = authConfig.enabled;
176
88
  res.locals.isDefaultPassword = authConfig.isDefaultPassword;
177
89
  res.locals.icons = icons_1.ICONS;
178
- if (!req.path.startsWith('/api/')) {
179
- res.locals.tables = [];
180
- res.locals.internalTables = [];
181
- }
90
+ res.locals.tables = [];
91
+ res.locals.internalTables = [];
182
92
  next();
183
93
  }
184
94
  catch (err) {
185
95
  next(err);
186
96
  }
187
97
  });
188
- const subApps = new Map();
189
- (0, databases_1.registerDatabasesRoutes)(app, {
190
- manager,
191
- logger,
192
- basePath,
193
- readonly: globalReadonly,
194
- allowBrowse: isServerless ? false : options.allowBrowse,
195
- browseRoot: options.browseRoot,
196
- invalidate: (id) => subApps.delete(id),
197
- });
198
- app.use('/:dbId', (req, res, next) => {
199
- const dbId = String(req.params.dbId);
200
- if (!manager.has(dbId)) {
201
- if (req.path.startsWith('/api/')) {
202
- return res.status(404).json({ success: false, error: `Database "${dbId}" does not exist.` });
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;
203
105
  }
204
- return res.status(404).render('pages/error', {
205
- title: 'Not found',
206
- status: 404,
207
- error: `Database "${dbId}" does not exist.`,
208
- });
209
- }
210
- const rawRo = req.query.readonly;
211
- if (rawRo !== undefined) {
212
- const explicitRo = rawRo === '1' || rawRo === 'true';
213
- if (!globalReadonly || explicitRo) {
214
- manager.setReadonly(dbId, explicitRo);
215
- subApps.delete(dbId);
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
+ }
216
112
  }
217
- }
218
- let sub = subApps.get(dbId);
219
- if (!sub) {
220
- const subBase = `${basePath || ''}/${encodeURIComponent(dbId)}`;
221
113
  const effectiveRo = globalReadonly ? true : manager.isDbReadOnly(dbId);
222
- sub = createSingleDbApp({
223
- db: manager.open(dbId, effectiveRo),
224
- basePath: subBase,
225
- logger,
226
- logLevel: options.logLevel,
227
- dbId,
228
- databasesUrl: `${basePath || ''}/`,
229
- readonly: effectiveRo,
230
- serverless: isServerless,
231
- auth: authConfig,
232
- });
233
- subApps.set(dbId, sub);
114
+ return manager.open(dbId, effectiveRo);
234
115
  }
235
- return sub(req, res, next);
236
- });
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 });
237
135
  addErrorHandlers(app, logger);
238
136
  return app;
239
137
  }
138
+ function setupViewEngine(app, _logger) {
139
+ app.engine('hbs', (0, express_handlebars_1.engine)({
140
+ extname: '.hbs',
141
+ defaultLayout: 'main',
142
+ layoutsDir: node_path_1.default.join(__dirname, 'views', 'layouts'),
143
+ partialsDir: node_path_1.default.join(__dirname, 'views', 'partials'),
144
+ helpers: {
145
+ eq: (a, b) => a === b,
146
+ startsWith: (a, b) => String(a ?? '').startsWith(String(b ?? '')),
147
+ isNull: (v) => v === null || v === undefined,
148
+ truncate: (v, n) => {
149
+ const s = v == null ? '' : String(v);
150
+ return s.length > n ? `${s.slice(0, n)}…` : s;
151
+ },
152
+ json: (v) => (JSON.stringify(v ?? null) ?? 'null').replace(/</g, '\\u003c'),
153
+ add: (a, b) => Number(a) + Number(b),
154
+ sub: (a, b) => Number(a) - Number(b),
155
+ gt: (a, b) => Number(a) > Number(b),
156
+ lt: (a, b) => Number(a) < Number(b),
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 ?? '')),
161
+ icon: (name, options) => {
162
+ const customClass = (options && options.hash && options.hash.class) || undefined;
163
+ return (0, icons_1.renderIcon)(String(name ?? ''), customClass);
164
+ },
165
+ },
166
+ }));
167
+ app.set('view engine', 'hbs');
168
+ app.set('views', node_path_1.default.join(__dirname, 'views'));
169
+ }
170
+ function addErrorHandlers(app, logger) {
171
+ app.use((req, res) => {
172
+ if (req.path.startsWith('/api/')) {
173
+ return res.status(404).json({ success: false, error: 'Not found.' });
174
+ }
175
+ res.status(404).render('pages/error', { title: 'Not found', status: 404, error: 'The page you requested does not exist.' });
176
+ });
177
+ app.use((err, req, res, _next) => {
178
+ const status = typeof err?.status === 'number' ? err.status : 500;
179
+ if (status >= 500) {
180
+ logger.error('Unhandled error', err);
181
+ }
182
+ if (req.path.startsWith('/api/')) {
183
+ return res.status(status).json({ success: false, error: (0, common_1.errorMessage)(err) });
184
+ }
185
+ res.status(status).render('pages/error', { title: status === 404 ? 'Not found' : 'Error', status, error: (0, common_1.errorMessage)(err) });
186
+ });
187
+ }