midql-cli 0.1.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/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/App-W6AAY42M.js +568 -0
- package/dist/chunk-3CIK5I4M.js +396 -0
- package/dist/chunk-3FPZQHHV.js +77 -0
- package/dist/chunk-3QAR6XBK.js +200 -0
- package/dist/chunk-7WNCISNV.js +106 -0
- package/dist/chunk-ATZPL4EX.js +35 -0
- package/dist/chunk-BAAEZXFH.js +73 -0
- package/dist/chunk-GWY47EDH.js +11 -0
- package/dist/chunk-NCN3ZBOJ.js +93 -0
- package/dist/chunk-PXDMSYWH.js +18 -0
- package/dist/chunk-TLTYYFT5.js +237 -0
- package/dist/chunk-VFC3HWTF.js +52 -0
- package/dist/chunk-WN6T44OZ.js +1643 -0
- package/dist/chunk-XJAN6OQ7.js +25 -0
- package/dist/cli.js +101 -0
- package/dist/controller-HDNDKU6K.js +18 -0
- package/dist/csv-F2MFR3XB.js +7 -0
- package/dist/dump-LQ7ULAAL.js +11 -0
- package/dist/execute-E45HSHNE.js +155 -0
- package/dist/humanize-UG6KBAFO.js +100 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +55 -0
- package/dist/json-Z56V7OEB.js +7 -0
- package/dist/maintenance-QHIUTVV3.js +49 -0
- package/dist/meta-NZ5Z6NYN.js +369 -0
- package/dist/mysql-GQSXUUEK.js +204 -0
- package/dist/profiles-4QGMCJAO.js +67 -0
- package/dist/query-RCUXCOAF.js +122 -0
- package/dist/restore-4QXG2WRR.js +9 -0
- package/package.json +74 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getProfile,
|
|
4
|
+
resolvePassword
|
|
5
|
+
} from "./chunk-3QAR6XBK.js";
|
|
6
|
+
import {
|
|
7
|
+
trackTransactionState
|
|
8
|
+
} from "./chunk-PXDMSYWH.js";
|
|
9
|
+
import {
|
|
10
|
+
ConnectionError,
|
|
11
|
+
ExecutionError
|
|
12
|
+
} from "./chunk-VFC3HWTF.js";
|
|
13
|
+
|
|
14
|
+
// src/adapters/introspect/postgres.ts
|
|
15
|
+
var columnsQuery = `
|
|
16
|
+
SELECT c.table_name, c.column_name, c.data_type, c.is_nullable, c.column_default
|
|
17
|
+
FROM information_schema.columns c
|
|
18
|
+
JOIN information_schema.tables t
|
|
19
|
+
ON t.table_schema = c.table_schema AND t.table_name = c.table_name
|
|
20
|
+
WHERE c.table_schema = 'public' AND t.table_type = 'BASE TABLE'
|
|
21
|
+
ORDER BY c.table_name, c.ordinal_position`;
|
|
22
|
+
var primaryKeysQuery = `
|
|
23
|
+
SELECT rel.relname AS table_name, att.attname AS column_name
|
|
24
|
+
FROM pg_constraint con
|
|
25
|
+
JOIN pg_class rel ON rel.oid = con.conrelid
|
|
26
|
+
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
|
|
27
|
+
JOIN unnest(con.conkey) WITH ORDINALITY AS cols(attnum, ord) ON true
|
|
28
|
+
JOIN pg_attribute att ON att.attrelid = rel.oid AND att.attnum = cols.attnum
|
|
29
|
+
WHERE con.contype = 'p' AND nsp.nspname = 'public'
|
|
30
|
+
ORDER BY rel.relname, cols.ord`;
|
|
31
|
+
var foreignKeysQuery = `
|
|
32
|
+
SELECT
|
|
33
|
+
con.conname AS constraint_name,
|
|
34
|
+
rel.relname AS table_name,
|
|
35
|
+
att.attname AS column_name,
|
|
36
|
+
frel.relname AS referenced_table,
|
|
37
|
+
fatt.attname AS referenced_column,
|
|
38
|
+
cols.ord
|
|
39
|
+
FROM pg_constraint con
|
|
40
|
+
JOIN pg_class rel ON rel.oid = con.conrelid
|
|
41
|
+
JOIN pg_class frel ON frel.oid = con.confrelid
|
|
42
|
+
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
|
|
43
|
+
JOIN unnest(con.conkey) WITH ORDINALITY AS cols(attnum, ord) ON true
|
|
44
|
+
JOIN unnest(con.confkey) WITH ORDINALITY AS fcols(attnum, ord) ON fcols.ord = cols.ord
|
|
45
|
+
JOIN pg_attribute att ON att.attrelid = rel.oid AND att.attnum = cols.attnum
|
|
46
|
+
JOIN pg_attribute fatt ON fatt.attrelid = frel.oid AND fatt.attnum = fcols.attnum
|
|
47
|
+
WHERE con.contype = 'f' AND nsp.nspname = 'public'
|
|
48
|
+
ORDER BY rel.relname, con.conname, cols.ord`;
|
|
49
|
+
var rowCountsQuery = `
|
|
50
|
+
SELECT relname AS table_name, GREATEST(reltuples, 0)::bigint AS approx_rows
|
|
51
|
+
FROM pg_class rel
|
|
52
|
+
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
|
|
53
|
+
WHERE nsp.nspname = 'public' AND rel.relkind = 'r'`;
|
|
54
|
+
function classifyPostgresType(dataType) {
|
|
55
|
+
const type = dataType.toLowerCase();
|
|
56
|
+
if (type.includes("int") || type.includes("numeric") || type.includes("decimal")) {
|
|
57
|
+
return "number";
|
|
58
|
+
}
|
|
59
|
+
if (type.includes("double") || type.includes("real") || type.includes("money") || type.includes("serial")) {
|
|
60
|
+
return "number";
|
|
61
|
+
}
|
|
62
|
+
if (type.includes("bool")) {
|
|
63
|
+
return "boolean";
|
|
64
|
+
}
|
|
65
|
+
if (type.includes("timestamp") || type.includes("date") || type.includes("time")) {
|
|
66
|
+
return "date";
|
|
67
|
+
}
|
|
68
|
+
if (type.includes("json")) {
|
|
69
|
+
return "json";
|
|
70
|
+
}
|
|
71
|
+
if (type.includes("uuid")) {
|
|
72
|
+
return "uuid";
|
|
73
|
+
}
|
|
74
|
+
if (type.includes("char") || type.includes("text") || type.includes("citext")) {
|
|
75
|
+
return "text";
|
|
76
|
+
}
|
|
77
|
+
return "other";
|
|
78
|
+
}
|
|
79
|
+
function assembleSchema(columnRows, primaryKeyRows, foreignKeyRows, rowCountRows) {
|
|
80
|
+
const primaryKeys = /* @__PURE__ */ new Map();
|
|
81
|
+
for (const row of primaryKeyRows) {
|
|
82
|
+
const existing = primaryKeys.get(row.table_name) ?? [];
|
|
83
|
+
existing.push(row.column_name);
|
|
84
|
+
primaryKeys.set(row.table_name, existing);
|
|
85
|
+
}
|
|
86
|
+
const foreignKeys = /* @__PURE__ */ new Map();
|
|
87
|
+
for (const row of foreignKeyRows) {
|
|
88
|
+
const tableFks = foreignKeys.get(row.table_name) ?? /* @__PURE__ */ new Map();
|
|
89
|
+
const fk = tableFks.get(row.constraint_name) ?? {
|
|
90
|
+
columns: [],
|
|
91
|
+
referencedTable: row.referenced_table,
|
|
92
|
+
referencedColumns: []
|
|
93
|
+
};
|
|
94
|
+
fk.columns.push(row.column_name);
|
|
95
|
+
fk.referencedColumns.push(row.referenced_column);
|
|
96
|
+
tableFks.set(row.constraint_name, fk);
|
|
97
|
+
foreignKeys.set(row.table_name, tableFks);
|
|
98
|
+
}
|
|
99
|
+
const rowCounts = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const row of rowCountRows) {
|
|
101
|
+
rowCounts.set(row.table_name, Number(row.approx_rows));
|
|
102
|
+
}
|
|
103
|
+
const tables = /* @__PURE__ */ new Map();
|
|
104
|
+
for (const row of columnRows) {
|
|
105
|
+
const pk = primaryKeys.get(row.table_name) ?? [];
|
|
106
|
+
const table = tables.get(row.table_name) ?? {
|
|
107
|
+
name: row.table_name,
|
|
108
|
+
columns: [],
|
|
109
|
+
primaryKey: pk,
|
|
110
|
+
foreignKeys: [...foreignKeys.get(row.table_name)?.values() ?? []],
|
|
111
|
+
approxRowCount: rowCounts.get(row.table_name) ?? 0
|
|
112
|
+
};
|
|
113
|
+
const column = {
|
|
114
|
+
name: row.column_name,
|
|
115
|
+
dataType: row.data_type,
|
|
116
|
+
kind: classifyPostgresType(row.data_type),
|
|
117
|
+
nullable: row.is_nullable === "YES",
|
|
118
|
+
isPrimaryKey: pk.includes(row.column_name),
|
|
119
|
+
hasDefault: row.column_default !== null
|
|
120
|
+
};
|
|
121
|
+
table.columns.push(column);
|
|
122
|
+
tables.set(row.table_name, table);
|
|
123
|
+
}
|
|
124
|
+
return { tables: [...tables.values()] };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/adapters/postgres.ts
|
|
128
|
+
var PostgresAdapter = class {
|
|
129
|
+
dialect = "postgres";
|
|
130
|
+
database;
|
|
131
|
+
options;
|
|
132
|
+
pool = null;
|
|
133
|
+
sessionClient = null;
|
|
134
|
+
transactionOpen = false;
|
|
135
|
+
constructor(options) {
|
|
136
|
+
this.options = options;
|
|
137
|
+
this.database = options.database;
|
|
138
|
+
}
|
|
139
|
+
async connect() {
|
|
140
|
+
const { Pool, Client } = (await import("pg")).default ?? await import("pg");
|
|
141
|
+
const config = {
|
|
142
|
+
host: this.options.host,
|
|
143
|
+
port: this.options.port,
|
|
144
|
+
database: this.options.database,
|
|
145
|
+
user: this.options.user,
|
|
146
|
+
password: this.options.password ?? void 0,
|
|
147
|
+
ssl: this.options.ssl ? { rejectUnauthorized: false } : void 0,
|
|
148
|
+
max: 3
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
this.pool = new Pool(config);
|
|
152
|
+
this.sessionClient = new Client(config);
|
|
153
|
+
await this.sessionClient.connect();
|
|
154
|
+
await this.pool.query("SELECT 1");
|
|
155
|
+
} catch (error) {
|
|
156
|
+
await this.close();
|
|
157
|
+
throw new ConnectionError(
|
|
158
|
+
`Could not connect to postgres://${this.options.user}@${this.options.host}:${this.options.port}/${this.options.database}: ${error.message}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async query(sql, params = []) {
|
|
163
|
+
if (!this.pool) {
|
|
164
|
+
throw new ConnectionError("Not connected");
|
|
165
|
+
}
|
|
166
|
+
return this.run(() => this.pool.query(sql, params), sql);
|
|
167
|
+
}
|
|
168
|
+
async sessionQuery(sql, params = []) {
|
|
169
|
+
if (!this.sessionClient) {
|
|
170
|
+
throw new ConnectionError("Not connected");
|
|
171
|
+
}
|
|
172
|
+
const result = await this.run(
|
|
173
|
+
() => this.sessionClient.query(sql, params),
|
|
174
|
+
sql
|
|
175
|
+
);
|
|
176
|
+
this.transactionOpen = trackTransactionState(sql, this.transactionOpen);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
async run(execute, sql) {
|
|
180
|
+
const started = performance.now();
|
|
181
|
+
let result;
|
|
182
|
+
try {
|
|
183
|
+
result = await execute();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throw new ExecutionError(error.message);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
columns: result.fields?.map((field) => field.name) ?? [],
|
|
189
|
+
rows: result.rows ?? [],
|
|
190
|
+
rowCount: result.rowCount ?? result.rows?.length ?? 0,
|
|
191
|
+
durationMs: Math.round(performance.now() - started),
|
|
192
|
+
command: result.command ?? sql.trim().split(/\s+/)[0]?.toUpperCase() ?? ""
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async introspect() {
|
|
196
|
+
const [columns, primaryKeys, foreignKeys, rowCounts] = await Promise.all([
|
|
197
|
+
this.query(columnsQuery),
|
|
198
|
+
this.query(primaryKeysQuery),
|
|
199
|
+
this.query(foreignKeysQuery),
|
|
200
|
+
this.query(rowCountsQuery)
|
|
201
|
+
]);
|
|
202
|
+
return assembleSchema(
|
|
203
|
+
columns.rows,
|
|
204
|
+
primaryKeys.rows,
|
|
205
|
+
foreignKeys.rows,
|
|
206
|
+
rowCounts.rows
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
inTransaction() {
|
|
210
|
+
return this.transactionOpen;
|
|
211
|
+
}
|
|
212
|
+
async close() {
|
|
213
|
+
if (this.sessionClient) {
|
|
214
|
+
try {
|
|
215
|
+
await this.sessionClient.end();
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
this.sessionClient = null;
|
|
219
|
+
}
|
|
220
|
+
if (this.pool) {
|
|
221
|
+
try {
|
|
222
|
+
await this.pool.end();
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
this.pool = null;
|
|
226
|
+
}
|
|
227
|
+
this.transactionOpen = false;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/connections/manager.ts
|
|
232
|
+
function parseConnectionUrl(url) {
|
|
233
|
+
let parsed;
|
|
234
|
+
try {
|
|
235
|
+
parsed = new URL(url);
|
|
236
|
+
} catch {
|
|
237
|
+
throw new ConnectionError(`Invalid connection URL: ${url}`);
|
|
238
|
+
}
|
|
239
|
+
const protocol = parsed.protocol.replace(":", "");
|
|
240
|
+
let dialect;
|
|
241
|
+
if (protocol === "postgres" || protocol === "postgresql") {
|
|
242
|
+
dialect = "postgres";
|
|
243
|
+
} else if (protocol === "mysql") {
|
|
244
|
+
dialect = "mysql";
|
|
245
|
+
} else {
|
|
246
|
+
throw new ConnectionError(`Unsupported protocol "${protocol}"; use postgres:// or mysql://`);
|
|
247
|
+
}
|
|
248
|
+
const database = parsed.pathname.replace(/^\//, "");
|
|
249
|
+
if (!database) {
|
|
250
|
+
throw new ConnectionError("Connection URL must include a database name");
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
dialect,
|
|
254
|
+
options: {
|
|
255
|
+
host: parsed.hostname || "localhost",
|
|
256
|
+
port: parsed.port ? Number(parsed.port) : dialect === "postgres" ? 5432 : 3306,
|
|
257
|
+
database,
|
|
258
|
+
user: decodeURIComponent(parsed.username || "postgres"),
|
|
259
|
+
password: parsed.password ? decodeURIComponent(parsed.password) : null,
|
|
260
|
+
ssl: parsed.searchParams.get("ssl") === "true" || parsed.searchParams.get("sslmode") === "require"
|
|
261
|
+
},
|
|
262
|
+
env: "dev",
|
|
263
|
+
readonly: false
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function targetFromProfile(profile) {
|
|
267
|
+
return {
|
|
268
|
+
dialect: profile.dialect,
|
|
269
|
+
options: {
|
|
270
|
+
host: profile.host,
|
|
271
|
+
port: profile.port,
|
|
272
|
+
database: profile.database,
|
|
273
|
+
user: profile.user,
|
|
274
|
+
password: resolvePassword(profile),
|
|
275
|
+
ssl: profile.ssl
|
|
276
|
+
},
|
|
277
|
+
env: profile.env,
|
|
278
|
+
readonly: profile.readonly
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function resolveTarget(profileNameOrUrl) {
|
|
282
|
+
if (profileNameOrUrl.includes("://")) {
|
|
283
|
+
return parseConnectionUrl(profileNameOrUrl);
|
|
284
|
+
}
|
|
285
|
+
const profile = getProfile(profileNameOrUrl);
|
|
286
|
+
if (!profile) {
|
|
287
|
+
throw new ConnectionError(
|
|
288
|
+
`No saved profile named "${profileNameOrUrl}". Use a connection URL or save a profile first.`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return targetFromProfile(profile);
|
|
292
|
+
}
|
|
293
|
+
async function createAdapter(target) {
|
|
294
|
+
if (target.dialect === "postgres") {
|
|
295
|
+
return new PostgresAdapter(target.options);
|
|
296
|
+
}
|
|
297
|
+
const { MysqlAdapter } = await import("./mysql-GQSXUUEK.js");
|
|
298
|
+
return new MysqlAdapter(target.options);
|
|
299
|
+
}
|
|
300
|
+
var ConnectionManager = class {
|
|
301
|
+
connections = /* @__PURE__ */ new Map();
|
|
302
|
+
activeAlias = null;
|
|
303
|
+
async connect(profileNameOrUrl, alias) {
|
|
304
|
+
const target = resolveTarget(profileNameOrUrl);
|
|
305
|
+
const resolvedAlias = alias ?? this.defaultAlias(profileNameOrUrl, target);
|
|
306
|
+
if (this.connections.has(resolvedAlias)) {
|
|
307
|
+
throw new ConnectionError(
|
|
308
|
+
`Alias "${resolvedAlias}" is already connected; use \\use ${resolvedAlias} or pick another alias`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const adapter = await createAdapter(target);
|
|
312
|
+
await adapter.connect();
|
|
313
|
+
const schema = await adapter.introspect();
|
|
314
|
+
const connection = {
|
|
315
|
+
alias: resolvedAlias,
|
|
316
|
+
adapter,
|
|
317
|
+
dialect: target.dialect,
|
|
318
|
+
database: target.options.database,
|
|
319
|
+
env: target.env,
|
|
320
|
+
readonly: target.readonly,
|
|
321
|
+
schema,
|
|
322
|
+
schemaLoadedAt: Date.now(),
|
|
323
|
+
options: target.options
|
|
324
|
+
};
|
|
325
|
+
this.connections.set(resolvedAlias, connection);
|
|
326
|
+
this.activeAlias = resolvedAlias;
|
|
327
|
+
return connection;
|
|
328
|
+
}
|
|
329
|
+
defaultAlias(profileNameOrUrl, target) {
|
|
330
|
+
const base = profileNameOrUrl.includes("://") ? target.options.database : profileNameOrUrl;
|
|
331
|
+
if (!this.connections.has(base)) {
|
|
332
|
+
return base;
|
|
333
|
+
}
|
|
334
|
+
let suffix = 2;
|
|
335
|
+
while (this.connections.has(`${base}${suffix}`)) {
|
|
336
|
+
suffix += 1;
|
|
337
|
+
}
|
|
338
|
+
return `${base}${suffix}`;
|
|
339
|
+
}
|
|
340
|
+
use(alias) {
|
|
341
|
+
const connection = this.connections.get(alias);
|
|
342
|
+
if (!connection) {
|
|
343
|
+
throw new ConnectionError(`No connection with alias "${alias}"`);
|
|
344
|
+
}
|
|
345
|
+
this.activeAlias = alias;
|
|
346
|
+
return connection;
|
|
347
|
+
}
|
|
348
|
+
async disconnect(alias) {
|
|
349
|
+
const connection = this.connections.get(alias);
|
|
350
|
+
if (!connection) {
|
|
351
|
+
throw new ConnectionError(`No connection with alias "${alias}"`);
|
|
352
|
+
}
|
|
353
|
+
await connection.adapter.close();
|
|
354
|
+
this.connections.delete(alias);
|
|
355
|
+
if (this.activeAlias === alias) {
|
|
356
|
+
this.activeAlias = this.connections.keys().next().value ?? null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
list() {
|
|
360
|
+
return [...this.connections.values()];
|
|
361
|
+
}
|
|
362
|
+
active() {
|
|
363
|
+
if (!this.activeAlias) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
return this.connections.get(this.activeAlias) ?? null;
|
|
367
|
+
}
|
|
368
|
+
async refreshSchema(alias) {
|
|
369
|
+
const connection = alias ? this.connections.get(alias) : this.active();
|
|
370
|
+
if (!connection) {
|
|
371
|
+
throw new ConnectionError("No active connection");
|
|
372
|
+
}
|
|
373
|
+
connection.schema = await connection.adapter.introspect();
|
|
374
|
+
connection.schemaLoadedAt = Date.now();
|
|
375
|
+
return connection;
|
|
376
|
+
}
|
|
377
|
+
setReadonly(readonly) {
|
|
378
|
+
const connection = this.active();
|
|
379
|
+
if (!connection) {
|
|
380
|
+
throw new ConnectionError("No active connection");
|
|
381
|
+
}
|
|
382
|
+
connection.readonly = readonly;
|
|
383
|
+
return connection;
|
|
384
|
+
}
|
|
385
|
+
async closeAll() {
|
|
386
|
+
await Promise.all(this.list().map((connection) => connection.adapter.close()));
|
|
387
|
+
this.connections.clear();
|
|
388
|
+
this.activeAlias = null;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
export {
|
|
393
|
+
PostgresAdapter,
|
|
394
|
+
resolveTarget,
|
|
395
|
+
ConnectionManager
|
|
396
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
findTool,
|
|
4
|
+
runTool
|
|
5
|
+
} from "./chunk-NCN3ZBOJ.js";
|
|
6
|
+
import {
|
|
7
|
+
ExecutionError
|
|
8
|
+
} from "./chunk-VFC3HWTF.js";
|
|
9
|
+
|
|
10
|
+
// src/backup/restore.ts
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
async function restoreDatabase(dialect, options, file, tools) {
|
|
13
|
+
if (!existsSync(file)) {
|
|
14
|
+
throw new ExecutionError(`Backup file not found: ${file}`);
|
|
15
|
+
}
|
|
16
|
+
if (dialect === "postgres") {
|
|
17
|
+
await restorePostgres(options, file, tools);
|
|
18
|
+
} else {
|
|
19
|
+
await restoreMysql(options, file, tools);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function restorePostgres(options, file, tools) {
|
|
23
|
+
const env = { PGPASSWORD: options.password ?? void 0 };
|
|
24
|
+
const connectionArgs = [
|
|
25
|
+
"--no-password",
|
|
26
|
+
"-h",
|
|
27
|
+
options.host,
|
|
28
|
+
"-p",
|
|
29
|
+
String(options.port),
|
|
30
|
+
"-U",
|
|
31
|
+
options.user,
|
|
32
|
+
"-d",
|
|
33
|
+
options.database
|
|
34
|
+
];
|
|
35
|
+
if (file.endsWith(".sql")) {
|
|
36
|
+
const binary2 = await findTool("psql", tools.psqlPath);
|
|
37
|
+
const run2 = await runTool(binary2, [...connectionArgs, "-f", file], env, {});
|
|
38
|
+
if (run2.code !== 0) {
|
|
39
|
+
throw new ExecutionError(
|
|
40
|
+
`psql restore failed: ${run2.stderr.trim() || `exit code ${run2.code}`}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const binary = await findTool("pg_restore", tools.pgRestorePath);
|
|
46
|
+
const run = await runTool(binary, [...connectionArgs, "--clean", "--if-exists", file], env, {});
|
|
47
|
+
if (run.code !== 0) {
|
|
48
|
+
throw new ExecutionError(`pg_restore failed: ${run.stderr.trim() || `exit code ${run.code}`}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function restoreMysql(options, file, tools) {
|
|
52
|
+
const binary = await findTool("mysql", tools.mysqlPath);
|
|
53
|
+
const args = [
|
|
54
|
+
"-h",
|
|
55
|
+
options.host,
|
|
56
|
+
"-P",
|
|
57
|
+
String(options.port),
|
|
58
|
+
"-u",
|
|
59
|
+
options.user,
|
|
60
|
+
options.database
|
|
61
|
+
];
|
|
62
|
+
const run = await runTool(
|
|
63
|
+
binary,
|
|
64
|
+
args,
|
|
65
|
+
{ MYSQL_PWD: options.password ?? void 0 },
|
|
66
|
+
{ fromFile: file }
|
|
67
|
+
);
|
|
68
|
+
if (run.code !== 0) {
|
|
69
|
+
throw new ExecutionError(
|
|
70
|
+
`mysql restore failed: ${run.stderr.trim() || `exit code ${run.code}`}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
restoreDatabase
|
|
77
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ConfigError
|
|
4
|
+
} from "./chunk-VFC3HWTF.js";
|
|
5
|
+
|
|
6
|
+
// src/config/config.ts
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
+
|
|
9
|
+
// src/config/paths.ts
|
|
10
|
+
import { mkdirSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
function configDir() {
|
|
14
|
+
return process.env.MIDQL_HOME ?? join(homedir(), ".midql");
|
|
15
|
+
}
|
|
16
|
+
function configFilePath() {
|
|
17
|
+
return join(configDir(), "config.json");
|
|
18
|
+
}
|
|
19
|
+
function keyFilePath() {
|
|
20
|
+
return join(configDir(), ".key");
|
|
21
|
+
}
|
|
22
|
+
function historyFilePath() {
|
|
23
|
+
return join(configDir(), "history.jsonl");
|
|
24
|
+
}
|
|
25
|
+
function ensureConfigDir() {
|
|
26
|
+
const dir = configDir();
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/config/config.ts
|
|
32
|
+
var defaultTools = {
|
|
33
|
+
pgDumpPath: null,
|
|
34
|
+
pgRestorePath: null,
|
|
35
|
+
psqlPath: null,
|
|
36
|
+
mysqldumpPath: null,
|
|
37
|
+
mysqlPath: null
|
|
38
|
+
};
|
|
39
|
+
function defaultConfig() {
|
|
40
|
+
return {
|
|
41
|
+
profiles: [],
|
|
42
|
+
safety: "on",
|
|
43
|
+
maxJoinDepth: 3,
|
|
44
|
+
tools: { ...defaultTools }
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
var environmentTags = ["dev", "staging", "prod"];
|
|
48
|
+
function validateProfile(raw, index) {
|
|
49
|
+
if (typeof raw !== "object" || raw === null) {
|
|
50
|
+
throw new ConfigError(`Profile at index ${index} is not an object`);
|
|
51
|
+
}
|
|
52
|
+
const p = raw;
|
|
53
|
+
if (typeof p.name !== "string" || p.name.length === 0) {
|
|
54
|
+
throw new ConfigError(`Profile at index ${index} is missing a name`);
|
|
55
|
+
}
|
|
56
|
+
if (p.dialect !== "postgres" && p.dialect !== "mysql") {
|
|
57
|
+
throw new ConfigError(`Profile "${p.name}" has invalid dialect "${String(p.dialect)}"`);
|
|
58
|
+
}
|
|
59
|
+
if (typeof p.host !== "string" || typeof p.database !== "string" || typeof p.user !== "string") {
|
|
60
|
+
throw new ConfigError(`Profile "${p.name}" is missing host, database, or user`);
|
|
61
|
+
}
|
|
62
|
+
const env = environmentTags.includes(p.env) ? p.env : "dev";
|
|
63
|
+
return {
|
|
64
|
+
name: p.name,
|
|
65
|
+
dialect: p.dialect,
|
|
66
|
+
host: p.host,
|
|
67
|
+
port: typeof p.port === "number" ? p.port : p.dialect === "postgres" ? 5432 : 3306,
|
|
68
|
+
database: p.database,
|
|
69
|
+
user: p.user,
|
|
70
|
+
encryptedPassword: typeof p.encryptedPassword === "string" ? p.encryptedPassword : null,
|
|
71
|
+
passwordEnv: typeof p.passwordEnv === "string" ? p.passwordEnv : null,
|
|
72
|
+
env,
|
|
73
|
+
readonly: p.readonly === true,
|
|
74
|
+
ssl: p.ssl === true
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function loadConfig() {
|
|
78
|
+
const path = configFilePath();
|
|
79
|
+
if (!existsSync(path)) {
|
|
80
|
+
return defaultConfig();
|
|
81
|
+
}
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new ConfigError(`Failed to parse ${path}: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
89
|
+
throw new ConfigError(`${path} must contain a JSON object`);
|
|
90
|
+
}
|
|
91
|
+
const raw = parsed;
|
|
92
|
+
const profiles = Array.isArray(raw.profiles) ? raw.profiles.map((entry, index) => validateProfile(entry, index)) : [];
|
|
93
|
+
const tools = typeof raw.tools === "object" && raw.tools !== null ? { ...defaultTools, ...raw.tools } : { ...defaultTools };
|
|
94
|
+
return {
|
|
95
|
+
profiles,
|
|
96
|
+
safety: raw.safety === "off" ? "off" : "on",
|
|
97
|
+
maxJoinDepth: typeof raw.maxJoinDepth === "number" ? raw.maxJoinDepth : 3,
|
|
98
|
+
tools
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function saveConfig(config) {
|
|
102
|
+
ensureConfigDir();
|
|
103
|
+
writeFileSync(configFilePath(), `${JSON.stringify(config, null, 2)}
|
|
104
|
+
`, "utf8");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/connections/secrets.ts
|
|
108
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
109
|
+
import { chmodSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
110
|
+
var KEY_LENGTH = 32;
|
|
111
|
+
var IV_LENGTH = 12;
|
|
112
|
+
function loadOrCreateKey() {
|
|
113
|
+
const path = keyFilePath();
|
|
114
|
+
if (existsSync2(path)) {
|
|
115
|
+
const key2 = Buffer.from(readFileSync2(path, "utf8").trim(), "base64");
|
|
116
|
+
if (key2.length !== KEY_LENGTH) {
|
|
117
|
+
throw new ConfigError(`Key file ${path} is corrupted; delete it to generate a new key`);
|
|
118
|
+
}
|
|
119
|
+
return key2;
|
|
120
|
+
}
|
|
121
|
+
ensureConfigDir();
|
|
122
|
+
const key = randomBytes(KEY_LENGTH);
|
|
123
|
+
writeFileSync2(path, key.toString("base64"), { mode: 384 });
|
|
124
|
+
try {
|
|
125
|
+
chmodSync(path, 384);
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
return key;
|
|
129
|
+
}
|
|
130
|
+
function encryptSecret(plaintext) {
|
|
131
|
+
const key = loadOrCreateKey();
|
|
132
|
+
const iv = randomBytes(IV_LENGTH);
|
|
133
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
134
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
135
|
+
const tag = cipher.getAuthTag();
|
|
136
|
+
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
|
137
|
+
}
|
|
138
|
+
function decryptSecret(payload) {
|
|
139
|
+
const key = loadOrCreateKey();
|
|
140
|
+
const raw = Buffer.from(payload, "base64");
|
|
141
|
+
const iv = raw.subarray(0, IV_LENGTH);
|
|
142
|
+
const tag = raw.subarray(IV_LENGTH, IV_LENGTH + 16);
|
|
143
|
+
const encrypted = raw.subarray(IV_LENGTH + 16);
|
|
144
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
145
|
+
decipher.setAuthTag(tag);
|
|
146
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/connections/profiles.ts
|
|
150
|
+
function listProfiles() {
|
|
151
|
+
return loadConfig().profiles;
|
|
152
|
+
}
|
|
153
|
+
function getProfile(name) {
|
|
154
|
+
return loadConfig().profiles.find((profile) => profile.name === name) ?? null;
|
|
155
|
+
}
|
|
156
|
+
function saveProfile(profile, password) {
|
|
157
|
+
const config = loadConfig();
|
|
158
|
+
const stored = {
|
|
159
|
+
...profile,
|
|
160
|
+
encryptedPassword: password ? encryptSecret(password) : profile.encryptedPassword
|
|
161
|
+
};
|
|
162
|
+
const index = config.profiles.findIndex((existing) => existing.name === profile.name);
|
|
163
|
+
if (index >= 0) {
|
|
164
|
+
config.profiles[index] = stored;
|
|
165
|
+
} else {
|
|
166
|
+
config.profiles.push(stored);
|
|
167
|
+
}
|
|
168
|
+
saveConfig(config);
|
|
169
|
+
}
|
|
170
|
+
function deleteProfile(name) {
|
|
171
|
+
const config = loadConfig();
|
|
172
|
+
const index = config.profiles.findIndex((profile) => profile.name === name);
|
|
173
|
+
if (index < 0) {
|
|
174
|
+
throw new ConfigError(`Profile "${name}" not found`);
|
|
175
|
+
}
|
|
176
|
+
config.profiles.splice(index, 1);
|
|
177
|
+
saveConfig(config);
|
|
178
|
+
}
|
|
179
|
+
function resolvePassword(profile) {
|
|
180
|
+
if (profile.passwordEnv) {
|
|
181
|
+
const value = process.env[profile.passwordEnv];
|
|
182
|
+
if (value) {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (profile.encryptedPassword) {
|
|
187
|
+
return decryptSecret(profile.encryptedPassword);
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export {
|
|
193
|
+
historyFilePath,
|
|
194
|
+
loadConfig,
|
|
195
|
+
listProfiles,
|
|
196
|
+
getProfile,
|
|
197
|
+
saveProfile,
|
|
198
|
+
deleteProfile,
|
|
199
|
+
resolvePassword
|
|
200
|
+
};
|