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/db/filters.js
CHANGED
|
@@ -1 +1,119 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.escapeLike = void 0;
|
|
4
|
+
exports.pushFilterCondition = pushFilterCondition;
|
|
5
|
+
exports.buildFilterClause = buildFilterClause;
|
|
6
|
+
exports.convertPlaceholdersToPostgres = convertPlaceholdersToPostgres;
|
|
7
|
+
const generator_1 = require("../sql/generator");
|
|
8
|
+
const escapeLike = (s) => String(s).replace(/[\\%_]/g, '\\$&');
|
|
9
|
+
exports.escapeLike = escapeLike;
|
|
10
|
+
function pushFilterCondition(conds, params, quotedCol, c) {
|
|
11
|
+
const value = String(c.value ?? '');
|
|
12
|
+
switch (c.op) {
|
|
13
|
+
case 'eq':
|
|
14
|
+
conds.push(`${quotedCol} = ?`);
|
|
15
|
+
params.push(value);
|
|
16
|
+
break;
|
|
17
|
+
case 'neq':
|
|
18
|
+
conds.push(`${quotedCol} != ?`);
|
|
19
|
+
params.push(value);
|
|
20
|
+
break;
|
|
21
|
+
case 'gt':
|
|
22
|
+
conds.push(`${quotedCol} > ?`);
|
|
23
|
+
params.push(value);
|
|
24
|
+
break;
|
|
25
|
+
case 'gte':
|
|
26
|
+
conds.push(`${quotedCol} >= ?`);
|
|
27
|
+
params.push(value);
|
|
28
|
+
break;
|
|
29
|
+
case 'lt':
|
|
30
|
+
conds.push(`${quotedCol} < ?`);
|
|
31
|
+
params.push(value);
|
|
32
|
+
break;
|
|
33
|
+
case 'lte':
|
|
34
|
+
conds.push(`${quotedCol} <= ?`);
|
|
35
|
+
params.push(value);
|
|
36
|
+
break;
|
|
37
|
+
case 'like':
|
|
38
|
+
conds.push(`${quotedCol} LIKE ? ESCAPE '\\'`);
|
|
39
|
+
params.push(`%${(0, exports.escapeLike)(value)}%`);
|
|
40
|
+
break;
|
|
41
|
+
case 'prefix':
|
|
42
|
+
conds.push(`${quotedCol} LIKE ? ESCAPE '\\'`);
|
|
43
|
+
params.push(`${(0, exports.escapeLike)(value)}%`);
|
|
44
|
+
break;
|
|
45
|
+
case 'between':
|
|
46
|
+
conds.push(`${quotedCol} >= ?`, `${quotedCol} <= ?`);
|
|
47
|
+
params.push(value, String(c.max ?? ''));
|
|
48
|
+
break;
|
|
49
|
+
case 'null':
|
|
50
|
+
conds.push(`${quotedCol} IS NULL`);
|
|
51
|
+
break;
|
|
52
|
+
case 'notnull':
|
|
53
|
+
conds.push(`${quotedCol} IS NOT NULL`);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function buildFilterClause(filters, availableCols) {
|
|
58
|
+
const conds = [];
|
|
59
|
+
const params = [];
|
|
60
|
+
const colSet = new Set(availableCols);
|
|
61
|
+
for (const [col, raw] of Object.entries(filters ?? {})) {
|
|
62
|
+
if (!colSet.has(col))
|
|
63
|
+
continue;
|
|
64
|
+
const q = (0, generator_1.quoteIdentifier)(col);
|
|
65
|
+
if (Array.isArray(raw)) {
|
|
66
|
+
for (const c of raw) {
|
|
67
|
+
if (c && typeof c === 'object')
|
|
68
|
+
pushFilterCondition(conds, params, q, c);
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (raw && typeof raw === 'object') {
|
|
73
|
+
pushFilterCondition(conds, params, q, raw);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const value = String(raw ?? '').trim();
|
|
77
|
+
if (!value)
|
|
78
|
+
continue;
|
|
79
|
+
const opMatch = value.match(/^(>=|<=|!=|=|>|<)(.*)$/);
|
|
80
|
+
if (opMatch) {
|
|
81
|
+
const [, op, val] = opMatch;
|
|
82
|
+
conds.push(`${q} ${op} ?`);
|
|
83
|
+
params.push(val.trim());
|
|
84
|
+
}
|
|
85
|
+
else if (value.endsWith('*')) {
|
|
86
|
+
conds.push(`${q} LIKE ? ESCAPE '\\'`);
|
|
87
|
+
params.push(`${(0, exports.escapeLike)(value.slice(0, -1))}%`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
conds.push(`${q} LIKE ? ESCAPE '\\'`);
|
|
91
|
+
params.push(`%${(0, exports.escapeLike)(value)}%`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { where: conds.length ? ` WHERE ${conds.join(' AND ')}` : '', params };
|
|
95
|
+
}
|
|
96
|
+
function convertPlaceholdersToPostgres(sql, startIdx = 1) {
|
|
97
|
+
let idx = startIdx;
|
|
98
|
+
let inString = false;
|
|
99
|
+
let result = '';
|
|
100
|
+
for (let i = 0; i < sql.length; i++) {
|
|
101
|
+
const ch = sql[i];
|
|
102
|
+
if (ch === "'") {
|
|
103
|
+
if (inString && sql[i + 1] === "'") {
|
|
104
|
+
result += "''";
|
|
105
|
+
i++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
inString = !inString;
|
|
109
|
+
result += ch;
|
|
110
|
+
}
|
|
111
|
+
else if (ch === '?' && !inString) {
|
|
112
|
+
result += `$${idx++}`;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
result += ch;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
package/dist/db/index.js
CHANGED
|
@@ -1 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./filters"), exports);
|
|
19
|
+
__exportStar(require("./introspection"), exports);
|
|
20
|
+
__exportStar(require("./migrations"), exports);
|
|
21
|
+
__exportStar(require("./database"), exports);
|
|
22
|
+
__exportStar(require("./postgres"), exports);
|
|
23
|
+
__exportStar(require("./manager"), exports);
|
|
24
|
+
__exportStar(require("./export"), exports);
|
package/dist/db/introspection.js
CHANGED
|
@@ -1 +1,164 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listTablesSync = listTablesSync;
|
|
4
|
+
exports.getTableInfoSync = getTableInfoSync;
|
|
5
|
+
exports.getSchemaSync = getSchemaSync;
|
|
6
|
+
exports.getReferencingTablesSync = getReferencingTablesSync;
|
|
7
|
+
const generator_1 = require("../sql/generator");
|
|
8
|
+
function listTablesSync(db) {
|
|
9
|
+
const rows = db
|
|
10
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
|
|
11
|
+
.all();
|
|
12
|
+
return rows.map((r) => ({ name: r.name }));
|
|
13
|
+
}
|
|
14
|
+
function getTableNames(db) {
|
|
15
|
+
return db
|
|
16
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
|
|
17
|
+
.all().map((r) => r.name);
|
|
18
|
+
}
|
|
19
|
+
function getIncomingFks(db, targetTable) {
|
|
20
|
+
const refs = [];
|
|
21
|
+
for (const t of getTableNames(db)) {
|
|
22
|
+
if (t === targetTable)
|
|
23
|
+
continue;
|
|
24
|
+
try {
|
|
25
|
+
const fks = db.prepare(`PRAGMA foreign_key_list(${(0, generator_1.quoteIdentifier)(t)})`).all();
|
|
26
|
+
for (const fk of fks) {
|
|
27
|
+
if (fk.table === targetTable)
|
|
28
|
+
refs.push({ table: t, from: fk.from, to: fk.to ?? '' });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
}
|
|
33
|
+
return refs;
|
|
34
|
+
}
|
|
35
|
+
function getTableInfoSync(db, table) {
|
|
36
|
+
const cols = db.prepare(`PRAGMA table_info(${(0, generator_1.quoteIdentifier)(table)})`).all();
|
|
37
|
+
const fks = db.prepare(`PRAGMA foreign_key_list(${(0, generator_1.quoteIdentifier)(table)})`).all();
|
|
38
|
+
const primaryKey = cols
|
|
39
|
+
.filter((c) => c.pk > 0)
|
|
40
|
+
.sort((a, b) => a.pk - b.pk)
|
|
41
|
+
.map((c) => c.name);
|
|
42
|
+
const indexRows = db
|
|
43
|
+
.prepare(`PRAGMA index_list(${(0, generator_1.quoteIdentifier)(table)})`)
|
|
44
|
+
.all();
|
|
45
|
+
const indexes = indexRows.map((ix) => {
|
|
46
|
+
const ixCols = db
|
|
47
|
+
.prepare(`PRAGMA index_info(${(0, generator_1.quoteIdentifier)(ix.name)})`)
|
|
48
|
+
.all().map((c) => c.name);
|
|
49
|
+
const sqlRow = db
|
|
50
|
+
.prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?")
|
|
51
|
+
.get(ix.name);
|
|
52
|
+
return {
|
|
53
|
+
name: ix.name,
|
|
54
|
+
unique: Boolean(ix.unique),
|
|
55
|
+
partial: ix.partial,
|
|
56
|
+
origin: ix.origin,
|
|
57
|
+
columns: ixCols,
|
|
58
|
+
sql: sqlRow?.sql ?? null,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
const ddlRow = db
|
|
62
|
+
.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
63
|
+
.get(table);
|
|
64
|
+
return {
|
|
65
|
+
table,
|
|
66
|
+
columns: cols.map((c) => ({
|
|
67
|
+
cid: c.cid,
|
|
68
|
+
name: c.name,
|
|
69
|
+
type: c.type,
|
|
70
|
+
notnull: c.notnull,
|
|
71
|
+
dflt_value: c.dflt_value,
|
|
72
|
+
pk: c.pk,
|
|
73
|
+
})),
|
|
74
|
+
foreignKeys: fks.map((f) => ({
|
|
75
|
+
id: f.id,
|
|
76
|
+
seq: f.seq,
|
|
77
|
+
table: f.table,
|
|
78
|
+
from: f.from,
|
|
79
|
+
to: f.to,
|
|
80
|
+
on_update: f.on_update,
|
|
81
|
+
on_delete: f.on_delete,
|
|
82
|
+
})),
|
|
83
|
+
primaryKey,
|
|
84
|
+
indexes,
|
|
85
|
+
sql: ddlRow?.sql ?? null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function getSchemaSync(db, table) {
|
|
89
|
+
const info = getTableInfoSync(db, table);
|
|
90
|
+
if (!info || info.columns.length === 0)
|
|
91
|
+
throw new Error(`Table "${table}" does not exist.`);
|
|
92
|
+
const { foreignKeys, indexes } = info;
|
|
93
|
+
const uniqueCols = new Set();
|
|
94
|
+
const indexedCols = new Set();
|
|
95
|
+
for (const ix of indexes) {
|
|
96
|
+
if (ix.unique)
|
|
97
|
+
ix.columns.forEach((c) => uniqueCols.add(c));
|
|
98
|
+
if (ix.origin === 'c')
|
|
99
|
+
ix.columns.forEach((c) => indexedCols.add(c));
|
|
100
|
+
}
|
|
101
|
+
const refs = getIncomingFks(db, table);
|
|
102
|
+
const refsByTo = new Map();
|
|
103
|
+
for (const r of refs) {
|
|
104
|
+
const arr = refsByTo.get(r.to) ?? [];
|
|
105
|
+
arr.push({ table: r.table, from: r.from });
|
|
106
|
+
refsByTo.set(r.to, arr);
|
|
107
|
+
}
|
|
108
|
+
const fkByFrom = new Map(foreignKeys.map((fk) => [fk.from, fk]));
|
|
109
|
+
const columns = info.columns.map((c) => {
|
|
110
|
+
const unique = c.pk > 0 || uniqueCols.has(c.name);
|
|
111
|
+
const indexed = indexedCols.has(c.name);
|
|
112
|
+
const referencedBy = refsByTo.get(c.name) ?? [];
|
|
113
|
+
const fk = fkByFrom.get(c.name) ?? null;
|
|
114
|
+
const dropBlockers = [];
|
|
115
|
+
if (c.pk > 0)
|
|
116
|
+
dropBlockers.push('Is part of the primary key');
|
|
117
|
+
if (unique && c.pk === 0)
|
|
118
|
+
dropBlockers.push('Has a UNIQUE constraint');
|
|
119
|
+
if (indexed)
|
|
120
|
+
dropBlockers.push('Is used by an index');
|
|
121
|
+
if (referencedBy.length)
|
|
122
|
+
dropBlockers.push(`Referenced by a foreign key in "${referencedBy[0].table}"`);
|
|
123
|
+
if (fk)
|
|
124
|
+
dropBlockers.push('Is part of a foreign key');
|
|
125
|
+
return {
|
|
126
|
+
...c,
|
|
127
|
+
unique,
|
|
128
|
+
indexed,
|
|
129
|
+
referencedBy,
|
|
130
|
+
fk,
|
|
131
|
+
canDrop: dropBlockers.length === 0,
|
|
132
|
+
dropBlockers,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
table,
|
|
137
|
+
columns,
|
|
138
|
+
foreignKeys,
|
|
139
|
+
indexes,
|
|
140
|
+
references: refs,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function getReferencingTablesSync(db, table) {
|
|
144
|
+
const refs = getIncomingFks(db, table);
|
|
145
|
+
const byTable = new Map();
|
|
146
|
+
for (const r of refs) {
|
|
147
|
+
const arr = byTable.get(r.table) ?? [];
|
|
148
|
+
arr.push({ from: r.from, to: r.to });
|
|
149
|
+
byTable.set(r.table, arr);
|
|
150
|
+
}
|
|
151
|
+
const result = [];
|
|
152
|
+
for (const [t, tRefs] of byTable) {
|
|
153
|
+
let refCount = 0;
|
|
154
|
+
try {
|
|
155
|
+
const conds = tRefs.map((r) => `${(0, generator_1.quoteIdentifier)(r.from)} IS NOT NULL`);
|
|
156
|
+
const row = db.prepare(`SELECT COUNT(*) AS c FROM ${(0, generator_1.quoteIdentifier)(t)} WHERE ${conds.join(' OR ')}`).get();
|
|
157
|
+
refCount = Number(row.c);
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
result.push({ table: t, refs: tRefs, refCount });
|
|
161
|
+
}
|
|
162
|
+
result.sort((a, b) => a.table.localeCompare(b.table));
|
|
163
|
+
return result;
|
|
164
|
+
}
|
package/dist/db/manager.js
CHANGED
|
@@ -1 +1,327 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DbManager = void 0;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const database_1 = require("./database");
|
|
10
|
+
const postgres_1 = require("./postgres");
|
|
11
|
+
const logger_1 = require("../utils/logger");
|
|
12
|
+
const common_1 = require("../utils/common");
|
|
13
|
+
const DB_EXTENSIONS = ['.db', '.sqlite', '.sqlite3'];
|
|
14
|
+
class DbManager {
|
|
15
|
+
logger;
|
|
16
|
+
dir;
|
|
17
|
+
createDir;
|
|
18
|
+
readonlyMode;
|
|
19
|
+
files = [];
|
|
20
|
+
idByPath = new Map();
|
|
21
|
+
pathById = new Map();
|
|
22
|
+
nameById = new Map();
|
|
23
|
+
readonlyById = new Map();
|
|
24
|
+
openDbs = new Map();
|
|
25
|
+
constructor(options, logger) {
|
|
26
|
+
const opts = typeof options === 'string' ? { dir: options } : (options ?? {});
|
|
27
|
+
this.logger = logger ?? (0, logger_1.createLogger)('info', 'admindb');
|
|
28
|
+
this.dir = opts.dir ? node_path_1.default.resolve(opts.dir) : undefined;
|
|
29
|
+
this.createDir = this.dir ?? process.cwd();
|
|
30
|
+
this.readonlyMode = Boolean(opts.readonly);
|
|
31
|
+
if (this.dir)
|
|
32
|
+
(0, node_fs_1.mkdirSync)(this.dir, { recursive: true });
|
|
33
|
+
if (opts.connections)
|
|
34
|
+
this.registerConnections(opts.connections);
|
|
35
|
+
this.registerDirFiles();
|
|
36
|
+
this.registerExplicitFiles(opts.files ?? []);
|
|
37
|
+
}
|
|
38
|
+
get directory() {
|
|
39
|
+
return this.dir ?? this.createDir;
|
|
40
|
+
}
|
|
41
|
+
get isReadOnly() {
|
|
42
|
+
return this.readonlyMode;
|
|
43
|
+
}
|
|
44
|
+
isDbFile(name) {
|
|
45
|
+
const lower = String(name ?? '').toLowerCase();
|
|
46
|
+
return DB_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
47
|
+
}
|
|
48
|
+
registerConnections(conns) {
|
|
49
|
+
for (const [name, target] of Object.entries(conns ?? {})) {
|
|
50
|
+
const cleanTarget = String(target ?? '').trim();
|
|
51
|
+
if (name && cleanTarget)
|
|
52
|
+
this.addNamedTarget(name.trim(), cleanTarget);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
registerDirFiles() {
|
|
56
|
+
if (!this.dir)
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
const names = (0, node_fs_1.readdirSync)(this.dir)
|
|
60
|
+
.filter((n) => this.isDbFile(n))
|
|
61
|
+
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
|
|
62
|
+
for (const name of names)
|
|
63
|
+
this.addFile(node_path_1.default.join(this.dir, name));
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
this.logger.warn(`Failed to list database directory ${this.dir}: ${(0, common_1.errorMessage)(err)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
registerExplicitFiles(files) {
|
|
70
|
+
for (const f of files ?? []) {
|
|
71
|
+
const raw = String(f ?? '').trim();
|
|
72
|
+
if (!raw)
|
|
73
|
+
continue;
|
|
74
|
+
if ((0, common_1.isPostgresConnectionString)(raw)) {
|
|
75
|
+
let name = 'postgres';
|
|
76
|
+
try {
|
|
77
|
+
name = new URL(raw).pathname.replace(/^\//, '') || 'postgres';
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
this.addNamedTarget(name, raw);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const abs = node_path_1.default.resolve(raw);
|
|
84
|
+
if (!(0, node_fs_1.existsSync)(abs)) {
|
|
85
|
+
this.logger.warn(`Skipping missing database file: ${f}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
this.addFile(abs);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
addNamedTarget(name, target) {
|
|
92
|
+
let id = name;
|
|
93
|
+
let n = 2;
|
|
94
|
+
while (this.pathById.has(id))
|
|
95
|
+
id = `${name}__${n++}`;
|
|
96
|
+
this.files.push(target);
|
|
97
|
+
this.idByPath.set(target, id);
|
|
98
|
+
this.pathById.set(id, target);
|
|
99
|
+
this.nameById.set(id, name);
|
|
100
|
+
}
|
|
101
|
+
addFile(abs) {
|
|
102
|
+
if (this.idByPath.has(abs))
|
|
103
|
+
return;
|
|
104
|
+
const base = node_path_1.default.basename(abs);
|
|
105
|
+
let id = base;
|
|
106
|
+
let n = 2;
|
|
107
|
+
while (this.pathById.has(id)) {
|
|
108
|
+
const ext = node_path_1.default.extname(base);
|
|
109
|
+
id = `${node_path_1.default.basename(base, ext)}__${n++}${ext}`;
|
|
110
|
+
}
|
|
111
|
+
this.files.push(abs);
|
|
112
|
+
this.idByPath.set(abs, id);
|
|
113
|
+
this.pathById.set(id, abs);
|
|
114
|
+
this.nameById.set(id, base);
|
|
115
|
+
}
|
|
116
|
+
list() {
|
|
117
|
+
const entries = [];
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
for (const [id, target] of this.pathById.entries()) {
|
|
120
|
+
if (seen.has(id))
|
|
121
|
+
continue;
|
|
122
|
+
seen.add(id);
|
|
123
|
+
if ((0, common_1.isPostgresConnectionString)(target)) {
|
|
124
|
+
entries.push({
|
|
125
|
+
id,
|
|
126
|
+
name: this.nameById.get(id) ?? id,
|
|
127
|
+
path: (0, common_1.sanitizeConnectionString)(target),
|
|
128
|
+
dialect: 'postgres',
|
|
129
|
+
size: 0,
|
|
130
|
+
modified: 'Connected',
|
|
131
|
+
readonly: this.isDbReadOnly(id),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
let size = 0;
|
|
136
|
+
let modified = '';
|
|
137
|
+
try {
|
|
138
|
+
const st = (0, node_fs_1.statSync)(target);
|
|
139
|
+
size = st.size;
|
|
140
|
+
modified = st.mtime.toISOString();
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
entries.push({
|
|
144
|
+
id,
|
|
145
|
+
name: this.nameById.get(id) ?? node_path_1.default.basename(target),
|
|
146
|
+
path: target,
|
|
147
|
+
dialect: 'sqlite',
|
|
148
|
+
size,
|
|
149
|
+
modified,
|
|
150
|
+
readonly: this.isDbReadOnly(id),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return entries;
|
|
155
|
+
}
|
|
156
|
+
has(id) {
|
|
157
|
+
return this.pathById.has(id);
|
|
158
|
+
}
|
|
159
|
+
isDbReadOnly(id) {
|
|
160
|
+
return this.readonlyMode || (this.readonlyById.get(id) ?? false);
|
|
161
|
+
}
|
|
162
|
+
setReadonly(id, readonly) {
|
|
163
|
+
if (this.readonlyMode && !readonly) {
|
|
164
|
+
throw new Error('Manager is in global read-only mode — databases cannot be made writable.');
|
|
165
|
+
}
|
|
166
|
+
if (this.readonlyById.get(id) === readonly)
|
|
167
|
+
return;
|
|
168
|
+
this.readonlyById.set(id, readonly);
|
|
169
|
+
const db = this.openDbs.get(id);
|
|
170
|
+
if (db) {
|
|
171
|
+
try {
|
|
172
|
+
db.close();
|
|
173
|
+
}
|
|
174
|
+
catch { }
|
|
175
|
+
this.openDbs.delete(id);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
open(id, readonlyOverride) {
|
|
179
|
+
const target = this.pathById.get(id);
|
|
180
|
+
if (!target)
|
|
181
|
+
throw new Error(`Database "${id}" is not registered.`);
|
|
182
|
+
const isRo = this.readonlyMode || (readonlyOverride !== undefined ? readonlyOverride : (this.readonlyById.get(id) ?? false));
|
|
183
|
+
let db = this.openDbs.get(id);
|
|
184
|
+
if (db && db.isReadOnly !== isRo) {
|
|
185
|
+
try {
|
|
186
|
+
db.close();
|
|
187
|
+
}
|
|
188
|
+
catch { }
|
|
189
|
+
this.openDbs.delete(id);
|
|
190
|
+
db = undefined;
|
|
191
|
+
}
|
|
192
|
+
if (!db) {
|
|
193
|
+
const childLogger = this.logger.child(`db:${id}`);
|
|
194
|
+
db = (0, common_1.isPostgresConnectionString)(target)
|
|
195
|
+
? new postgres_1.PostgresDatabase(target, childLogger, { readonly: isRo })
|
|
196
|
+
: new database_1.SqliteDatabase(target, childLogger, { readonly: isRo });
|
|
197
|
+
this.openDbs.set(id, db);
|
|
198
|
+
}
|
|
199
|
+
return db;
|
|
200
|
+
}
|
|
201
|
+
openFile(absPath, readonly) {
|
|
202
|
+
const abs = node_path_1.default.resolve(String(absPath ?? '').trim());
|
|
203
|
+
if (!abs)
|
|
204
|
+
throw new Error('Database path is required.');
|
|
205
|
+
if (!(0, node_fs_1.existsSync)(abs) || !(0, node_fs_1.statSync)(abs).isFile())
|
|
206
|
+
throw new Error(`Not a file: ${abs}`);
|
|
207
|
+
const existing = this.idByPath.get(abs);
|
|
208
|
+
if (existing) {
|
|
209
|
+
if (readonly !== undefined)
|
|
210
|
+
this.setReadonly(existing, readonly);
|
|
211
|
+
return existing;
|
|
212
|
+
}
|
|
213
|
+
this.addFile(abs);
|
|
214
|
+
const id = this.idByPath.get(abs);
|
|
215
|
+
if (!id)
|
|
216
|
+
throw new Error('Failed to register database file.');
|
|
217
|
+
if (readonly !== undefined)
|
|
218
|
+
this.readonlyById.set(id, readonly);
|
|
219
|
+
try {
|
|
220
|
+
this.open(id);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
this.pathById.delete(id);
|
|
224
|
+
this.idByPath.delete(abs);
|
|
225
|
+
this.nameById.delete(id);
|
|
226
|
+
this.readonlyById.delete(id);
|
|
227
|
+
this.files = this.files.filter((p) => p !== abs);
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
return id;
|
|
231
|
+
}
|
|
232
|
+
get(id) {
|
|
233
|
+
return this.openDbs.get(id);
|
|
234
|
+
}
|
|
235
|
+
async countTables(id) {
|
|
236
|
+
try {
|
|
237
|
+
const r = await this.open(id).listTables();
|
|
238
|
+
return r.success ? (r.data?.length ?? 0) : 0;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
create(name) {
|
|
245
|
+
if (this.readonlyMode)
|
|
246
|
+
throw new Error('Read-only mode — creating databases is disabled.');
|
|
247
|
+
const fileName = sanitizeFileName(name);
|
|
248
|
+
if (!fileName)
|
|
249
|
+
throw new Error('Database name is required.');
|
|
250
|
+
const abs = node_path_1.default.join(this.createDir, fileName);
|
|
251
|
+
this.addFile(abs);
|
|
252
|
+
const id = this.idByPath.get(abs);
|
|
253
|
+
if (!id)
|
|
254
|
+
throw new Error('Failed to register new database.');
|
|
255
|
+
this.open(id);
|
|
256
|
+
return id;
|
|
257
|
+
}
|
|
258
|
+
addConnection(name, connectionString, readonly) {
|
|
259
|
+
if (this.readonlyMode)
|
|
260
|
+
throw new Error('Read-only mode — adding connections is disabled.');
|
|
261
|
+
const raw = String(connectionString ?? '').trim();
|
|
262
|
+
if (!raw)
|
|
263
|
+
throw new Error('Connection string is required.');
|
|
264
|
+
let cleanName = String(name ?? '').trim();
|
|
265
|
+
if (!cleanName) {
|
|
266
|
+
try {
|
|
267
|
+
cleanName = new URL(raw).pathname.replace(/^\//, '') || 'postgres';
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
cleanName = 'postgres';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
this.addNamedTarget(cleanName, raw);
|
|
274
|
+
const id = this.idByPath.get(raw);
|
|
275
|
+
if (!id)
|
|
276
|
+
throw new Error('Failed to register connection.');
|
|
277
|
+
if (readonly !== undefined)
|
|
278
|
+
this.readonlyById.set(id, readonly);
|
|
279
|
+
return id;
|
|
280
|
+
}
|
|
281
|
+
remove(id) {
|
|
282
|
+
if (this.readonlyMode)
|
|
283
|
+
throw new Error('Read-only mode — deleting databases is disabled.');
|
|
284
|
+
const target = this.pathById.get(id);
|
|
285
|
+
if (!target)
|
|
286
|
+
return;
|
|
287
|
+
const db = this.openDbs.get(id);
|
|
288
|
+
if (db) {
|
|
289
|
+
try {
|
|
290
|
+
db.close();
|
|
291
|
+
}
|
|
292
|
+
catch { }
|
|
293
|
+
this.openDbs.delete(id);
|
|
294
|
+
}
|
|
295
|
+
this.pathById.delete(id);
|
|
296
|
+
this.idByPath.delete(target);
|
|
297
|
+
this.nameById.delete(id);
|
|
298
|
+
this.readonlyById.delete(id);
|
|
299
|
+
this.files = this.files.filter((p) => p !== target);
|
|
300
|
+
if (!(0, common_1.isPostgresConnectionString)(target)) {
|
|
301
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
302
|
+
try {
|
|
303
|
+
(0, node_fs_1.unlinkSync)(target + suffix);
|
|
304
|
+
}
|
|
305
|
+
catch { }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
closeAll() {
|
|
310
|
+
for (const db of this.openDbs.values()) {
|
|
311
|
+
try {
|
|
312
|
+
db.close();
|
|
313
|
+
}
|
|
314
|
+
catch { }
|
|
315
|
+
}
|
|
316
|
+
this.openDbs.clear();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
exports.DbManager = DbManager;
|
|
320
|
+
function sanitizeFileName(name) {
|
|
321
|
+
let n = String(name ?? '').trim().replace(/[\\/]/g, '');
|
|
322
|
+
if (!n)
|
|
323
|
+
return '';
|
|
324
|
+
if (!DB_EXTENSIONS.some((ext) => n.toLowerCase().endsWith(ext)))
|
|
325
|
+
n += '.db';
|
|
326
|
+
return n;
|
|
327
|
+
}
|