arcadiasuite/server/bi/staging.ts

408 lines
13 KiB
TypeScript

import type { Express, Request, Response } from "express";
import { db } from "../../db/index";
import { stagedTables, stagingMappings, migrationJobs, erpConnections } from "@shared/schema";
import { eq, desc, and } from "drizzle-orm";
import { sql } from "drizzle-orm";
export function registerStagingRoutes(app: Express): void {
app.get("/api/staging/tables", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tables = await db
.select()
.from(stagedTables)
.where(eq(stagedTables.userId, req.user!.id))
.orderBy(desc(stagedTables.createdAt));
res.json(tables);
} catch (error: any) {
console.error("Get staged tables error:", error);
res.status(500).json({ error: error.message });
}
});
app.get("/api/staging/tables/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const [table] = await db
.select()
.from(stagedTables)
.where(and(eq(stagedTables.id, parseInt(req.params.id)), eq(stagedTables.userId, req.user!.id)));
if (!table) {
return res.status(404).json({ error: "Table not found" });
}
res.json(table);
} catch (error: any) {
console.error("Get staged table error:", error);
res.status(500).json({ error: error.message });
}
});
app.get("/api/staging/tables/:id/data", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const [table] = await db
.select()
.from(stagedTables)
.where(and(eq(stagedTables.id, parseInt(req.params.id)), eq(stagedTables.userId, req.user!.id)));
if (!table) {
return res.status(404).json({ error: "Table not found" });
}
const limit = parseInt(req.query.limit as string) || 100;
const offset = parseInt(req.query.offset as string) || 0;
const safeTableName = table.tableName.replace(/[^a-zA-Z0-9_]/g, "");
const result = await db.execute(sql.raw(`SELECT * FROM "${safeTableName}" LIMIT ${limit} OFFSET ${offset}`));
const countResult = await db.execute(sql.raw(`SELECT COUNT(*) as total FROM "${safeTableName}"`));
res.json({
data: result.rows,
total: parseInt((countResult.rows[0] as any).total),
limit,
offset,
});
} catch (error: any) {
console.error("Get staged table data error:", error);
res.status(500).json({ error: error.message });
}
});
app.delete("/api/staging/tables/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const [table] = await db
.select()
.from(stagedTables)
.where(and(eq(stagedTables.id, parseInt(req.params.id)), eq(stagedTables.userId, req.user!.id)));
if (!table) {
return res.status(404).json({ error: "Table not found" });
}
try {
const safeTableName = table.tableName.replace(/[^a-zA-Z0-9_]/g, "");
await db.execute(sql.raw(`DROP TABLE IF EXISTS "${safeTableName}"`));
} catch {}
await db.delete(stagedTables).where(eq(stagedTables.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error: any) {
console.error("Delete staged table error:", error);
res.status(500).json({ error: error.message });
}
});
app.patch("/api/staging/tables/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const { name, targetErp, description, status } = req.body;
const [updated] = await db
.update(stagedTables)
.set({
name: name,
targetErp: targetErp,
description: description,
status: status,
updatedAt: new Date(),
})
.where(and(eq(stagedTables.id, parseInt(req.params.id)), eq(stagedTables.userId, req.user!.id)))
.returning();
res.json(updated);
} catch (error: any) {
console.error("Update staged table error:", error);
res.status(500).json({ error: error.message });
}
});
app.get("/api/staging/mappings", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const stagedTableId = req.query.stagedTableId
? parseInt(req.query.stagedTableId as string)
: undefined;
const userTables = await db
.select({ id: stagedTables.id })
.from(stagedTables)
.where(eq(stagedTables.userId, req.user!.id));
const userTableIds = userTables.map(t => t.id);
if (userTableIds.length === 0) {
return res.json([]);
}
const allMappings = await db
.select()
.from(stagingMappings)
.orderBy(desc(stagingMappings.createdAt));
let mappings = allMappings.filter(m =>
m.stagedTableId && userTableIds.includes(m.stagedTableId)
);
if (stagedTableId) {
if (!userTableIds.includes(stagedTableId)) {
return res.status(403).json({ error: "Access denied" });
}
mappings = mappings.filter(m => m.stagedTableId === stagedTableId);
}
res.json(mappings);
} catch (error: any) {
console.error("Get mappings error:", error);
res.status(500).json({ error: error.message });
}
});
app.post("/api/staging/mappings", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const { stagedTableId, name, targetErp, targetEntity, fieldMappings, filters, transformations } = req.body;
const [table] = await db
.select()
.from(stagedTables)
.where(and(eq(stagedTables.id, stagedTableId), eq(stagedTables.userId, req.user!.id)));
if (!table) {
return res.status(403).json({ error: "Access denied to this staged table" });
}
const [mapping] = await db
.insert(stagingMappings)
.values({
stagedTableId,
name,
targetErp,
targetEntity,
fieldMappings: JSON.stringify(fieldMappings),
filters: filters ? JSON.stringify(filters) : null,
transformations: transformations ? JSON.stringify(transformations) : null,
})
.returning();
await db
.update(stagedTables)
.set({ status: "mapped", targetErp })
.where(eq(stagedTables.id, stagedTableId));
res.json(mapping);
} catch (error: any) {
console.error("Create mapping error:", error);
res.status(500).json({ error: error.message });
}
});
app.delete("/api/staging/mappings/:id", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const [mapping] = await db
.select()
.from(stagingMappings)
.where(eq(stagingMappings.id, parseInt(req.params.id)));
if (!mapping || !mapping.stagedTableId) {
return res.status(404).json({ error: "Mapping not found" });
}
const [table] = await db
.select()
.from(stagedTables)
.where(and(eq(stagedTables.id, mapping.stagedTableId), eq(stagedTables.userId, req.user!.id)));
if (!table) {
return res.status(403).json({ error: "Access denied" });
}
await db.delete(stagingMappings).where(eq(stagingMappings.id, parseInt(req.params.id)));
res.json({ success: true });
} catch (error: any) {
console.error("Delete mapping error:", error);
res.status(500).json({ error: error.message });
}
});
app.get("/api/staging/jobs", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const jobs = await db
.select()
.from(migrationJobs)
.where(eq(migrationJobs.userId, req.user!.id))
.orderBy(desc(migrationJobs.createdAt));
res.json(jobs);
} catch (error: any) {
console.error("Get migration jobs error:", error);
res.status(500).json({ error: error.message });
}
});
app.post("/api/staging/jobs", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const { stagedTableId, mappingId, erpConnectionId } = req.body;
const [table] = await db
.select()
.from(stagedTables)
.where(and(eq(stagedTables.id, stagedTableId), eq(stagedTables.userId, req.user!.id)));
if (!table) {
return res.status(403).json({ error: "Access denied to this staged table" });
}
const [job] = await db
.insert(migrationJobs)
.values({
userId: req.user!.id,
stagedTableId,
mappingId,
erpConnectionId,
totalRecords: table.rowCount || 0,
status: "pending",
})
.returning();
res.json(job);
} catch (error: any) {
console.error("Create migration job error:", error);
res.status(500).json({ error: error.message });
}
});
app.post("/api/staging/jobs/:id/run", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const [job] = await db
.select()
.from(migrationJobs)
.where(and(eq(migrationJobs.id, parseInt(req.params.id)), eq(migrationJobs.userId, req.user!.id)));
if (!job) {
return res.status(404).json({ error: "Job not found" });
}
await db
.update(migrationJobs)
.set({ status: "running", startedAt: new Date() })
.where(eq(migrationJobs.id, job.id));
res.json({ success: true, message: "Migration job started" });
} catch (error: any) {
console.error("Run migration job error:", error);
res.status(500).json({ error: error.message });
}
});
app.get("/api/staging/erp-targets", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const erpTargets = [
{
id: "plus",
name: "Arcadia Plus",
entities: ["clientes", "produtos", "pedidos", "estoque", "financeiro", "fiscal"],
},
{
id: "next",
name: "Arcadia Next",
entities: ["clientes", "produtos", "servicos", "contratos", "ordens_servico", "financeiro"],
},
{
id: "totvs",
name: "TOTVS Protheus",
entities: ["SA1_CLIENTES", "SB1_PRODUTOS", "SC5_PEDIDOS", "SE1_TITULOS", "SF1_NOTAS"],
},
{
id: "sap",
name: "SAP Business One",
entities: ["BusinessPartners", "Items", "Orders", "Invoices", "JournalEntries"],
},
];
res.json(erpTargets);
} catch (error: any) {
console.error("Get ERP targets error:", error);
res.status(500).json({ error: error.message });
}
});
app.get("/api/staging/stats", async (req: Request, res: Response) => {
try {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
const tables = await db.select().from(stagedTables).where(eq(stagedTables.userId, req.user!.id));
const mappings = await db.select().from(stagingMappings);
const jobs = await db.select().from(migrationJobs).where(eq(migrationJobs.userId, req.user!.id));
const stats = {
totalTables: tables.length,
totalRecords: tables.reduce((sum, t) => sum + (t.rowCount || 0), 0),
mappedTables: tables.filter((t) => t.status === "mapped").length,
pendingMigrations: jobs.filter((j) => j.status === "pending").length,
completedMigrations: jobs.filter((j) => j.status === "completed").length,
bySourceType: tables.reduce((acc: Record<string, number>, t) => {
acc[t.sourceType] = (acc[t.sourceType] || 0) + 1;
return acc;
}, {}),
byTargetErp: tables.reduce((acc: Record<string, number>, t) => {
if (t.targetErp) acc[t.targetErp] = (acc[t.targetErp] || 0) + 1;
return acc;
}, {}),
};
res.json(stats);
} catch (error: any) {
console.error("Get staging stats error:", error);
res.status(500).json({ error: error.message });
}
});
}