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.
Files changed (45) hide show
  1. package/README.md +17 -17
  2. package/dist/app.js +123 -163
  3. package/dist/cli/args.d.ts +1 -1
  4. package/dist/cli/args.js +5 -3
  5. package/dist/cli/config.d.ts +1 -1
  6. package/dist/cli/config.js +9 -2
  7. package/dist/cli/runner.js +9 -6
  8. package/dist/db/manager.d.ts +1 -1
  9. package/dist/db/manager.js +21 -2
  10. package/dist/public/css/app.css +1 -1
  11. package/dist/public/js/browse.js +1 -1
  12. package/dist/public/js/databases.js +1 -1
  13. package/dist/public/js/designer.js +1 -1
  14. package/dist/public/js/forms.js +1 -1
  15. package/dist/public/js/home.js +1 -1
  16. package/dist/public/js/inspector.js +1 -1
  17. package/dist/public/js/query.js +1 -1
  18. package/dist/public/js/schema.js +1 -1
  19. package/dist/public/js/seed.js +1 -1
  20. package/dist/routes/api/erd.js +2 -2
  21. package/dist/routes/api/helpers.d.ts +1 -1
  22. package/dist/routes/api/import-export.js +8 -6
  23. package/dist/routes/api/index.js +16 -9
  24. package/dist/routes/api/query.js +8 -5
  25. package/dist/routes/api/rows.js +36 -21
  26. package/dist/routes/api/seed.js +29 -16
  27. package/dist/routes/api/tables.js +32 -18
  28. package/dist/routes/databases.js +2 -0
  29. package/dist/routes/pages.d.ts +1 -1
  30. package/dist/routes/pages.js +137 -43
  31. package/dist/serverless.js +16 -4
  32. package/dist/views/layouts/main.hbs +1 -1
  33. package/dist/views/pages/databases.hbs +1 -1
  34. package/dist/views/pages/erd.hbs +1 -1
  35. package/dist/views/pages/form.hbs +1 -1
  36. package/dist/views/pages/home.hbs +1 -1
  37. package/dist/views/pages/info.hbs +1 -1
  38. package/dist/views/pages/schema.hbs +1 -1
  39. package/dist/views/pages/seed-select.hbs +6 -5
  40. package/dist/views/pages/seed.hbs +3 -3
  41. package/dist/views/pages/table.hbs +1 -1
  42. package/dist/views/partials/navbar.hbs +1 -1
  43. package/dist/views/partials/sidebar.hbs +1 -1
  44. package/docs/API.md +60 -55
  45. package/package.json +3 -2
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
5
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
6
6
  [![Downloads](https://img.shields.io/npm/dm/admindb.svg)](https://www.npmjs.com/package/admindb)
7
- [![Publish](https://github.com/MIbnEKhalid/admindb/actions/workflows/publish.yml/badge.svg?branch=main)](https://github.com/MIbnEKhalid/admindb/actions/workflows/publish.yml)
7
+ [![Check npm version](https://github.com/MIbnEKhalid/mbkauthe/actions/workflows/checkLatestVersion.yml/badge.svg)](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 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,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
- return options.manager ? createManagerApp(options) : createSingleDbApp(options);
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
- 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) });
178
+ const status = typeof err?.status === 'number' ? err.status : 500;
179
+ if (status >= 500) {
180
+ logger.error('Unhandled error', err);
65
181
  }
66
- res.status(500).render('pages/error', { title: 'Error', status: 500, error: (0, common_1.errorMessage)(err) });
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
- return sub(req, res, next);
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
  }
@@ -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 (enforces read-only, disables writes) [SERVERLESS / ADMINDB_SERVERLESS]',
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;
@@ -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;
@@ -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 readonly = serverless || ['1', 'true', 'yes', 'on'].includes(String(readonlyRaw).trim().toLowerCase());
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,
@@ -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 readonly = serverless || args.readonly || env.readonly;
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.readonly }, logger),
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: config.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.readonly,
99
+ readonly: explicitReadonly !== undefined ? explicitReadonly : (!isPg && config.serverless),
97
100
  serverless: config.serverless,
98
101
  auth: config.auth,
99
102
  });
@@ -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;}
@@ -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
- return this.readonlyMode || (this.readonlyById.get(id) ?? false);
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 = this.readonlyMode || (readonlyOverride !== undefined ? readonlyOverride : (this.readonlyById.get(id) ?? false));
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 {