Files
ams-backend/src/routes/export.ts

139 lines
4.3 KiB
TypeScript

import { Router } from "@oak/oak";
import { getDB } from "../db/mongo.ts";
import { authMiddleware, type AuthState } from "../middleware/auth.ts";
import type { Context, Next } from "@oak/oak";
export const exportRouter = new Router({ prefix: "/api/data" });
async function adminOnly(ctx: Context, next: Next) {
const user = (ctx.state as AuthState).user;
if (user.role !== "admin") {
ctx.response.status = 403;
ctx.response.body = { error: "Admin access required" };
return;
}
await next();
}
// Export all data as JSON
exportRouter.get("/export", authMiddleware, adminOnly, async (ctx) => {
try {
const db = await getDB();
const url = ctx.request.url;
// Which collections to export (default: all)
const collectionsParam = url.searchParams.get("collections");
const available = ["tasks", "projects", "agents", "labels", "logs", "users"];
const requested = collectionsParam
? collectionsParam.split(",").filter((c) => available.includes(c))
: available;
const data: Record<string, unknown[]> = {};
for (const col of requested) {
data[col] = await db.collection(col).find({}).toArray();
}
const exportPayload = {
version: "1.0",
exportedAt: new Date().toISOString(),
collections: requested,
data,
};
ctx.response.headers.set(
"Content-Disposition",
`attachment; filename="ams-export-${new Date().toISOString().slice(0, 10)}.json"`
);
ctx.response.type = "application/json";
ctx.response.body = exportPayload;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
ctx.response.status = 500;
ctx.response.body = { error: "Export failed", message };
}
});
// Import data from JSON
exportRouter.post("/import", authMiddleware, adminOnly, async (ctx) => {
try {
const body = await ctx.request.body.json();
const { data, mode } = body as {
data: Record<string, unknown[]>;
mode: "merge" | "replace";
};
if (!data || typeof data !== "object") {
ctx.response.status = 400;
ctx.response.body = { error: "Invalid import data" };
return;
}
const db = await getDB();
const available = ["tasks", "projects", "agents", "labels", "logs"];
const results: Record<string, { inserted: number; skipped: number }> = {};
for (const [collection, documents] of Object.entries(data)) {
// Skip users for security
if (!available.includes(collection) || !Array.isArray(documents)) continue;
if (mode === "replace") {
await db.collection(collection).deleteMany({});
}
let inserted = 0;
let skipped = 0;
for (const doc of documents) {
try {
const { _id, ...rest } = doc as Record<string, unknown>;
if (mode === "merge" && _id) {
// Try to find existing by _id, skip if exists
const existing = await db
.collection(collection)
.findOne({ _id });
if (existing) {
skipped++;
continue;
}
}
await db.collection(collection).insertOne(rest);
inserted++;
} catch {
skipped++;
}
}
results[collection] = { inserted, skipped };
}
ctx.response.body = {
success: true,
message: "Import completed",
results,
};
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
ctx.response.status = 500;
ctx.response.body = { error: "Import failed", message };
}
});
// Get export stats (what's available to export)
exportRouter.get("/stats", authMiddleware, adminOnly, async (ctx) => {
try {
const db = await getDB();
const collections = ["tasks", "projects", "agents", "labels", "logs", "users"];
const stats: Record<string, number> = {};
for (const col of collections) {
stats[col] = await db.collection(col).countDocuments({});
}
ctx.response.body = { stats };
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
ctx.response.status = 500;
ctx.response.body = { error: "Failed to get stats", message };
}
});