hive-am 1.3.0 → 1.5.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/CHANGELOG.md +47 -1
- package/README.md +20 -5
- package/bin/hive-am.mjs +242 -31
- package/bin/run.mjs +135 -0
- package/package.json +1 -1
- package/server/dist/connections/manager.js +15 -3
- package/server/dist/connections/router.js +2 -2
- package/server/dist/connections/secrets.js +8 -0
- package/server/dist/db/migrations.js +68 -0
- package/server/dist/db.js +11 -10
- package/server/dist/efforts.js +105 -0
- package/server/dist/http/routes/agents.js +4 -3
- package/server/dist/http/routes/meta.js +12 -0
- package/server/dist/http/routes/notebook.js +10 -2
- package/server/dist/http/routes/resources.js +3 -0
- package/server/dist/http/routes/types.js +18 -1
- package/server/dist/http/validate.js +5 -2
- package/server/dist/index.js +4 -1
- package/server/dist/instructions.js +10 -2
- package/server/dist/objects/agent-db.js +258 -0
- package/server/dist/objects/database.js +496 -34
- package/server/dist/objects/index.js +20 -14
- package/server/dist/objects/runner.js +20 -0
- package/server/dist/objects/supervisor.js +2 -2
- package/server/dist/providers/kiro.js +7 -1
- package/server/dist/providers/opencode-server.js +11 -1
- package/server/dist/providers/opencode.js +4 -3
- package/server/dist/providers/spawn.js +38 -4
- package/server/dist/retention.js +50 -0
- package/server/dist/runtime.js +52 -21
- package/server/dist/services/agents.js +53 -1
- package/server/dist/services/notebook.js +7 -3
- package/server/dist/skills/defaults.js +35 -1
- package/server/dist/skills/notebook.js +57 -13
- package/server/mcp/dispatch.mjs +68 -0
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-build-manifest.json +79 -70
- package/web/.next/app-path-routes-manifest.json +9 -8
- package/web/.next/build-manifest.json +2 -2
- package/web/.next/prerender-manifest.json +71 -47
- package/web/.next/routes-manifest.json +6 -0
- package/web/.next/server/app/_not-found/page.js +1 -1
- package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/server/app/_not-found.rsc +3 -3
- package/web/.next/server/app/agents/[id]/page.js +4 -4
- package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents/page.js +2 -2
- package/web/.next/server/app/agents/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents.html +1 -1
- package/web/.next/server/app/agents.rsc +4 -4
- package/web/.next/server/app/chat/page.js +2 -0
- package/web/.next/server/app/chat/page.js.nft.json +1 -0
- package/web/.next/server/app/chat/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/chat.html +1 -0
- package/web/.next/server/app/chat.meta +15 -0
- package/web/.next/server/app/chat.rsc +25 -0
- package/web/.next/server/app/connections/page.js +1 -1
- package/web/.next/server/app/connections/page.js.nft.json +1 -1
- package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/connections.html +1 -1
- package/web/.next/server/app/connections.rsc +4 -4
- package/web/.next/server/app/index.html +1 -1
- package/web/.next/server/app/index.rsc +4 -4
- package/web/.next/server/app/page.js +1 -1
- package/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations/page.js +2 -2
- package/web/.next/server/app/relations/page.js.nft.json +1 -1
- package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations.html +1 -1
- package/web/.next/server/app/relations.rsc +4 -4
- package/web/.next/server/app/resources/[id]/page.js +2 -2
- package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -1
- package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/resources/page.js +2 -2
- package/web/.next/server/app/resources/page.js.nft.json +1 -1
- package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/resources.html +1 -1
- package/web/.next/server/app/resources.rsc +4 -4
- package/web/.next/server/app/schedules/page.js +1 -1
- package/web/.next/server/app/schedules/page.js.nft.json +1 -1
- package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/schedules.html +1 -1
- package/web/.next/server/app/schedules.rsc +4 -4
- package/web/.next/server/app/sessions/page.js +2 -2
- package/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/sessions.html +1 -1
- package/web/.next/server/app/sessions.rsc +4 -4
- package/web/.next/server/app/skills/page.js +1 -1
- package/web/.next/server/app/skills/page.js.nft.json +1 -1
- package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/skills.html +1 -1
- package/web/.next/server/app/skills.rsc +4 -4
- package/web/.next/server/app/types/page.js +1 -1
- package/web/.next/server/app/types/page.js.nft.json +1 -1
- package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/types.html +1 -1
- package/web/.next/server/app/types.rsc +4 -4
- package/web/.next/server/app-paths-manifest.json +9 -8
- package/web/.next/server/chunks/143.js +1 -1
- package/web/.next/server/chunks/148.js +1 -0
- package/web/.next/server/chunks/213.js +6 -0
- package/web/.next/server/chunks/274.js +1 -1
- package/web/.next/server/chunks/368.js +1 -0
- package/web/.next/server/chunks/403.js +1 -0
- package/web/.next/server/chunks/{790.js → 435.js} +2 -2
- package/web/.next/server/chunks/505.js +1 -6
- package/web/.next/server/chunks/612.js +1 -1
- package/web/.next/server/pages/404.html +1 -1
- package/web/.next/server/pages/500.html +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/static/UDA414PFPu9ZROltNmjbX/_buildManifest.js +1 -0
- package/web/.next/static/chunks/{316-26da9cc233ba7557.js → 107-77dd87cc2ca4f941.js} +1 -1
- package/web/.next/static/chunks/281-30e32d932b8b5a75.js +1 -0
- package/web/.next/static/chunks/391-18768499e741112e.js +1 -0
- package/web/.next/static/chunks/517-b3b1c9044d2ef23c.js +1 -0
- package/web/.next/static/chunks/587-5b511f5f93910180.js +1 -0
- package/web/.next/static/chunks/606-14fb5aaa4d0ff22d.js +1 -0
- package/web/.next/static/chunks/656-f3cc78328603241a.js +1 -0
- package/web/.next/static/chunks/{705-780e48e55b0346bd.js → 705-ce86175dea00e6a9.js} +1 -1
- package/web/.next/static/chunks/757-03361e7a97a605a2.js +1 -0
- package/web/.next/static/chunks/app/agents/[id]/page-638e12ba3cb813c3.js +1 -0
- package/web/.next/static/chunks/app/agents/page-9da499db6d22731f.js +1 -0
- package/web/.next/static/chunks/app/chat/page-383bc24e72dab7fa.js +1 -0
- package/web/.next/static/chunks/app/connections/{page-eb11cfb1ffeab3ca.js → page-06bd2d7d9495126a.js} +1 -1
- package/web/.next/static/chunks/app/layout-fad968a635f9a4b3.js +1 -0
- package/web/.next/static/chunks/app/page-0062ebc30322b1ae.js +1 -0
- package/web/.next/static/chunks/app/relations/page-02710fe1f8a670a9.js +1 -0
- package/web/.next/static/chunks/app/resources/[id]/{page-bf05836f97248ca7.js → page-fc494e85b01e8cb7.js} +1 -1
- package/web/.next/static/chunks/app/resources/{page-800f774e5cacf4c9.js → page-61cccd8fdcc2f6c8.js} +1 -1
- package/web/.next/static/chunks/app/sessions/page-85638705fdc18174.js +1 -0
- package/web/.next/static/chunks/app/types/page-8e25f503bdcd72ad.js +1 -0
- package/web/.next/static/css/dfcaa19a8d46e60d.css +1 -0
- package/web/.next/trace +30 -29
- package/web/.next/types/app/chat/page.ts +84 -0
- package/web/.next/types/routes.d.ts +2 -1
- package/web/.next/types/validator.ts +9 -0
- package/web/.next/server/chunks/306.js +0 -1
- package/web/.next/server/chunks/451.js +0 -1
- package/web/.next/server/chunks/638.js +0 -1
- package/web/.next/server/chunks/802.js +0 -1
- package/web/.next/static/aSYzbHk1M5YeS_d3SQKTZ/_buildManifest.js +0 -1
- package/web/.next/static/chunks/281-d8a0dcfaa6d064e4.js +0 -1
- package/web/.next/static/chunks/291-23f314877fa39054.js +0 -1
- package/web/.next/static/chunks/304-681348099f874a30.js +0 -1
- package/web/.next/static/chunks/491-28f093772bd460ac.js +0 -1
- package/web/.next/static/chunks/517-373a80c8ba432bdc.js +0 -1
- package/web/.next/static/chunks/562-7c2e33d838e869bf.js +0 -1
- package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +0 -1
- package/web/.next/static/chunks/844-519177e6198ca84f.js +0 -1
- package/web/.next/static/chunks/app/agents/[id]/page-c005161cec5527d8.js +0 -1
- package/web/.next/static/chunks/app/agents/page-9bb8cba69776c719.js +0 -1
- package/web/.next/static/chunks/app/layout-49b46c9b5aa512a8.js +0 -1
- package/web/.next/static/chunks/app/page-da042d146caeeab6.js +0 -1
- package/web/.next/static/chunks/app/relations/page-a44fd515a3b0525e.js +0 -1
- package/web/.next/static/chunks/app/sessions/page-a7534ebd90e982d6.js +0 -1
- package/web/.next/static/chunks/app/types/page-ebd37e220f77c1f3.js +0 -1
- package/web/.next/static/css/d42799b7e6a0e8a2.css +0 -1
- /package/web/.next/static/{aSYzbHk1M5YeS_d3SQKTZ → UDA414PFPu9ZROltNmjbX}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { agents, resolved } from '../db.js';
|
|
4
|
+
import { mcpCaps } from '../instructions.js';
|
|
5
|
+
import { dbColumns, dbDdl, dbDescribe, dbExec, dbExplain, dbKill, dbQuery, dbSample, dbSchema, dbSearch, dbSessions, dbTest, isDestructive, maskSecretsInSql, assertReadOnly } from './database.js';
|
|
6
|
+
import { addSaved, clearQueries, listQueries, listSaved, recordQuery, removeSaved, updateSaved } from './db-history.js';
|
|
7
|
+
import { createObject, listObjects, removeObject, setObjectManager, updateObject } from './index.js';
|
|
8
|
+
import { ObjectError, parseDatabase } from './model.js';
|
|
9
|
+
import { objectsStore } from './store.js';
|
|
10
|
+
function whoAsks(from) {
|
|
11
|
+
const a = typeof from === 'string' ? agents.get(from) : undefined;
|
|
12
|
+
if (!a)
|
|
13
|
+
throw new ObjectError('Unknown agent');
|
|
14
|
+
if (!mcpCaps(resolved(a)).includes('databases'))
|
|
15
|
+
throw new ObjectError('You do not have the "Database management" skill.');
|
|
16
|
+
return a;
|
|
17
|
+
}
|
|
18
|
+
const visibleTo = (a) => (o) => (o.manager_id ? o.manager_id === a.id : (o.colony_id ?? null) === (a.colony_id ?? null));
|
|
19
|
+
function pick(a, name) {
|
|
20
|
+
const wanted = String(name ?? '').trim().toLowerCase();
|
|
21
|
+
const o = listObjects().filter(visibleTo(a)).find((x) => x.name.toLowerCase() === wanted);
|
|
22
|
+
if (!o)
|
|
23
|
+
throw new ObjectError(`There is no resource named "${String(name ?? '')}" for you. Call object_db_list for the names.`);
|
|
24
|
+
if (o.kind !== 'database')
|
|
25
|
+
throw new ObjectError(`"${o.name}" is not a database resource.`);
|
|
26
|
+
return o;
|
|
27
|
+
}
|
|
28
|
+
const stored = (o) => objectsStore.get(o.id).config;
|
|
29
|
+
/** Permission table: read = everyone (plan too); write = acceptEdits and Full access; admin = Full access only. The permission that counts is the effective one. */
|
|
30
|
+
function need(a, level, what) {
|
|
31
|
+
const p = a.effective.permission;
|
|
32
|
+
if (level === 'read')
|
|
33
|
+
return;
|
|
34
|
+
if (p === 'plan')
|
|
35
|
+
throw new ObjectError(`You are read-only: you can look at the database but not ${what}.`);
|
|
36
|
+
if (level === 'admin' && p !== 'bypassPermissions')
|
|
37
|
+
throw new ObjectError(`${what[0].toUpperCase()}${what.slice(1)} needs Full access.`);
|
|
38
|
+
}
|
|
39
|
+
const refuseDestructive = (a, sql) => { if (a.effective.permission !== 'bypassPermissions' && isDestructive(sql))
|
|
40
|
+
throw new ObjectError('DROP and TRUNCATE need Full access.'); };
|
|
41
|
+
const clean = (s) => maskSecretsInSql(String(s ?? ''));
|
|
42
|
+
const fail = (e) => { throw e instanceof ObjectError ? new ObjectError(clean(e.message)) : new ObjectError(clean(e instanceof Error ? e.message : 'the database failed')); };
|
|
43
|
+
/** Runs something that touches the data and leaves the record a person can read: the history of the resource and a line in the server log. */
|
|
44
|
+
async function audited(a, o, sql, database, run) {
|
|
45
|
+
const text = maskSecretsInSql(sql);
|
|
46
|
+
console.log(`[hive-am] ${a.name} ran ${text.replace(/\s+/g, ' ').trim().slice(0, 200)}`);
|
|
47
|
+
try {
|
|
48
|
+
const r = await run();
|
|
49
|
+
recordQuery(o.id, { sql: text, database, ok: true, ms: r.ms ?? 0, rows: r.columns?.length ? r.rows?.length : undefined, affected: r.affected, by: a.name });
|
|
50
|
+
return r;
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
const msg = clean(e instanceof Error ? e.message : 'failed');
|
|
54
|
+
recordQuery(o.id, { sql: text, database, ok: false, ms: 0, error: msg, by: a.name });
|
|
55
|
+
return fail(e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const database = (args) => (args.database === undefined || args.database === null || args.database === '' ? undefined : String(args.database));
|
|
59
|
+
const sqlOf = (args) => { if (typeof args.sql !== 'string' || !args.sql.trim())
|
|
60
|
+
throw new ObjectError('Write the SQL'); return args.sql; };
|
|
61
|
+
/** `mine` is the agent's own colony, `none` is no colony. Anything else must be the agent's own colony id: an agent does not put resources into other colonies. */
|
|
62
|
+
function colonyArg(a, v) {
|
|
63
|
+
const s = String(v ?? '').trim().toLowerCase();
|
|
64
|
+
if (s === '' || s === 'none' || s === 'null')
|
|
65
|
+
return null;
|
|
66
|
+
if (s === 'mine' || s === (a.colony_id ?? '').toLowerCase())
|
|
67
|
+
return a.colony_id ?? null;
|
|
68
|
+
throw new ObjectError('A database can only be in your own colony ("mine") or in none ("none").');
|
|
69
|
+
}
|
|
70
|
+
/** The agent that looks after a database: by name or id, `me`, or `none` for the colony. */
|
|
71
|
+
function managerArg(a, v) {
|
|
72
|
+
const s = String(v ?? '').trim();
|
|
73
|
+
if (!s || s.toLowerCase() === 'none')
|
|
74
|
+
return null;
|
|
75
|
+
if (s.toLowerCase() === 'me')
|
|
76
|
+
return a.id;
|
|
77
|
+
const m = agents.get(s) ?? agents.byName(s);
|
|
78
|
+
if (!m)
|
|
79
|
+
throw new ObjectError(`There is no agent "${s}"`);
|
|
80
|
+
return m.id;
|
|
81
|
+
}
|
|
82
|
+
// ---- the operations --------------------------------------------------------------------------------------------------------------------
|
|
83
|
+
function toCsv(columns, rows) {
|
|
84
|
+
const q = (v) => { const t = v === null || v === undefined ? '' : String(v); return /[",\r\n]/.test(t) ? `"${t.replace(/"/g, '""')}"` : t; };
|
|
85
|
+
return [columns.map((c) => q(c.name)).join(','), ...rows.map((r) => r.map(q).join(','))].join('\r\n') + '\r\n';
|
|
86
|
+
}
|
|
87
|
+
function toJson(columns, rows) {
|
|
88
|
+
const seen = new Map();
|
|
89
|
+
const keys = columns.map((c) => { const n = seen.get(c.name) ?? 0; seen.set(c.name, n + 1); return n ? `${c.name}_${n + 1}` : c.name; });
|
|
90
|
+
return `${JSON.stringify(rows.map((r) => Object.fromEntries(keys.map((k, i) => [k, r[i]]))), null, 2)}\n`;
|
|
91
|
+
}
|
|
92
|
+
/** Writes a result as a file inside the agent's own folder: never outside it (a symlink included), and never over an existing file unless asked. */
|
|
93
|
+
function writeInside(a, file, overwrite, content) {
|
|
94
|
+
const rel = String(file ?? '').trim();
|
|
95
|
+
if (!rel)
|
|
96
|
+
throw new ObjectError('Say which file to write');
|
|
97
|
+
const root = realpathSync(a.effective.cwd);
|
|
98
|
+
const target = resolve(root, rel);
|
|
99
|
+
const inside = (p) => p === root || p.startsWith(root + sep);
|
|
100
|
+
if (isAbsolute(rel) ? !inside(resolve(rel)) : !inside(target))
|
|
101
|
+
throw new ObjectError('The file has to be inside your own working folder.');
|
|
102
|
+
const dir = dirname(target);
|
|
103
|
+
mkdirSync(dir, { recursive: true });
|
|
104
|
+
if (!inside(realpathSync(dir)))
|
|
105
|
+
throw new ObjectError('The file has to be inside your own working folder.');
|
|
106
|
+
if (existsSync(target) && !overwrite)
|
|
107
|
+
throw new ObjectError(`${relative(root, target)} already exists: choose another name or pass overwrite: true.`);
|
|
108
|
+
writeFileSync(target, content, { flag: overwrite ? 'w' : 'wx' });
|
|
109
|
+
return relative(root, target);
|
|
110
|
+
}
|
|
111
|
+
export async function agentDb(op, body) {
|
|
112
|
+
const a = whoAsks(body.from);
|
|
113
|
+
const args = { ...body };
|
|
114
|
+
delete args.from;
|
|
115
|
+
const o = String(op ?? '');
|
|
116
|
+
// Operations that do not start from an existing database.
|
|
117
|
+
if (o === 'list') {
|
|
118
|
+
return { databases: listObjects().filter(visibleTo(a)).filter((x) => x.kind === 'database').map((x) => { const c = x.config; return { name: x.name, engine: c.engine, database: c.engine === 'sqlite' ? c.file : c.database ?? null, status: x.state.status, detail: x.state.detail ?? null }; }) };
|
|
119
|
+
}
|
|
120
|
+
if (o === 'create') {
|
|
121
|
+
need(a, 'admin', 'creating a database resource');
|
|
122
|
+
const cfg = parseDatabase(args.config);
|
|
123
|
+
await dbTest(cfg).catch((e) => fail(e));
|
|
124
|
+
const made = createObject({ name: args.name, kind: 'database', colony_id: colonyArg(a, args.colony_id ?? 'mine'), config: args.config });
|
|
125
|
+
setObjectManager(made.id, a.id);
|
|
126
|
+
return { name: made.name, engine: cfg.engine, created: true };
|
|
127
|
+
}
|
|
128
|
+
if (o === 'test' && args.config !== undefined && args.name === undefined) {
|
|
129
|
+
need(a, 'admin', 'testing a connection that is not saved');
|
|
130
|
+
const t = await dbTest(parseDatabase(args.config)).catch((e) => fail(e));
|
|
131
|
+
return { ok: t.ok, version: t.version, ms: t.ms };
|
|
132
|
+
}
|
|
133
|
+
const view = pick(a, args.name);
|
|
134
|
+
const cfg = stored(view);
|
|
135
|
+
const db = database(args);
|
|
136
|
+
switch (o) {
|
|
137
|
+
// ---- discovery and reads ----
|
|
138
|
+
case 'schema': return dbSchema(cfg, db).catch(fail);
|
|
139
|
+
case 'columns': return { columns: await dbColumns(cfg, { database: db, schema: args.schema, table: args.table }).catch(fail) };
|
|
140
|
+
case 'describe': return dbDescribe(cfg, { database: db, schema: args.schema, table: args.table }).catch(fail);
|
|
141
|
+
case 'search': return { matches: await dbSearch(cfg, { database: db, query: args.query }).catch(fail) };
|
|
142
|
+
case 'ddl': return { ddl: await dbDdl(cfg, { database: db, schema: args.schema, table: args.table }).catch(fail) };
|
|
143
|
+
case 'sample': return dbSample(cfg, { database: db, schema: args.schema, table: args.table, limit: args.limit }).catch(fail);
|
|
144
|
+
case 'explain': return dbExplain(cfg, { sql: sqlOf(args), database: db }).catch(fail);
|
|
145
|
+
case 'test': {
|
|
146
|
+
const t = await dbTest(cfg).catch((e) => fail(e));
|
|
147
|
+
return { ok: t.ok, version: t.version, ms: t.ms };
|
|
148
|
+
}
|
|
149
|
+
case 'config': {
|
|
150
|
+
const c = { ...cfg };
|
|
151
|
+
const has = !!c.password;
|
|
152
|
+
delete c.password;
|
|
153
|
+
return { name: view.name, colony_id: view.colony_id, manager_id: view.manager_id, config: { ...c, passwordSet: has } };
|
|
154
|
+
}
|
|
155
|
+
// ---- queries ----
|
|
156
|
+
case 'query': {
|
|
157
|
+
const sql = sqlOf(args);
|
|
158
|
+
if (a.effective.permission === 'plan') {
|
|
159
|
+
try {
|
|
160
|
+
assertReadOnly(sql);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
throw new ObjectError('You are read-only: only SELECT, WITH, SHOW, DESCRIBE and EXPLAIN are allowed.');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
refuseDestructive(a, sql);
|
|
167
|
+
return audited(a, view, sql, db, () => dbQuery(cfg, { sql, database: db, readOnly: a.effective.permission === 'plan' }));
|
|
168
|
+
}
|
|
169
|
+
case 'exec': {
|
|
170
|
+
need(a, 'write', 'run a script');
|
|
171
|
+
const sql = sqlOf(args);
|
|
172
|
+
refuseDestructive(a, sql);
|
|
173
|
+
return audited(a, view, sql, db, async () => { const r = await dbExec(cfg, { sql, database: db, readOnly: false }); return { ...r, rows: undefined }; });
|
|
174
|
+
}
|
|
175
|
+
case 'export': {
|
|
176
|
+
need(a, 'write', 'export to a file');
|
|
177
|
+
const sql = sqlOf(args), format = String(args.format ?? 'csv').toLowerCase();
|
|
178
|
+
if (format !== 'csv' && format !== 'json')
|
|
179
|
+
throw new ObjectError('The format is csv or json');
|
|
180
|
+
refuseDestructive(a, sql);
|
|
181
|
+
const r = await audited(a, view, sql, db, () => dbQuery(cfg, { sql, database: db, readOnly: false }));
|
|
182
|
+
if (!r.columns.length)
|
|
183
|
+
throw new ObjectError('That statement returns no rows, so there is nothing to export.');
|
|
184
|
+
const content = format === 'csv' ? toCsv(r.columns, r.rows) : toJson(r.columns, r.rows);
|
|
185
|
+
const file = writeInside(a, args.file, args.overwrite === true, content);
|
|
186
|
+
return { file, format, rows: r.rows.length, truncated: r.truncated, bytes: Buffer.byteLength(content) };
|
|
187
|
+
}
|
|
188
|
+
// ---- saved queries, history, sessions ----
|
|
189
|
+
case 'saved': {
|
|
190
|
+
const sub = String(args.op ?? 'list');
|
|
191
|
+
if (sub === 'list')
|
|
192
|
+
return { saved: listSaved(view.id).map((s) => ({ ...s, sql: maskSecretsInSql(s.sql) })) };
|
|
193
|
+
need(a, 'write', 'change the saved queries');
|
|
194
|
+
if (sub === 'add') {
|
|
195
|
+
const name = String(args.query_name ?? '').trim().slice(0, 80), sql = String(args.sql ?? '');
|
|
196
|
+
if (!name || !sql.trim())
|
|
197
|
+
throw new ObjectError('Give the query a name (query_name) and some text (sql)');
|
|
198
|
+
try {
|
|
199
|
+
const e = addSaved(view.id, { name, sql: maskSecretsInSql(sql), database: db });
|
|
200
|
+
return { saved: { ...e, sql: maskSecretsInSql(e.sql) } };
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
return fail(e);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (sub === 'update') {
|
|
207
|
+
const e = updateSaved(view.id, String(args.id ?? ''), { name: args.query_name === undefined ? undefined : String(args.query_name).trim().slice(0, 80), sql: args.sql === undefined ? undefined : maskSecretsInSql(String(args.sql)), database: args.database === undefined ? undefined : db ?? null });
|
|
208
|
+
if (!e)
|
|
209
|
+
throw new ObjectError('Saved query not found');
|
|
210
|
+
return { saved: e };
|
|
211
|
+
}
|
|
212
|
+
if (sub === 'remove') {
|
|
213
|
+
if (!removeSaved(view.id, String(args.id ?? '')))
|
|
214
|
+
throw new ObjectError('Saved query not found');
|
|
215
|
+
return { removed: true };
|
|
216
|
+
}
|
|
217
|
+
throw new ObjectError('op is list, add, update or remove');
|
|
218
|
+
}
|
|
219
|
+
case 'history': {
|
|
220
|
+
const sub = String(args.op ?? 'list');
|
|
221
|
+
if (sub === 'list')
|
|
222
|
+
return { history: listQueries(view.id).map((h) => ({ ...h, sql: maskSecretsInSql(h.sql), error: h.error ? clean(h.error) : undefined })) };
|
|
223
|
+
if (sub === 'clear') {
|
|
224
|
+
need(a, 'write', 'clear the history');
|
|
225
|
+
clearQueries(view.id);
|
|
226
|
+
return { cleared: true };
|
|
227
|
+
}
|
|
228
|
+
throw new ObjectError('op is list or clear');
|
|
229
|
+
}
|
|
230
|
+
case 'sessions':
|
|
231
|
+
need(a, 'write', 'see the sessions');
|
|
232
|
+
return dbSessions(cfg).catch(fail);
|
|
233
|
+
case 'kill':
|
|
234
|
+
need(a, 'admin', 'cancelling a session');
|
|
235
|
+
return { text: await dbKill(cfg, args.id).catch(fail) };
|
|
236
|
+
// ---- the resource itself ----
|
|
237
|
+
case 'config_set': {
|
|
238
|
+
need(a, 'admin', 'changing a connection');
|
|
239
|
+
const patch = args.config && typeof args.config === 'object' ? { ...args.config } : {};
|
|
240
|
+
if (patch.engine !== undefined && patch.engine !== cfg.engine)
|
|
241
|
+
throw new ObjectError('The engine of a resource cannot change: create another one.');
|
|
242
|
+
const empty = patch.password === undefined || patch.password === '';
|
|
243
|
+
const merged = { ...cfg, ...patch, engine: cfg.engine, password: empty ? cfg.password : patch.password };
|
|
244
|
+
const next = parseDatabase(merged);
|
|
245
|
+
await dbTest(next).catch((e) => fail(e));
|
|
246
|
+
const colony = args.colony_id === undefined ? undefined : colonyArg(a, args.colony_id);
|
|
247
|
+
const updated = updateObject(view.id, { name: args.new_name === undefined ? undefined : args.new_name, colony_id: colony, config: merged });
|
|
248
|
+
if (args.manager_id !== undefined)
|
|
249
|
+
setObjectManager(view.id, managerArg(a, args.manager_id));
|
|
250
|
+
return { name: updated.name, saved: true };
|
|
251
|
+
}
|
|
252
|
+
case 'delete':
|
|
253
|
+
need(a, 'admin', 'deleting a database resource');
|
|
254
|
+
await removeObject(view.id);
|
|
255
|
+
return { deleted: view.name };
|
|
256
|
+
}
|
|
257
|
+
throw new ObjectError(`Unknown database operation "${o}"`);
|
|
258
|
+
}
|