@agenticmail/api 0.5.40 → 0.5.42
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/dist/index.js +937 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3348,6 +3348,942 @@ function createSmsRoutes(db, accountManager, config, gatewayManager) {
|
|
|
3348
3348
|
return router;
|
|
3349
3349
|
}
|
|
3350
3350
|
|
|
3351
|
+
// src/routes/storage.ts
|
|
3352
|
+
import { Router as Router11 } from "express";
|
|
3353
|
+
function mapColumnType(col, dialect) {
|
|
3354
|
+
const typeMap = {
|
|
3355
|
+
sqlite: { text: "TEXT", integer: "INTEGER", real: "REAL", boolean: "INTEGER", json: "JSON", blob: "BLOB", timestamp: "TEXT" },
|
|
3356
|
+
postgres: { text: "TEXT", integer: "INTEGER", real: "DOUBLE PRECISION", boolean: "BOOLEAN", json: "JSONB", blob: "BYTEA", timestamp: "TIMESTAMPTZ" },
|
|
3357
|
+
mysql: { text: "TEXT", integer: "INT", real: "DOUBLE", boolean: "TINYINT(1)", json: "JSON", blob: "LONGBLOB", timestamp: "DATETIME" },
|
|
3358
|
+
turso: { text: "TEXT", integer: "INTEGER", real: "REAL", boolean: "INTEGER", json: "TEXT", blob: "BLOB", timestamp: "TEXT" }
|
|
3359
|
+
};
|
|
3360
|
+
return (typeMap[dialect] || typeMap.sqlite)[col.type] || "TEXT";
|
|
3361
|
+
}
|
|
3362
|
+
function buildColumnDDL(col, dialect) {
|
|
3363
|
+
let ddl = `${col.name} ${mapColumnType(col, dialect)}`;
|
|
3364
|
+
if (col.primaryKey) ddl += " PRIMARY KEY";
|
|
3365
|
+
if (col.required && !col.primaryKey) ddl += " NOT NULL";
|
|
3366
|
+
if (col.unique && !col.primaryKey) ddl += " UNIQUE";
|
|
3367
|
+
if (col.default !== void 0) {
|
|
3368
|
+
const val = typeof col.default === "string" ? `'${col.default}'` : col.default;
|
|
3369
|
+
ddl += ` DEFAULT ${val}`;
|
|
3370
|
+
}
|
|
3371
|
+
if (col.check) ddl += ` CHECK (${col.check})`;
|
|
3372
|
+
if (col.references) {
|
|
3373
|
+
ddl += ` REFERENCES ${col.references.table}(${col.references.column})`;
|
|
3374
|
+
if (col.references.onDelete) ddl += ` ON DELETE ${col.references.onDelete}`;
|
|
3375
|
+
}
|
|
3376
|
+
return ddl;
|
|
3377
|
+
}
|
|
3378
|
+
function safeTableName(agentId, name2, shared) {
|
|
3379
|
+
const clean = name2.replace(/[^a-zA-Z0-9_]/g, "").substring(0, 64);
|
|
3380
|
+
if (!clean) throw new Error("Invalid table name");
|
|
3381
|
+
const prefix = shared ? "shared" : `agt_${agentId.replace(/[^a-zA-Z0-9]/g, "").substring(0, 16)}`;
|
|
3382
|
+
return `${prefix}_${clean}`;
|
|
3383
|
+
}
|
|
3384
|
+
function resolveTable(agentId, name2) {
|
|
3385
|
+
if (name2.startsWith("agt_") || name2.startsWith("shared_")) return name2;
|
|
3386
|
+
return safeTableName(agentId, name2, false);
|
|
3387
|
+
}
|
|
3388
|
+
function isSafeTable(tableName) {
|
|
3389
|
+
return tableName.startsWith("agt_") || tableName.startsWith("shared_");
|
|
3390
|
+
}
|
|
3391
|
+
function buildWhereClause(where) {
|
|
3392
|
+
const params = [];
|
|
3393
|
+
const conditions = Object.entries(where).map(([k, v]) => {
|
|
3394
|
+
if (v === null) return `${k} IS NULL`;
|
|
3395
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
3396
|
+
const ops = Object.entries(v).map(([op, val]) => {
|
|
3397
|
+
switch (op) {
|
|
3398
|
+
case "$gt":
|
|
3399
|
+
params.push(val);
|
|
3400
|
+
return `${k} > ?`;
|
|
3401
|
+
case "$gte":
|
|
3402
|
+
params.push(val);
|
|
3403
|
+
return `${k} >= ?`;
|
|
3404
|
+
case "$lt":
|
|
3405
|
+
params.push(val);
|
|
3406
|
+
return `${k} < ?`;
|
|
3407
|
+
case "$lte":
|
|
3408
|
+
params.push(val);
|
|
3409
|
+
return `${k} <= ?`;
|
|
3410
|
+
case "$ne":
|
|
3411
|
+
params.push(val);
|
|
3412
|
+
return `${k} != ?`;
|
|
3413
|
+
case "$like":
|
|
3414
|
+
params.push(val);
|
|
3415
|
+
return `${k} LIKE ?`;
|
|
3416
|
+
case "$ilike":
|
|
3417
|
+
params.push(val);
|
|
3418
|
+
return `LOWER(${k}) LIKE LOWER(?)`;
|
|
3419
|
+
case "$not_like":
|
|
3420
|
+
params.push(val);
|
|
3421
|
+
return `${k} NOT LIKE ?`;
|
|
3422
|
+
case "$in": {
|
|
3423
|
+
const arr = val;
|
|
3424
|
+
params.push(...arr);
|
|
3425
|
+
return `${k} IN (${arr.map(() => "?").join(", ")})`;
|
|
3426
|
+
}
|
|
3427
|
+
case "$not_in": {
|
|
3428
|
+
const arr = val;
|
|
3429
|
+
params.push(...arr);
|
|
3430
|
+
return `${k} NOT IN (${arr.map(() => "?").join(", ")})`;
|
|
3431
|
+
}
|
|
3432
|
+
case "$is_null":
|
|
3433
|
+
return val ? `${k} IS NULL` : `${k} IS NOT NULL`;
|
|
3434
|
+
case "$between": {
|
|
3435
|
+
const [lo, hi] = val;
|
|
3436
|
+
params.push(lo, hi);
|
|
3437
|
+
return `${k} BETWEEN ? AND ?`;
|
|
3438
|
+
}
|
|
3439
|
+
default:
|
|
3440
|
+
params.push(val);
|
|
3441
|
+
return `${k} = ?`;
|
|
3442
|
+
}
|
|
3443
|
+
});
|
|
3444
|
+
return ops.join(" AND ");
|
|
3445
|
+
}
|
|
3446
|
+
if (Array.isArray(v)) {
|
|
3447
|
+
params.push(...v);
|
|
3448
|
+
return `${k} IN (${v.map(() => "?").join(", ")})`;
|
|
3449
|
+
}
|
|
3450
|
+
params.push(typeof v === "object" ? JSON.stringify(v) : v);
|
|
3451
|
+
return `${k} = ?`;
|
|
3452
|
+
});
|
|
3453
|
+
return { sql: conditions.join(" AND "), params };
|
|
3454
|
+
}
|
|
3455
|
+
function nowExpr(dialect) {
|
|
3456
|
+
return dialect === "postgres" ? "NOW()" : "datetime('now')";
|
|
3457
|
+
}
|
|
3458
|
+
function createStorageRoutes(db, accountManager, config, dialect = "sqlite") {
|
|
3459
|
+
const router = Router11();
|
|
3460
|
+
function getAgent(req, res) {
|
|
3461
|
+
const agent = req.agent;
|
|
3462
|
+
if (!agent) {
|
|
3463
|
+
res.status(401).json({ error: "Authentication required" });
|
|
3464
|
+
return null;
|
|
3465
|
+
}
|
|
3466
|
+
return agent;
|
|
3467
|
+
}
|
|
3468
|
+
async function verifyAccess(agent, tableName, res, requireOwner = false) {
|
|
3469
|
+
const meta = await db.get("SELECT * FROM agenticmail_storage_meta WHERE table_name = ?", [tableName]);
|
|
3470
|
+
if (!meta) {
|
|
3471
|
+
res.status(404).json({ error: "Table not found" });
|
|
3472
|
+
return null;
|
|
3473
|
+
}
|
|
3474
|
+
if (requireOwner && meta.agent_id !== agent.id) {
|
|
3475
|
+
res.status(403).json({ error: "Only the owner can perform this action" });
|
|
3476
|
+
return null;
|
|
3477
|
+
}
|
|
3478
|
+
if (meta.agent_id !== agent.id && !meta.shared) {
|
|
3479
|
+
res.status(403).json({ error: "Access denied" });
|
|
3480
|
+
return null;
|
|
3481
|
+
}
|
|
3482
|
+
return meta;
|
|
3483
|
+
}
|
|
3484
|
+
const ensureMetaTable = /* @__PURE__ */ (() => {
|
|
3485
|
+
let done = false;
|
|
3486
|
+
return async () => {
|
|
3487
|
+
if (done) return;
|
|
3488
|
+
await db.run(`
|
|
3489
|
+
CREATE TABLE IF NOT EXISTS agenticmail_storage_meta (
|
|
3490
|
+
table_name TEXT PRIMARY KEY,
|
|
3491
|
+
agent_id TEXT NOT NULL,
|
|
3492
|
+
display_name TEXT NOT NULL,
|
|
3493
|
+
description TEXT DEFAULT '',
|
|
3494
|
+
shared INTEGER NOT NULL DEFAULT 0,
|
|
3495
|
+
columns JSON NOT NULL DEFAULT '[]',
|
|
3496
|
+
indexes JSON NOT NULL DEFAULT '[]',
|
|
3497
|
+
row_count INTEGER NOT NULL DEFAULT 0,
|
|
3498
|
+
created_at TEXT NOT NULL DEFAULT (${nowExpr(dialect)}),
|
|
3499
|
+
updated_at TEXT NOT NULL DEFAULT (${nowExpr(dialect)}),
|
|
3500
|
+
archived_at TEXT
|
|
3501
|
+
)
|
|
3502
|
+
`);
|
|
3503
|
+
done = true;
|
|
3504
|
+
};
|
|
3505
|
+
})();
|
|
3506
|
+
router.post("/storage/tables", async (req, res) => {
|
|
3507
|
+
const agent = getAgent(req, res);
|
|
3508
|
+
if (!agent) return;
|
|
3509
|
+
await ensureMetaTable();
|
|
3510
|
+
try {
|
|
3511
|
+
const { name: name2, columns, indexes, shared, description, timestamps } = req.body;
|
|
3512
|
+
if (!name2 || !columns?.length) return res.status(400).json({ error: "name and columns are required" });
|
|
3513
|
+
const hasPK = columns.some((c) => c.primaryKey);
|
|
3514
|
+
const allCols = [...hasPK ? [] : [{ name: "id", type: "text", primaryKey: true }], ...columns];
|
|
3515
|
+
if (timestamps !== false) {
|
|
3516
|
+
if (!allCols.find((c) => c.name === "created_at")) {
|
|
3517
|
+
allCols.push({ name: "created_at", type: "timestamp", default: nowExpr(dialect) });
|
|
3518
|
+
}
|
|
3519
|
+
if (!allCols.find((c) => c.name === "updated_at")) {
|
|
3520
|
+
allCols.push({ name: "updated_at", type: "timestamp", default: nowExpr(dialect) });
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
const tableName = safeTableName(agent.id, name2, !!shared);
|
|
3524
|
+
const existing = await db.get("SELECT table_name FROM agenticmail_storage_meta WHERE table_name = ?", [tableName]);
|
|
3525
|
+
if (existing) return res.status(409).json({ error: `Table "${name2}" already exists`, table: tableName });
|
|
3526
|
+
const colDefs = allCols.map((c) => buildColumnDDL(c, dialect)).join(",\n ");
|
|
3527
|
+
await db.run(`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
3528
|
+
${colDefs}
|
|
3529
|
+
)`);
|
|
3530
|
+
const idxMeta = [];
|
|
3531
|
+
if (indexes?.length) {
|
|
3532
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
3533
|
+
const idx = indexes[i];
|
|
3534
|
+
const idxName = idx.name || `idx_${tableName}_${idx.columns.join("_")}`;
|
|
3535
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
3536
|
+
let idxSql = `CREATE ${unique}INDEX IF NOT EXISTS ${idxName} ON ${tableName}(${idx.columns.join(", ")})`;
|
|
3537
|
+
if (idx.where && (dialect === "sqlite" || dialect === "postgres" || dialect === "turso")) {
|
|
3538
|
+
idxSql += ` WHERE ${idx.where}`;
|
|
3539
|
+
}
|
|
3540
|
+
await db.run(idxSql);
|
|
3541
|
+
idxMeta.push({ name: idxName, columns: idx.columns, unique: !!idx.unique, where: idx.where });
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
await db.run(
|
|
3545
|
+
"INSERT INTO agenticmail_storage_meta (table_name, agent_id, display_name, description, shared, columns, indexes) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
3546
|
+
[tableName, agent.id, name2, description || "", shared ? 1 : 0, JSON.stringify(allCols), JSON.stringify(idxMeta)]
|
|
3547
|
+
);
|
|
3548
|
+
res.json({ ok: true, table: tableName, columns: allCols, indexes: idxMeta });
|
|
3549
|
+
} catch (err) {
|
|
3550
|
+
res.status(500).json({ error: err.message });
|
|
3551
|
+
}
|
|
3552
|
+
});
|
|
3553
|
+
router.get("/storage/tables", async (req, res) => {
|
|
3554
|
+
const agent = getAgent(req, res);
|
|
3555
|
+
if (!agent) return;
|
|
3556
|
+
await ensureMetaTable();
|
|
3557
|
+
try {
|
|
3558
|
+
const includeShared = req.query.includeShared !== "false";
|
|
3559
|
+
const includeArchived = req.query.includeArchived === "true";
|
|
3560
|
+
let sql = "SELECT * FROM agenticmail_storage_meta WHERE (agent_id = ?";
|
|
3561
|
+
const params = [agent.id];
|
|
3562
|
+
if (includeShared) sql += " OR shared = 1";
|
|
3563
|
+
sql += ")";
|
|
3564
|
+
if (!includeArchived) sql += " AND archived_at IS NULL";
|
|
3565
|
+
const tables = await db.all(sql, params);
|
|
3566
|
+
res.json({
|
|
3567
|
+
tables: tables.map((t) => ({
|
|
3568
|
+
name: t.display_name,
|
|
3569
|
+
table: t.table_name,
|
|
3570
|
+
description: t.description,
|
|
3571
|
+
shared: !!t.shared,
|
|
3572
|
+
archived: !!t.archived_at,
|
|
3573
|
+
columns: typeof t.columns === "string" ? JSON.parse(t.columns) : t.columns,
|
|
3574
|
+
indexes: typeof t.indexes === "string" ? JSON.parse(t.indexes) : t.indexes || [],
|
|
3575
|
+
rowCount: t.row_count,
|
|
3576
|
+
createdAt: t.created_at,
|
|
3577
|
+
updatedAt: t.updated_at
|
|
3578
|
+
}))
|
|
3579
|
+
});
|
|
3580
|
+
} catch (err) {
|
|
3581
|
+
res.status(500).json({ error: err.message });
|
|
3582
|
+
}
|
|
3583
|
+
});
|
|
3584
|
+
router.get("/storage/tables/:name/describe", async (req, res) => {
|
|
3585
|
+
const agent = getAgent(req, res);
|
|
3586
|
+
if (!agent) return;
|
|
3587
|
+
await ensureMetaTable();
|
|
3588
|
+
try {
|
|
3589
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
3590
|
+
const meta = await verifyAccess(agent, tableName, res);
|
|
3591
|
+
if (!meta) return;
|
|
3592
|
+
let schemaInfo = [];
|
|
3593
|
+
if (dialect === "sqlite" || dialect === "turso") {
|
|
3594
|
+
schemaInfo = await db.all(`PRAGMA table_info(${tableName})`);
|
|
3595
|
+
} else if (dialect === "postgres") {
|
|
3596
|
+
schemaInfo = await db.all(
|
|
3597
|
+
`SELECT column_name as name, data_type as type, is_nullable, column_default
|
|
3598
|
+
FROM information_schema.columns WHERE table_name = ? ORDER BY ordinal_position`,
|
|
3599
|
+
[tableName]
|
|
3600
|
+
);
|
|
3601
|
+
} else if (dialect === "mysql") {
|
|
3602
|
+
schemaInfo = await db.all(`DESCRIBE ${tableName}`);
|
|
3603
|
+
}
|
|
3604
|
+
let indexInfo = [];
|
|
3605
|
+
if (dialect === "sqlite" || dialect === "turso") {
|
|
3606
|
+
indexInfo = await db.all(`PRAGMA index_list(${tableName})`);
|
|
3607
|
+
} else if (dialect === "postgres") {
|
|
3608
|
+
indexInfo = await db.all(
|
|
3609
|
+
`SELECT indexname, indexdef FROM pg_indexes WHERE tablename = ?`,
|
|
3610
|
+
[tableName]
|
|
3611
|
+
);
|
|
3612
|
+
}
|
|
3613
|
+
const countResult = await db.get(`SELECT COUNT(*) as cnt FROM ${tableName}`);
|
|
3614
|
+
const rowCount = countResult?.cnt || 0;
|
|
3615
|
+
await db.run("UPDATE agenticmail_storage_meta SET row_count = ? WHERE table_name = ?", [rowCount, tableName]);
|
|
3616
|
+
res.json({
|
|
3617
|
+
table: tableName,
|
|
3618
|
+
name: meta.display_name,
|
|
3619
|
+
description: meta.description,
|
|
3620
|
+
shared: !!meta.shared,
|
|
3621
|
+
columns: typeof meta.columns === "string" ? JSON.parse(meta.columns) : meta.columns,
|
|
3622
|
+
indexes: typeof meta.indexes === "string" ? JSON.parse(meta.indexes) : meta.indexes || [],
|
|
3623
|
+
rowCount,
|
|
3624
|
+
dbSchema: schemaInfo,
|
|
3625
|
+
dbIndexes: indexInfo,
|
|
3626
|
+
createdAt: meta.created_at
|
|
3627
|
+
});
|
|
3628
|
+
} catch (err) {
|
|
3629
|
+
res.status(500).json({ error: err.message });
|
|
3630
|
+
}
|
|
3631
|
+
});
|
|
3632
|
+
router.post("/storage/tables/:name/columns", async (req, res) => {
|
|
3633
|
+
const agent = getAgent(req, res);
|
|
3634
|
+
if (!agent) return;
|
|
3635
|
+
await ensureMetaTable();
|
|
3636
|
+
try {
|
|
3637
|
+
const { column } = req.body;
|
|
3638
|
+
if (!column?.name || !column?.type) return res.status(400).json({ error: "column with name and type is required" });
|
|
3639
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
3640
|
+
const meta = await verifyAccess(agent, tableName, res, true);
|
|
3641
|
+
if (!meta) return;
|
|
3642
|
+
await db.run(`ALTER TABLE ${tableName} ADD COLUMN ${buildColumnDDL(column, dialect)}`);
|
|
3643
|
+
const cols = typeof meta.columns === "string" ? JSON.parse(meta.columns) : meta.columns;
|
|
3644
|
+
cols.push(column);
|
|
3645
|
+
await db.run(`UPDATE agenticmail_storage_meta SET columns = ?, updated_at = ${nowExpr(dialect)} WHERE table_name = ?`, [JSON.stringify(cols), tableName]);
|
|
3646
|
+
res.json({ ok: true, column: column.name });
|
|
3647
|
+
} catch (err) {
|
|
3648
|
+
res.status(500).json({ error: err.message });
|
|
3649
|
+
}
|
|
3650
|
+
});
|
|
3651
|
+
router.delete("/storage/tables/:name/columns/:col", async (req, res) => {
|
|
3652
|
+
const agent = getAgent(req, res);
|
|
3653
|
+
if (!agent) return;
|
|
3654
|
+
await ensureMetaTable();
|
|
3655
|
+
try {
|
|
3656
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
3657
|
+
const colName = req.params.col;
|
|
3658
|
+
const meta = await verifyAccess(agent, tableName, res, true);
|
|
3659
|
+
if (!meta) return;
|
|
3660
|
+
if (dialect === "sqlite" || dialect === "turso") {
|
|
3661
|
+
await db.run(`ALTER TABLE ${tableName} DROP COLUMN ${colName}`);
|
|
3662
|
+
} else {
|
|
3663
|
+
await db.run(`ALTER TABLE ${tableName} DROP COLUMN ${colName}`);
|
|
3664
|
+
}
|
|
3665
|
+
const cols = (typeof meta.columns === "string" ? JSON.parse(meta.columns) : meta.columns).filter((c) => c.name !== colName);
|
|
3666
|
+
await db.run(`UPDATE agenticmail_storage_meta SET columns = ?, updated_at = ${nowExpr(dialect)} WHERE table_name = ?`, [JSON.stringify(cols), tableName]);
|
|
3667
|
+
res.json({ ok: true, dropped: colName });
|
|
3668
|
+
} catch (err) {
|
|
3669
|
+
res.status(500).json({ error: err.message });
|
|
3670
|
+
}
|
|
3671
|
+
});
|
|
3672
|
+
router.post("/storage/tables/:name/rename", async (req, res) => {
|
|
3673
|
+
const agent = getAgent(req, res);
|
|
3674
|
+
if (!agent) return;
|
|
3675
|
+
await ensureMetaTable();
|
|
3676
|
+
try {
|
|
3677
|
+
const { newName } = req.body;
|
|
3678
|
+
if (!newName) return res.status(400).json({ error: "newName is required" });
|
|
3679
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
3680
|
+
const meta = await verifyAccess(agent, tableName, res, true);
|
|
3681
|
+
if (!meta) return;
|
|
3682
|
+
const newTableName = safeTableName(agent.id, newName, !!meta.shared);
|
|
3683
|
+
await db.run(`ALTER TABLE ${tableName} RENAME TO ${newTableName}`);
|
|
3684
|
+
await db.run(
|
|
3685
|
+
"UPDATE agenticmail_storage_meta SET table_name = ?, display_name = ?, updated_at = " + nowExpr(dialect) + " WHERE table_name = ?",
|
|
3686
|
+
[newTableName, newName, tableName]
|
|
3687
|
+
);
|
|
3688
|
+
res.json({ ok: true, oldTable: tableName, newTable: newTableName });
|
|
3689
|
+
} catch (err) {
|
|
3690
|
+
res.status(500).json({ error: err.message });
|
|
3691
|
+
}
|
|
3692
|
+
});
|
|
3693
|
+
router.post("/storage/tables/:name/rename-column", async (req, res) => {
|
|
3694
|
+
const agent = getAgent(req, res);
|
|
3695
|
+
if (!agent) return;
|
|
3696
|
+
await ensureMetaTable();
|
|
3697
|
+
try {
|
|
3698
|
+
const { oldName, newName } = req.body;
|
|
3699
|
+
if (!oldName || !newName) return res.status(400).json({ error: "oldName and newName are required" });
|
|
3700
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
3701
|
+
const meta = await verifyAccess(agent, tableName, res, true);
|
|
3702
|
+
if (!meta) return;
|
|
3703
|
+
await db.run(`ALTER TABLE ${tableName} RENAME COLUMN ${oldName} TO ${newName}`);
|
|
3704
|
+
const cols = typeof meta.columns === "string" ? JSON.parse(meta.columns) : meta.columns;
|
|
3705
|
+
const col = cols.find((c) => c.name === oldName);
|
|
3706
|
+
if (col) col.name = newName;
|
|
3707
|
+
await db.run(`UPDATE agenticmail_storage_meta SET columns = ?, updated_at = ${nowExpr(dialect)} WHERE table_name = ?`, [JSON.stringify(cols), tableName]);
|
|
3708
|
+
res.json({ ok: true, oldName, newName });
|
|
3709
|
+
} catch (err) {
|
|
3710
|
+
res.status(500).json({ error: err.message });
|
|
3711
|
+
}
|
|
3712
|
+
});
|
|
3713
|
+
router.delete("/storage/tables/:name", async (req, res) => {
|
|
3714
|
+
const agent = getAgent(req, res);
|
|
3715
|
+
if (!agent) return;
|
|
3716
|
+
await ensureMetaTable();
|
|
3717
|
+
try {
|
|
3718
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
3719
|
+
const meta = await verifyAccess(agent, tableName, res, true);
|
|
3720
|
+
if (!meta) return;
|
|
3721
|
+
await db.run(`DROP TABLE IF EXISTS ${tableName}`);
|
|
3722
|
+
await db.run("DELETE FROM agenticmail_storage_meta WHERE table_name = ?", [tableName]);
|
|
3723
|
+
res.json({ ok: true, dropped: tableName });
|
|
3724
|
+
} catch (err) {
|
|
3725
|
+
res.status(500).json({ error: err.message });
|
|
3726
|
+
}
|
|
3727
|
+
});
|
|
3728
|
+
router.post("/storage/tables/:name/clone", async (req, res) => {
|
|
3729
|
+
const agent = getAgent(req, res);
|
|
3730
|
+
if (!agent) return;
|
|
3731
|
+
await ensureMetaTable();
|
|
3732
|
+
try {
|
|
3733
|
+
const { newName, includeData } = req.body;
|
|
3734
|
+
if (!newName) return res.status(400).json({ error: "newName is required" });
|
|
3735
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
3736
|
+
const meta = await verifyAccess(agent, tableName, res);
|
|
3737
|
+
if (!meta) return;
|
|
3738
|
+
const newTableName = safeTableName(agent.id, newName, false);
|
|
3739
|
+
if (includeData !== false) {
|
|
3740
|
+
await db.run(`CREATE TABLE ${newTableName} AS SELECT * FROM ${tableName}`);
|
|
3741
|
+
} else {
|
|
3742
|
+
await db.run(`CREATE TABLE ${newTableName} AS SELECT * FROM ${tableName} WHERE 0`);
|
|
3743
|
+
}
|
|
3744
|
+
const countResult = await db.get(`SELECT COUNT(*) as cnt FROM ${newTableName}`);
|
|
3745
|
+
await db.run(
|
|
3746
|
+
"INSERT INTO agenticmail_storage_meta (table_name, agent_id, display_name, description, shared, columns, indexes, row_count) VALUES (?, ?, ?, ?, 0, ?, ?, ?)",
|
|
3747
|
+
[newTableName, agent.id, newName, `Clone of ${meta.display_name}`, meta.columns, "[]", countResult?.cnt || 0]
|
|
3748
|
+
);
|
|
3749
|
+
res.json({ ok: true, table: newTableName, rows: countResult?.cnt || 0 });
|
|
3750
|
+
} catch (err) {
|
|
3751
|
+
res.status(500).json({ error: err.message });
|
|
3752
|
+
}
|
|
3753
|
+
});
|
|
3754
|
+
router.post("/storage/tables/:name/indexes", async (req, res) => {
|
|
3755
|
+
const agent = getAgent(req, res);
|
|
3756
|
+
if (!agent) return;
|
|
3757
|
+
await ensureMetaTable();
|
|
3758
|
+
try {
|
|
3759
|
+
const { columns, unique, name: idxName, where: whereClause } = req.body;
|
|
3760
|
+
if (!columns?.length) return res.status(400).json({ error: "columns are required" });
|
|
3761
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
3762
|
+
const meta = await verifyAccess(agent, tableName, res, true);
|
|
3763
|
+
if (!meta) return;
|
|
3764
|
+
const finalName = idxName || `idx_${tableName}_${columns.join("_")}`;
|
|
3765
|
+
let sql = `CREATE ${unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${finalName} ON ${tableName}(${columns.join(", ")})`;
|
|
3766
|
+
if (whereClause && (dialect === "sqlite" || dialect === "postgres" || dialect === "turso")) {
|
|
3767
|
+
sql += ` WHERE ${whereClause}`;
|
|
3768
|
+
}
|
|
3769
|
+
await db.run(sql);
|
|
3770
|
+
const indexes = typeof meta.indexes === "string" ? JSON.parse(meta.indexes) : meta.indexes || [];
|
|
3771
|
+
indexes.push({ name: finalName, columns, unique: !!unique, where: whereClause });
|
|
3772
|
+
await db.run(`UPDATE agenticmail_storage_meta SET indexes = ?, updated_at = ${nowExpr(dialect)} WHERE table_name = ?`, [JSON.stringify(indexes), tableName]);
|
|
3773
|
+
res.json({ ok: true, index: finalName });
|
|
3774
|
+
} catch (err) {
|
|
3775
|
+
res.status(500).json({ error: err.message });
|
|
3776
|
+
}
|
|
3777
|
+
});
|
|
3778
|
+
router.get("/storage/tables/:name/indexes", async (req, res) => {
|
|
3779
|
+
const agent = getAgent(req, res);
|
|
3780
|
+
if (!agent) return;
|
|
3781
|
+
await ensureMetaTable();
|
|
3782
|
+
try {
|
|
3783
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
3784
|
+
const meta = await verifyAccess(agent, tableName, res);
|
|
3785
|
+
if (!meta) return;
|
|
3786
|
+
let dbIndexes = [];
|
|
3787
|
+
if (dialect === "sqlite" || dialect === "turso") {
|
|
3788
|
+
const idxList = await db.all(`PRAGMA index_list(${tableName})`);
|
|
3789
|
+
for (const idx of idxList) {
|
|
3790
|
+
const info = await db.all(`PRAGMA index_info(${idx.name})`);
|
|
3791
|
+
dbIndexes.push({ name: idx.name, unique: !!idx.unique, columns: info.map((i) => i.name) });
|
|
3792
|
+
}
|
|
3793
|
+
} else if (dialect === "postgres") {
|
|
3794
|
+
dbIndexes = await db.all(`SELECT indexname as name, indexdef as definition FROM pg_indexes WHERE tablename = ?`, [tableName]);
|
|
3795
|
+
} else if (dialect === "mysql") {
|
|
3796
|
+
dbIndexes = await db.all(`SHOW INDEX FROM ${tableName}`);
|
|
3797
|
+
}
|
|
3798
|
+
res.json({ indexes: dbIndexes });
|
|
3799
|
+
} catch (err) {
|
|
3800
|
+
res.status(500).json({ error: err.message });
|
|
3801
|
+
}
|
|
3802
|
+
});
|
|
3803
|
+
router.delete("/storage/tables/:name/indexes/:idx", async (req, res) => {
|
|
3804
|
+
const agent = getAgent(req, res);
|
|
3805
|
+
if (!agent) return;
|
|
3806
|
+
await ensureMetaTable();
|
|
3807
|
+
try {
|
|
3808
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
3809
|
+
const idxName = req.params.idx;
|
|
3810
|
+
const meta = await verifyAccess(agent, tableName, res, true);
|
|
3811
|
+
if (!meta) return;
|
|
3812
|
+
if (dialect === "mysql") {
|
|
3813
|
+
await db.run(`DROP INDEX ${idxName} ON ${tableName}`);
|
|
3814
|
+
} else {
|
|
3815
|
+
await db.run(`DROP INDEX IF EXISTS ${idxName}`);
|
|
3816
|
+
}
|
|
3817
|
+
const indexes = (typeof meta.indexes === "string" ? JSON.parse(meta.indexes) : meta.indexes || []).filter((i) => i.name !== idxName);
|
|
3818
|
+
await db.run(`UPDATE agenticmail_storage_meta SET indexes = ?, updated_at = ${nowExpr(dialect)} WHERE table_name = ?`, [JSON.stringify(indexes), tableName]);
|
|
3819
|
+
res.json({ ok: true, dropped: idxName });
|
|
3820
|
+
} catch (err) {
|
|
3821
|
+
res.status(500).json({ error: err.message });
|
|
3822
|
+
}
|
|
3823
|
+
});
|
|
3824
|
+
router.post("/storage/tables/:name/reindex", async (req, res) => {
|
|
3825
|
+
const agent = getAgent(req, res);
|
|
3826
|
+
if (!agent) return;
|
|
3827
|
+
await ensureMetaTable();
|
|
3828
|
+
try {
|
|
3829
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
3830
|
+
const meta = await verifyAccess(agent, tableName, res, true);
|
|
3831
|
+
if (!meta) return;
|
|
3832
|
+
if (dialect === "sqlite" || dialect === "turso") {
|
|
3833
|
+
await db.run(`REINDEX ${tableName}`);
|
|
3834
|
+
} else if (dialect === "postgres") {
|
|
3835
|
+
await db.run(`REINDEX TABLE ${tableName}`);
|
|
3836
|
+
} else if (dialect === "mysql") {
|
|
3837
|
+
await db.run(`OPTIMIZE TABLE ${tableName}`);
|
|
3838
|
+
}
|
|
3839
|
+
res.json({ ok: true });
|
|
3840
|
+
} catch (err) {
|
|
3841
|
+
res.status(500).json({ error: err.message });
|
|
3842
|
+
}
|
|
3843
|
+
});
|
|
3844
|
+
router.post("/storage/insert", async (req, res) => {
|
|
3845
|
+
const agent = getAgent(req, res);
|
|
3846
|
+
if (!agent) return;
|
|
3847
|
+
await ensureMetaTable();
|
|
3848
|
+
try {
|
|
3849
|
+
const { table, rows } = req.body;
|
|
3850
|
+
if (!table || !rows?.length) return res.status(400).json({ error: "table and rows are required" });
|
|
3851
|
+
const tableName = resolveTable(agent.id, table);
|
|
3852
|
+
if (!isSafeTable(tableName)) return res.status(403).json({ error: "Cannot insert into system tables" });
|
|
3853
|
+
const meta = await verifyAccess(agent, tableName, res);
|
|
3854
|
+
if (!meta) return;
|
|
3855
|
+
let inserted = 0;
|
|
3856
|
+
for (const row of rows) {
|
|
3857
|
+
const keys = Object.keys(row);
|
|
3858
|
+
const vals = Object.values(row).map((v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v);
|
|
3859
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
3860
|
+
await db.run(`INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders})`, vals);
|
|
3861
|
+
inserted++;
|
|
3862
|
+
}
|
|
3863
|
+
const countResult = await db.get(`SELECT COUNT(*) as cnt FROM ${tableName}`);
|
|
3864
|
+
await db.run("UPDATE agenticmail_storage_meta SET row_count = ?, updated_at = " + nowExpr(dialect) + " WHERE table_name = ?", [countResult?.cnt || 0, tableName]);
|
|
3865
|
+
res.json({ ok: true, inserted });
|
|
3866
|
+
} catch (err) {
|
|
3867
|
+
res.status(500).json({ error: err.message });
|
|
3868
|
+
}
|
|
3869
|
+
});
|
|
3870
|
+
router.post("/storage/upsert", async (req, res) => {
|
|
3871
|
+
const agent = getAgent(req, res);
|
|
3872
|
+
if (!agent) return;
|
|
3873
|
+
await ensureMetaTable();
|
|
3874
|
+
try {
|
|
3875
|
+
const { table, rows, conflictColumn } = req.body;
|
|
3876
|
+
if (!table || !rows?.length || !conflictColumn) return res.status(400).json({ error: "table, rows, and conflictColumn are required" });
|
|
3877
|
+
const tableName = resolveTable(agent.id, table);
|
|
3878
|
+
if (!isSafeTable(tableName)) return res.status(403).json({ error: "Cannot upsert into system tables" });
|
|
3879
|
+
const meta = await verifyAccess(agent, tableName, res);
|
|
3880
|
+
if (!meta) return;
|
|
3881
|
+
let upserted = 0;
|
|
3882
|
+
for (const row of rows) {
|
|
3883
|
+
const keys = Object.keys(row);
|
|
3884
|
+
const vals = Object.values(row).map((v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v);
|
|
3885
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
3886
|
+
const updateCols = keys.filter((k) => k !== conflictColumn).map((k) => `${k} = excluded.${k}`).join(", ");
|
|
3887
|
+
if (dialect === "mysql") {
|
|
3888
|
+
const dupUpdate = keys.filter((k) => k !== conflictColumn).map((k) => `${k} = VALUES(${k})`).join(", ");
|
|
3889
|
+
await db.run(`INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${dupUpdate}`, vals);
|
|
3890
|
+
} else {
|
|
3891
|
+
await db.run(`INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflictColumn}) DO UPDATE SET ${updateCols}`, vals);
|
|
3892
|
+
}
|
|
3893
|
+
upserted++;
|
|
3894
|
+
}
|
|
3895
|
+
res.json({ ok: true, upserted });
|
|
3896
|
+
} catch (err) {
|
|
3897
|
+
res.status(500).json({ error: err.message });
|
|
3898
|
+
}
|
|
3899
|
+
});
|
|
3900
|
+
router.post("/storage/query", async (req, res) => {
|
|
3901
|
+
const agent = getAgent(req, res);
|
|
3902
|
+
if (!agent) return;
|
|
3903
|
+
await ensureMetaTable();
|
|
3904
|
+
try {
|
|
3905
|
+
const { table, where, orderBy, limit, offset, columns, distinct, groupBy, having } = req.body;
|
|
3906
|
+
if (!table) return res.status(400).json({ error: "table is required" });
|
|
3907
|
+
const tableName = resolveTable(agent.id, table);
|
|
3908
|
+
if (!isSafeTable(tableName)) return res.status(403).json({ error: "Cannot query system tables" });
|
|
3909
|
+
const meta = await verifyAccess(agent, tableName, res);
|
|
3910
|
+
if (!meta) return;
|
|
3911
|
+
const selectCols = columns?.length ? columns.join(", ") : "*";
|
|
3912
|
+
let sql = `SELECT ${distinct ? "DISTINCT " : ""}${selectCols} FROM ${tableName}`;
|
|
3913
|
+
let params = [];
|
|
3914
|
+
if (where && Object.keys(where).length) {
|
|
3915
|
+
const w = buildWhereClause(where);
|
|
3916
|
+
sql += ` WHERE ${w.sql}`;
|
|
3917
|
+
params = w.params;
|
|
3918
|
+
}
|
|
3919
|
+
if (groupBy) sql += ` GROUP BY ${groupBy.replace(/[^a-zA-Z0-9_, ()]/g, "")}`;
|
|
3920
|
+
if (having) sql += ` HAVING ${having}`;
|
|
3921
|
+
if (orderBy) sql += ` ORDER BY ${orderBy.replace(/[^a-zA-Z0-9_, ]/g, "")}`;
|
|
3922
|
+
if (limit) {
|
|
3923
|
+
sql += " LIMIT ?";
|
|
3924
|
+
params.push(limit);
|
|
3925
|
+
}
|
|
3926
|
+
if (offset) {
|
|
3927
|
+
sql += " OFFSET ?";
|
|
3928
|
+
params.push(offset);
|
|
3929
|
+
}
|
|
3930
|
+
const rows = await db.all(sql, params);
|
|
3931
|
+
res.json({ rows, count: rows.length });
|
|
3932
|
+
} catch (err) {
|
|
3933
|
+
res.status(500).json({ error: err.message });
|
|
3934
|
+
}
|
|
3935
|
+
});
|
|
3936
|
+
router.post("/storage/aggregate", async (req, res) => {
|
|
3937
|
+
const agent = getAgent(req, res);
|
|
3938
|
+
if (!agent) return;
|
|
3939
|
+
await ensureMetaTable();
|
|
3940
|
+
try {
|
|
3941
|
+
const { table, where, operations, groupBy } = req.body;
|
|
3942
|
+
if (!table || !operations?.length) return res.status(400).json({ error: "table and operations are required" });
|
|
3943
|
+
const tableName = resolveTable(agent.id, table);
|
|
3944
|
+
if (!isSafeTable(tableName)) return res.status(403).json({ error: "Cannot aggregate system tables" });
|
|
3945
|
+
const meta = await verifyAccess(agent, tableName, res);
|
|
3946
|
+
if (!meta) return;
|
|
3947
|
+
const selects = operations.map((op, i) => {
|
|
3948
|
+
const alias = op.alias || `${op.fn}_${op.column || "all"}`;
|
|
3949
|
+
const col = op.column || "*";
|
|
3950
|
+
switch (op.fn) {
|
|
3951
|
+
case "count":
|
|
3952
|
+
return `COUNT(${col}) as ${alias}`;
|
|
3953
|
+
case "count_distinct":
|
|
3954
|
+
return `COUNT(DISTINCT ${col}) as ${alias}`;
|
|
3955
|
+
case "sum":
|
|
3956
|
+
return `SUM(${col}) as ${alias}`;
|
|
3957
|
+
case "avg":
|
|
3958
|
+
return `AVG(${col}) as ${alias}`;
|
|
3959
|
+
case "min":
|
|
3960
|
+
return `MIN(${col}) as ${alias}`;
|
|
3961
|
+
case "max":
|
|
3962
|
+
return `MAX(${col}) as ${alias}`;
|
|
3963
|
+
default:
|
|
3964
|
+
return `COUNT(*) as agg_${i}`;
|
|
3965
|
+
}
|
|
3966
|
+
});
|
|
3967
|
+
let sql = `SELECT ${groupBy ? groupBy + ", " : ""}${selects.join(", ")} FROM ${tableName}`;
|
|
3968
|
+
let params = [];
|
|
3969
|
+
if (where && Object.keys(where).length) {
|
|
3970
|
+
const w = buildWhereClause(where);
|
|
3971
|
+
sql += ` WHERE ${w.sql}`;
|
|
3972
|
+
params = w.params;
|
|
3973
|
+
}
|
|
3974
|
+
if (groupBy) sql += ` GROUP BY ${groupBy.replace(/[^a-zA-Z0-9_, ]/g, "")}`;
|
|
3975
|
+
const rows = await db.all(sql, params);
|
|
3976
|
+
res.json({ result: rows });
|
|
3977
|
+
} catch (err) {
|
|
3978
|
+
res.status(500).json({ error: err.message });
|
|
3979
|
+
}
|
|
3980
|
+
});
|
|
3981
|
+
router.post("/storage/update", async (req, res) => {
|
|
3982
|
+
const agent = getAgent(req, res);
|
|
3983
|
+
if (!agent) return;
|
|
3984
|
+
await ensureMetaTable();
|
|
3985
|
+
try {
|
|
3986
|
+
const { table, where, set } = req.body;
|
|
3987
|
+
if (!table || !where || !set) return res.status(400).json({ error: "table, where, and set are required" });
|
|
3988
|
+
const tableName = resolveTable(agent.id, table);
|
|
3989
|
+
if (!isSafeTable(tableName)) return res.status(403).json({ error: "Cannot update system tables" });
|
|
3990
|
+
const meta = await verifyAccess(agent, tableName, res);
|
|
3991
|
+
if (!meta) return;
|
|
3992
|
+
const setClauses = Object.keys(set).map((k) => `${k} = ?`);
|
|
3993
|
+
const setVals = Object.values(set).map((v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v);
|
|
3994
|
+
const w = buildWhereClause(where);
|
|
3995
|
+
await db.run(`UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${w.sql}`, [...setVals, ...w.params]);
|
|
3996
|
+
res.json({ ok: true });
|
|
3997
|
+
} catch (err) {
|
|
3998
|
+
res.status(500).json({ error: err.message });
|
|
3999
|
+
}
|
|
4000
|
+
});
|
|
4001
|
+
router.post("/storage/delete-rows", async (req, res) => {
|
|
4002
|
+
const agent = getAgent(req, res);
|
|
4003
|
+
if (!agent) return;
|
|
4004
|
+
await ensureMetaTable();
|
|
4005
|
+
try {
|
|
4006
|
+
const { table, where } = req.body;
|
|
4007
|
+
if (!table || !where || !Object.keys(where).length) return res.status(400).json({ error: "table and where are required (no blanket deletes)" });
|
|
4008
|
+
const tableName = resolveTable(agent.id, table);
|
|
4009
|
+
if (!isSafeTable(tableName)) return res.status(403).json({ error: "Cannot delete from system tables" });
|
|
4010
|
+
const meta = await verifyAccess(agent, tableName, res);
|
|
4011
|
+
if (!meta) return;
|
|
4012
|
+
const w = buildWhereClause(where);
|
|
4013
|
+
await db.run(`DELETE FROM ${tableName} WHERE ${w.sql}`, w.params);
|
|
4014
|
+
const countResult = await db.get(`SELECT COUNT(*) as cnt FROM ${tableName}`);
|
|
4015
|
+
await db.run("UPDATE agenticmail_storage_meta SET row_count = ?, updated_at = " + nowExpr(dialect) + " WHERE table_name = ?", [countResult?.cnt || 0, tableName]);
|
|
4016
|
+
res.json({ ok: true });
|
|
4017
|
+
} catch (err) {
|
|
4018
|
+
res.status(500).json({ error: err.message });
|
|
4019
|
+
}
|
|
4020
|
+
});
|
|
4021
|
+
router.post("/storage/tables/:name/truncate", async (req, res) => {
|
|
4022
|
+
const agent = getAgent(req, res);
|
|
4023
|
+
if (!agent) return;
|
|
4024
|
+
await ensureMetaTable();
|
|
4025
|
+
try {
|
|
4026
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
4027
|
+
const meta = await verifyAccess(agent, tableName, res, true);
|
|
4028
|
+
if (!meta) return;
|
|
4029
|
+
if (dialect === "sqlite" || dialect === "turso") {
|
|
4030
|
+
await db.run(`DELETE FROM ${tableName}`);
|
|
4031
|
+
} else {
|
|
4032
|
+
await db.run(`TRUNCATE TABLE ${tableName}`);
|
|
4033
|
+
}
|
|
4034
|
+
await db.run("UPDATE agenticmail_storage_meta SET row_count = 0, updated_at = " + nowExpr(dialect) + " WHERE table_name = ?", [tableName]);
|
|
4035
|
+
res.json({ ok: true });
|
|
4036
|
+
} catch (err) {
|
|
4037
|
+
res.status(500).json({ error: err.message });
|
|
4038
|
+
}
|
|
4039
|
+
});
|
|
4040
|
+
router.post("/storage/tables/:name/archive", async (req, res) => {
|
|
4041
|
+
const agent = getAgent(req, res);
|
|
4042
|
+
if (!agent) return;
|
|
4043
|
+
await ensureMetaTable();
|
|
4044
|
+
try {
|
|
4045
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
4046
|
+
const meta = await verifyAccess(agent, tableName, res, true);
|
|
4047
|
+
if (!meta) return;
|
|
4048
|
+
const archivedName = `${tableName}__archived`;
|
|
4049
|
+
await db.run(`ALTER TABLE ${tableName} RENAME TO ${archivedName}`);
|
|
4050
|
+
await db.run(`UPDATE agenticmail_storage_meta SET table_name = ?, archived_at = ${nowExpr(dialect)} WHERE table_name = ?`, [archivedName, tableName]);
|
|
4051
|
+
res.json({ ok: true, archived: archivedName });
|
|
4052
|
+
} catch (err) {
|
|
4053
|
+
res.status(500).json({ error: err.message });
|
|
4054
|
+
}
|
|
4055
|
+
});
|
|
4056
|
+
router.post("/storage/tables/:name/unarchive", async (req, res) => {
|
|
4057
|
+
const agent = getAgent(req, res);
|
|
4058
|
+
if (!agent) return;
|
|
4059
|
+
await ensureMetaTable();
|
|
4060
|
+
try {
|
|
4061
|
+
const archivedName = req.params.name.endsWith("__archived") ? req.params.name : `${resolveTable(agent.id, req.params.name)}__archived`;
|
|
4062
|
+
const meta = await db.get("SELECT * FROM agenticmail_storage_meta WHERE table_name = ?", [archivedName]);
|
|
4063
|
+
if (!meta) return res.status(404).json({ error: "Archived table not found" });
|
|
4064
|
+
if (meta.agent_id !== agent.id) return res.status(403).json({ error: "Only the owner can unarchive" });
|
|
4065
|
+
const restoredName = archivedName.replace("__archived", "");
|
|
4066
|
+
await db.run(`ALTER TABLE ${archivedName} RENAME TO ${restoredName}`);
|
|
4067
|
+
await db.run("UPDATE agenticmail_storage_meta SET table_name = ?, archived_at = NULL WHERE table_name = ?", [restoredName, archivedName]);
|
|
4068
|
+
res.json({ ok: true, restored: restoredName });
|
|
4069
|
+
} catch (err) {
|
|
4070
|
+
res.status(500).json({ error: err.message });
|
|
4071
|
+
}
|
|
4072
|
+
});
|
|
4073
|
+
router.post("/storage/tables/:name/export", async (req, res) => {
|
|
4074
|
+
const agent = getAgent(req, res);
|
|
4075
|
+
if (!agent) return;
|
|
4076
|
+
await ensureMetaTable();
|
|
4077
|
+
try {
|
|
4078
|
+
const { format, where, limit } = req.body;
|
|
4079
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
4080
|
+
const meta = await verifyAccess(agent, tableName, res);
|
|
4081
|
+
if (!meta) return;
|
|
4082
|
+
let sql = `SELECT * FROM ${tableName}`;
|
|
4083
|
+
let params = [];
|
|
4084
|
+
if (where && Object.keys(where).length) {
|
|
4085
|
+
const w = buildWhereClause(where);
|
|
4086
|
+
sql += ` WHERE ${w.sql}`;
|
|
4087
|
+
params = w.params;
|
|
4088
|
+
}
|
|
4089
|
+
if (limit) {
|
|
4090
|
+
sql += " LIMIT ?";
|
|
4091
|
+
params.push(limit);
|
|
4092
|
+
}
|
|
4093
|
+
const rows = await db.all(sql, params);
|
|
4094
|
+
if (format === "csv") {
|
|
4095
|
+
if (!rows.length) return res.json({ csv: "", rowCount: 0 });
|
|
4096
|
+
const headers = Object.keys(rows[0]);
|
|
4097
|
+
const csvLines = [headers.join(",")];
|
|
4098
|
+
for (const row of rows) {
|
|
4099
|
+
csvLines.push(headers.map((h) => {
|
|
4100
|
+
const v = row[h];
|
|
4101
|
+
if (v === null || v === void 0) return "";
|
|
4102
|
+
const s = String(v);
|
|
4103
|
+
return s.includes(",") || s.includes('"') || s.includes("\n") ? `"${s.replace(/"/g, '""')}"` : s;
|
|
4104
|
+
}).join(","));
|
|
4105
|
+
}
|
|
4106
|
+
return res.json({ csv: csvLines.join("\n"), rowCount: rows.length });
|
|
4107
|
+
}
|
|
4108
|
+
res.json({ rows, rowCount: rows.length });
|
|
4109
|
+
} catch (err) {
|
|
4110
|
+
res.status(500).json({ error: err.message });
|
|
4111
|
+
}
|
|
4112
|
+
});
|
|
4113
|
+
router.post("/storage/tables/:name/import", async (req, res) => {
|
|
4114
|
+
const agent = getAgent(req, res);
|
|
4115
|
+
if (!agent) return;
|
|
4116
|
+
await ensureMetaTable();
|
|
4117
|
+
try {
|
|
4118
|
+
const { rows, onConflict, conflictColumn } = req.body;
|
|
4119
|
+
if (!rows?.length) return res.status(400).json({ error: "rows are required" });
|
|
4120
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
4121
|
+
if (!isSafeTable(tableName)) return res.status(403).json({ error: "Cannot import into system tables" });
|
|
4122
|
+
const meta = await verifyAccess(agent, tableName, res);
|
|
4123
|
+
if (!meta) return;
|
|
4124
|
+
let imported = 0;
|
|
4125
|
+
let skipped = 0;
|
|
4126
|
+
for (const row of rows) {
|
|
4127
|
+
const keys = Object.keys(row);
|
|
4128
|
+
const vals = Object.values(row).map((v) => typeof v === "object" && v !== null ? JSON.stringify(v) : v);
|
|
4129
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
4130
|
+
try {
|
|
4131
|
+
if (onConflict === "replace" && conflictColumn) {
|
|
4132
|
+
const updateCols = keys.filter((k) => k !== conflictColumn).map((k) => `${k} = excluded.${k}`).join(", ");
|
|
4133
|
+
if (dialect === "mysql") {
|
|
4134
|
+
const dupUpdate = keys.filter((k) => k !== conflictColumn).map((k) => `${k} = VALUES(${k})`).join(", ");
|
|
4135
|
+
await db.run(`INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${dupUpdate}`, vals);
|
|
4136
|
+
} else {
|
|
4137
|
+
await db.run(`INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflictColumn}) DO UPDATE SET ${updateCols}`, vals);
|
|
4138
|
+
}
|
|
4139
|
+
} else if (onConflict === "skip" && conflictColumn) {
|
|
4140
|
+
if (dialect === "mysql") {
|
|
4141
|
+
await db.run(`INSERT IGNORE INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders})`, vals);
|
|
4142
|
+
} else {
|
|
4143
|
+
await db.run(`INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflictColumn}) DO NOTHING`, vals);
|
|
4144
|
+
}
|
|
4145
|
+
} else {
|
|
4146
|
+
await db.run(`INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders})`, vals);
|
|
4147
|
+
}
|
|
4148
|
+
imported++;
|
|
4149
|
+
} catch (e) {
|
|
4150
|
+
if (onConflict === "skip") {
|
|
4151
|
+
skipped++;
|
|
4152
|
+
continue;
|
|
4153
|
+
}
|
|
4154
|
+
throw e;
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
const countResult = await db.get(`SELECT COUNT(*) as cnt FROM ${tableName}`);
|
|
4158
|
+
await db.run("UPDATE agenticmail_storage_meta SET row_count = ?, updated_at = " + nowExpr(dialect) + " WHERE table_name = ?", [countResult?.cnt || 0, tableName]);
|
|
4159
|
+
res.json({ ok: true, imported, skipped, totalRows: countResult?.cnt || 0 });
|
|
4160
|
+
} catch (err) {
|
|
4161
|
+
res.status(500).json({ error: err.message });
|
|
4162
|
+
}
|
|
4163
|
+
});
|
|
4164
|
+
router.post("/storage/sql", async (req, res) => {
|
|
4165
|
+
const agent = getAgent(req, res);
|
|
4166
|
+
if (!agent) return;
|
|
4167
|
+
await ensureMetaTable();
|
|
4168
|
+
try {
|
|
4169
|
+
const { sql, params } = req.body;
|
|
4170
|
+
if (!sql) return res.status(400).json({ error: "sql is required" });
|
|
4171
|
+
const upper = sql.trim().toUpperCase();
|
|
4172
|
+
const dangerousPatterns = ["DROP DATABASE", "DROP SCHEMA", "GRANT ", "REVOKE ", "CREATE USER", "ALTER USER"];
|
|
4173
|
+
for (const p of dangerousPatterns) {
|
|
4174
|
+
if (upper.includes(p)) return res.status(403).json({ error: `Operation not allowed: ${p}` });
|
|
4175
|
+
}
|
|
4176
|
+
const tableRefs = sql.match(/(?:FROM|INTO|UPDATE|TABLE|JOIN)\s+(\w+)/gi);
|
|
4177
|
+
if (tableRefs) {
|
|
4178
|
+
for (const ref of tableRefs) {
|
|
4179
|
+
const tbl = ref.split(/\s+/).pop();
|
|
4180
|
+
if (!isSafeTable(tbl) && tbl !== "agenticmail_storage_meta") {
|
|
4181
|
+
return res.status(403).json({ error: `Cannot operate on table "${tbl}". Only agt_* and shared_* tables are allowed.` });
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
if (upper.startsWith("SELECT") || upper.startsWith("WITH") || upper.startsWith("EXPLAIN") || upper.startsWith("PRAGMA")) {
|
|
4186
|
+
const rows = await db.all(sql, params);
|
|
4187
|
+
return res.json({ rows, count: rows.length });
|
|
4188
|
+
} else {
|
|
4189
|
+
await db.run(sql, params);
|
|
4190
|
+
return res.json({ ok: true });
|
|
4191
|
+
}
|
|
4192
|
+
} catch (err) {
|
|
4193
|
+
res.status(500).json({ error: err.message });
|
|
4194
|
+
}
|
|
4195
|
+
});
|
|
4196
|
+
router.get("/storage/stats", async (req, res) => {
|
|
4197
|
+
const agent = getAgent(req, res);
|
|
4198
|
+
if (!agent) return;
|
|
4199
|
+
await ensureMetaTable();
|
|
4200
|
+
try {
|
|
4201
|
+
const tables = await db.all("SELECT * FROM agenticmail_storage_meta WHERE agent_id = ? AND archived_at IS NULL", [agent.id]);
|
|
4202
|
+
const archived = await db.all("SELECT * FROM agenticmail_storage_meta WHERE agent_id = ? AND archived_at IS NOT NULL", [agent.id]);
|
|
4203
|
+
let totalRows = 0;
|
|
4204
|
+
for (const t of tables) totalRows += t.row_count || 0;
|
|
4205
|
+
let dbSize = null;
|
|
4206
|
+
if (dialect === "sqlite" || dialect === "turso") {
|
|
4207
|
+
try {
|
|
4208
|
+
const pageCount = await db.get("PRAGMA page_count");
|
|
4209
|
+
const pageSize = await db.get("PRAGMA page_size");
|
|
4210
|
+
if (pageCount && pageSize) {
|
|
4211
|
+
dbSize = { bytes: (pageCount.page_count || 0) * (pageSize.page_size || 0) };
|
|
4212
|
+
}
|
|
4213
|
+
} catch {
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
res.json({
|
|
4217
|
+
tables: tables.length,
|
|
4218
|
+
archivedTables: archived.length,
|
|
4219
|
+
totalRows,
|
|
4220
|
+
dbSize,
|
|
4221
|
+
dialect
|
|
4222
|
+
});
|
|
4223
|
+
} catch (err) {
|
|
4224
|
+
res.status(500).json({ error: err.message });
|
|
4225
|
+
}
|
|
4226
|
+
});
|
|
4227
|
+
router.post("/storage/vacuum", async (req, res) => {
|
|
4228
|
+
const agent = getAgent(req, res);
|
|
4229
|
+
if (!agent) return;
|
|
4230
|
+
try {
|
|
4231
|
+
if (dialect === "sqlite" || dialect === "turso") {
|
|
4232
|
+
await db.run("VACUUM");
|
|
4233
|
+
} else if (dialect === "postgres") {
|
|
4234
|
+
await db.run("VACUUM ANALYZE");
|
|
4235
|
+
} else if (dialect === "mysql") {
|
|
4236
|
+
const tables = await db.all("SELECT table_name FROM agenticmail_storage_meta WHERE agent_id = ? AND archived_at IS NULL", [agent.id]);
|
|
4237
|
+
for (const t of tables) await db.run(`OPTIMIZE TABLE ${t.table_name}`);
|
|
4238
|
+
}
|
|
4239
|
+
res.json({ ok: true });
|
|
4240
|
+
} catch (err) {
|
|
4241
|
+
res.status(500).json({ error: err.message });
|
|
4242
|
+
}
|
|
4243
|
+
});
|
|
4244
|
+
router.post("/storage/tables/:name/analyze", async (req, res) => {
|
|
4245
|
+
const agent = getAgent(req, res);
|
|
4246
|
+
if (!agent) return;
|
|
4247
|
+
await ensureMetaTable();
|
|
4248
|
+
try {
|
|
4249
|
+
const tableName = resolveTable(agent.id, req.params.name);
|
|
4250
|
+
const meta = await verifyAccess(agent, tableName, res, true);
|
|
4251
|
+
if (!meta) return;
|
|
4252
|
+
if (dialect === "sqlite" || dialect === "turso") {
|
|
4253
|
+
await db.run(`ANALYZE ${tableName}`);
|
|
4254
|
+
} else if (dialect === "postgres") {
|
|
4255
|
+
await db.run(`ANALYZE ${tableName}`);
|
|
4256
|
+
} else if (dialect === "mysql") {
|
|
4257
|
+
await db.run(`ANALYZE TABLE ${tableName}`);
|
|
4258
|
+
}
|
|
4259
|
+
res.json({ ok: true });
|
|
4260
|
+
} catch (err) {
|
|
4261
|
+
res.status(500).json({ error: err.message });
|
|
4262
|
+
}
|
|
4263
|
+
});
|
|
4264
|
+
router.post("/storage/explain", async (req, res) => {
|
|
4265
|
+
const agent = getAgent(req, res);
|
|
4266
|
+
if (!agent) return;
|
|
4267
|
+
try {
|
|
4268
|
+
const { sql, params } = req.body;
|
|
4269
|
+
if (!sql) return res.status(400).json({ error: "sql is required" });
|
|
4270
|
+
let explainSql;
|
|
4271
|
+
if (dialect === "postgres") {
|
|
4272
|
+
explainSql = `EXPLAIN (ANALYZE false, FORMAT JSON) ${sql}`;
|
|
4273
|
+
} else if (dialect === "mysql") {
|
|
4274
|
+
explainSql = `EXPLAIN FORMAT=JSON ${sql}`;
|
|
4275
|
+
} else {
|
|
4276
|
+
explainSql = `EXPLAIN QUERY PLAN ${sql}`;
|
|
4277
|
+
}
|
|
4278
|
+
const plan = await db.all(explainSql, params);
|
|
4279
|
+
res.json({ plan });
|
|
4280
|
+
} catch (err) {
|
|
4281
|
+
res.status(500).json({ error: err.message });
|
|
4282
|
+
}
|
|
4283
|
+
});
|
|
4284
|
+
return router;
|
|
4285
|
+
}
|
|
4286
|
+
|
|
3351
4287
|
// src/app.ts
|
|
3352
4288
|
function createApp(configOverrides) {
|
|
3353
4289
|
const config = resolveConfig(configOverrides);
|
|
@@ -3399,6 +4335,7 @@ function createApp(configOverrides) {
|
|
|
3399
4335
|
app2.use("/api/agenticmail", createFeatureRoutes(db, accountManager, config, gatewayManager));
|
|
3400
4336
|
app2.use("/api/agenticmail", createTaskRoutes(db, accountManager, config));
|
|
3401
4337
|
app2.use("/api/agenticmail", createSmsRoutes(db, accountManager, config, gatewayManager));
|
|
4338
|
+
app2.use("/api/agenticmail", createStorageRoutes(db, accountManager, config));
|
|
3402
4339
|
app2.use("/api/agenticmail", (_req, res) => {
|
|
3403
4340
|
res.status(404).json({ error: "Not found" });
|
|
3404
4341
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticmail/api",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "REST API server for AgenticMail
|
|
3
|
+
"version": "0.5.42",
|
|
4
|
+
"description": "REST API server for AgenticMail — email and SMS endpoints for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|