130 行
3.7 KiB
JavaScript
130 行
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export function parseThresholds(argv = process.argv.slice(2)) {
|
|
const arg = argv.find((item) => item.startsWith("--thresholds="));
|
|
const raw = arg ? arg.slice("--thresholds=".length) : "2,5,10";
|
|
return Array.from(new Set(
|
|
raw
|
|
.split(",")
|
|
.map((item) => Number(item.trim()))
|
|
.filter((item) => Number.isFinite(item) && item > 0)
|
|
.map((item) => Math.floor(item)),
|
|
)).sort((a, b) => a - b);
|
|
}
|
|
|
|
function minutesAgo(minutes, now) {
|
|
return new Date(now.getTime() - minutes * 60_000);
|
|
}
|
|
|
|
export async function buildPendingOcrSummary({
|
|
databaseReady,
|
|
thresholds,
|
|
countPending,
|
|
now = new Date(),
|
|
}) {
|
|
if (!databaseReady) {
|
|
return {
|
|
mode: "pending-ocr-readiness",
|
|
status: "SKIP",
|
|
findingCount: 1,
|
|
findings: [{ code: "database_not_configured", severity: "manual_or_config" }],
|
|
thresholds: [],
|
|
checkedAt: now.toISOString(),
|
|
};
|
|
}
|
|
|
|
const thresholdResults = [];
|
|
for (const thresholdMinutes of thresholds) {
|
|
const count = Number(await countPending({
|
|
status: "pending_ocr",
|
|
created_at: { lte: minutesAgo(thresholdMinutes, now) },
|
|
}));
|
|
thresholdResults.push({
|
|
thresholdMinutes,
|
|
count: Number.isFinite(count) && count > 0 ? Math.floor(count) : 0,
|
|
});
|
|
}
|
|
|
|
const findingCount = thresholdResults.reduce((maximum, item) => Math.max(maximum, item.count), 0);
|
|
return {
|
|
mode: "pending-ocr-readiness",
|
|
status: findingCount > 0 ? "PASS_WITH_FINDINGS" : "PASS",
|
|
findingCount,
|
|
findings: findingCount > 0
|
|
? [{ code: "pending_ocr_detected", severity: "finding", count: findingCount }]
|
|
: [],
|
|
thresholds: thresholdResults,
|
|
checkedAt: now.toISOString(),
|
|
};
|
|
}
|
|
|
|
export async function executePendingOcrCheck({
|
|
databaseReady = Boolean(process.env.DATABASE_URL?.trim()),
|
|
thresholds = parseThresholds(),
|
|
} = {}) {
|
|
if (!databaseReady) {
|
|
return buildPendingOcrSummary({ databaseReady: false, thresholds, countPending: async () => 0 });
|
|
}
|
|
|
|
const prismaClientModule = await import("../../server/src/prismaClient.js");
|
|
const createPrismaClient = prismaClientModule.createPrismaClient
|
|
?? prismaClientModule.default?.createPrismaClient;
|
|
if (typeof createPrismaClient !== "function") {
|
|
throw new Error("Prisma client factory is unavailable");
|
|
}
|
|
|
|
const { prisma, pool } = createPrismaClient();
|
|
try {
|
|
return await buildPendingOcrSummary({
|
|
databaseReady: true,
|
|
thresholds,
|
|
countPending: (where) => prisma.invoicePool.count({ where }),
|
|
});
|
|
} finally {
|
|
try {
|
|
await prisma.$disconnect();
|
|
} finally {
|
|
await pool.end();
|
|
}
|
|
}
|
|
}
|
|
|
|
function printSummary(summary, jsonOnly) {
|
|
if (jsonOnly) {
|
|
console.log(JSON.stringify(summary, null, 2));
|
|
return;
|
|
}
|
|
console.log(`[pending-ocr][${summary.status}] findings=${summary.findingCount}`);
|
|
for (const item of summary.thresholds) {
|
|
console.log(`[pending-ocr] thresholdMinutes=${item.thresholdMinutes} count=${item.count}`);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const jsonOnly = process.argv.includes("--json");
|
|
try {
|
|
const summary = await executePendingOcrCheck();
|
|
printSummary(summary, jsonOnly);
|
|
} catch {
|
|
const summary = {
|
|
mode: "pending-ocr-readiness",
|
|
status: "FAIL",
|
|
findingCount: 1,
|
|
findings: [{ code: "pending_ocr_check_failed", severity: "blocker" }],
|
|
thresholds: [],
|
|
checkedAt: new Date().toISOString(),
|
|
};
|
|
printSummary(summary, jsonOnly);
|
|
console.error("[pending-ocr][FAIL] database check failed");
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
if (isMain) {
|
|
await main();
|
|
}
|