pre-bemis
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { requireSession } from "@/lib/auth/requireSession";
|
||||
import { normalizeEvent } from "@/lib/events/normalizeEvent";
|
||||
import { invalidateMachineAuth } from "@/lib/machineAuthCache";
|
||||
|
||||
const machineIdSchema = z.string().uuid();
|
||||
|
||||
@@ -29,10 +31,63 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
}
|
||||
|
||||
function parseNumber(value: string | null, fallback: number) {
|
||||
if (value == null || value === "") return fallback;
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
type MachineFkReference = {
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
deleteRule: string;
|
||||
};
|
||||
|
||||
function quoteIdent(identifier: string) {
|
||||
return `"${identifier.replace(/"/g, "\"\"")}"`;
|
||||
}
|
||||
|
||||
async function cleanupMachineReferences(machineId: string) {
|
||||
const refs = await prisma.$queryRaw<MachineFkReference[]>`
|
||||
SELECT DISTINCT
|
||||
tc.table_name AS "tableName",
|
||||
kcu.column_name AS "columnName",
|
||||
rc.delete_rule AS "deleteRule"
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.referential_constraints rc
|
||||
ON tc.constraint_name = rc.constraint_name
|
||||
AND tc.table_schema = rc.constraint_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = 'public'
|
||||
AND rc.unique_constraint_schema = 'public'
|
||||
AND rc.unique_constraint_name IN (
|
||||
SELECT constraint_name
|
||||
FROM information_schema.table_constraints
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'Machine'
|
||||
AND constraint_type IN ('PRIMARY KEY', 'UNIQUE')
|
||||
)
|
||||
`;
|
||||
|
||||
for (const ref of refs) {
|
||||
if (ref.tableName === "Machine") continue;
|
||||
const table = quoteIdent(ref.tableName);
|
||||
const column = quoteIdent(ref.columnName);
|
||||
const rule = String(ref.deleteRule ?? "").toUpperCase();
|
||||
|
||||
if (rule === "CASCADE") continue;
|
||||
|
||||
if (rule === "SET NULL") {
|
||||
await prisma.$executeRawUnsafe(`UPDATE ${table} SET ${column} = NULL WHERE ${column} = $1`, machineId);
|
||||
continue;
|
||||
}
|
||||
|
||||
await prisma.$executeRawUnsafe(`DELETE FROM ${table} WHERE ${column} = $1`, machineId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest, { params }: { params: Promise<{ machineId: string }> }) {
|
||||
const session = await requireSession();
|
||||
if (!session) {
|
||||
@@ -158,25 +213,15 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ mach
|
||||
|
||||
const eventWindowStart = new Date(Date.now() - eventsWindowSec * 1000);
|
||||
const criticalSeverities = ["critical", "error", "high"];
|
||||
const eventWhere = {
|
||||
const eventWhereBase = {
|
||||
orgId: session.orgId,
|
||||
machineId,
|
||||
ts: { gte: eventWindowStart },
|
||||
eventType: { in: Array.from(ALLOWED_EVENT_TYPES) },
|
||||
...(eventsMode === "critical"
|
||||
? {
|
||||
OR: [
|
||||
{ eventType: "macrostop" },
|
||||
{ requiresAck: true },
|
||||
{ severity: { in: criticalSeverities } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const [rawEvents, eventsCountAll] = await Promise.all([
|
||||
prisma.machineEvent.findMany({
|
||||
where: eventWhere,
|
||||
where: eventWhereBase,
|
||||
orderBy: { ts: "desc" },
|
||||
take: eventsOnly ? 300 : 120,
|
||||
select: {
|
||||
@@ -192,15 +237,29 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ mach
|
||||
workOrderId: true,
|
||||
},
|
||||
}),
|
||||
prisma.machineEvent.count({ where: eventWhere }),
|
||||
prisma.machineEvent.count({ where: eventWhereBase }),
|
||||
]);
|
||||
|
||||
const normalized = rawEvents.map((row) =>
|
||||
normalizeEvent(row, { microMultiplier: stoppageMultiplier, macroMultiplier: macroStoppageMultiplier })
|
||||
);
|
||||
|
||||
const allowed = normalized.filter((event) => ALLOWED_EVENT_TYPES.has(event.eventType));
|
||||
const criticalEventTypes = new Set(["macrostop", "microstop", "slow-cycle", "offline", "error"]);
|
||||
const filtered =
|
||||
eventsMode === "critical"
|
||||
? allowed.filter((event) => {
|
||||
const severity = String(event.severity ?? "").toLowerCase();
|
||||
return (
|
||||
criticalEventTypes.has(event.eventType) ||
|
||||
event.requiresAck === true ||
|
||||
criticalSeverities.includes(severity)
|
||||
);
|
||||
})
|
||||
: allowed;
|
||||
|
||||
const seen = new Set<string>();
|
||||
const deduped = normalized.filter((event) => {
|
||||
const deduped = filtered.filter((event) => {
|
||||
const key = `${event.eventType}-${event.ts ?? ""}-${event.title}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
@@ -249,25 +308,185 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ mach
|
||||
return NextResponse.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
await tx.machineCycle.deleteMany({
|
||||
where: {
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
if (attempt === 0) {
|
||||
// Revoke credentials first in a committed write so ingest auth fails immediately.
|
||||
const revoked = await prisma.machine.updateMany({
|
||||
where: {
|
||||
id: machineId,
|
||||
orgId: session.orgId,
|
||||
},
|
||||
data: {
|
||||
apiKey: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (revoked.count === 0) {
|
||||
return NextResponse.json({ ok: false, error: "Machine not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
invalidateMachineAuth(machineId);
|
||||
}
|
||||
|
||||
// Avoid long interactive transactions on very large history tables (P2028 timeout).
|
||||
// This sequence is idempotent and safe to retry because apiKey is revoked first.
|
||||
await prisma.machineCycle.deleteMany({
|
||||
where: {
|
||||
machineId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.machineHeartbeat.deleteMany({
|
||||
where: {
|
||||
machineId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.machineKpiSnapshot.deleteMany({
|
||||
where: {
|
||||
machineId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.machineEvent.deleteMany({
|
||||
where: {
|
||||
machineId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.machineWorkOrder.deleteMany({
|
||||
where: {
|
||||
machineId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.machineSettings.deleteMany({
|
||||
where: {
|
||||
machineId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.settingsAudit.deleteMany({
|
||||
where: {
|
||||
machineId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.alertNotification.deleteMany({
|
||||
where: {
|
||||
machineId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.machineFinancialOverride.deleteMany({
|
||||
where: {
|
||||
machineId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.reasonEntry.deleteMany({
|
||||
where: {
|
||||
machineId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.downtimeAction.updateMany({
|
||||
where: {
|
||||
machineId,
|
||||
},
|
||||
data: {
|
||||
machineId: null,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await prisma.machine.deleteMany({
|
||||
where: {
|
||||
id: machineId,
|
||||
orgId: session.orgId,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.count === 0) {
|
||||
return NextResponse.json({ ok: false, error: "Machine not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
invalidateMachineAuth(machineId);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err: unknown) {
|
||||
const code = err instanceof Prisma.PrismaClientKnownRequestError ? err.code : undefined;
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error("DELETE /api/machines/[machineId] failed", {
|
||||
machineId,
|
||||
orgId: session.orgId,
|
||||
},
|
||||
});
|
||||
attempt,
|
||||
code,
|
||||
message,
|
||||
});
|
||||
|
||||
return tx.machine.deleteMany({
|
||||
where: {
|
||||
id: machineId,
|
||||
orgId: session.orgId,
|
||||
},
|
||||
});
|
||||
});
|
||||
if (code === "P2003") {
|
||||
if (attempt < 2) {
|
||||
try {
|
||||
await cleanupMachineReferences(machineId);
|
||||
} catch (cleanupErr: unknown) {
|
||||
const cleanupMessage = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
|
||||
console.error("DELETE /api/machines/[machineId] cleanup failed", {
|
||||
machineId,
|
||||
orgId: session.orgId,
|
||||
attempt,
|
||||
cleanupMessage,
|
||||
});
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, (attempt + 1) * 150));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.count === 0) {
|
||||
return NextResponse.json({ ok: false, error: "Machine not found" }, { status: 404 });
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error: "Machine has dependent records and could not be removed",
|
||||
code,
|
||||
},
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
if (code === "P2022") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error: "Server schema is out of date for machine delete",
|
||||
code,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
if (code === "P2028") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error: "Delete timed out while removing machine history",
|
||||
code,
|
||||
},
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
if (code) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error: "Delete failed due to database error",
|
||||
code,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: false, error: "Delete failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
return NextResponse.json({ ok: false, error: "Delete failed", code: "DELETE_RETRY_EXHAUSTED" }, { status: 500 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user