Files
optima/api/utils/validateCwEndpoints.ts
T

248 lines
7.5 KiB
TypeScript

/**
* CW Endpoint Validator
*
* Validates that all ConnectWise API endpoints used by the application
* are reachable and respond correctly. Uses the same axios instance
* and credentials as the running app.
*
* Usage: bun ./utils/validateCwEndpoints.ts
*/
import { connectWiseApi } from "../src/constants";
import { prisma } from "../src/constants";
interface EndpointTest {
name: string;
method: "get" | "post" | "patch" | "delete";
url: string;
/** If true, a 404 is treated as "endpoint exists but resource doesn't" — still a pass. */
allow404?: boolean;
}
// ---------------------------------------------------------------------------
// Build test list — some require real IDs from the database
// ---------------------------------------------------------------------------
async function buildTestList(): Promise<EndpointTest[]> {
// Grab a sample opportunity from the DB to test with real IDs
const sampleOpp = await prisma.opportunity.findFirst({
where: { closedFlag: false },
select: { cwOpportunityId: true, companyId: true },
orderBy: { cwLastUpdated: "desc" },
});
const sampleCompany = await prisma.company.findFirst({
select: { cw_CompanyId: true },
});
const oppId = sampleOpp?.cwOpportunityId ?? 1;
const companyId = sampleCompany?.cw_CompanyId ?? 1;
return [
// ── Core counts (lightweight, always work) ──────────────────────────
{
name: "Opportunities count",
method: "get",
url: "/sales/opportunities/count",
},
{
name: "Activities count",
method: "get",
url: "/sales/activities/count",
},
{
name: "Companies count",
method: "get",
url: "/company/companies/count",
},
{
name: "Members count",
method: "get",
url: "/system/members/count",
},
{
name: "Catalog count",
method: "get",
url: "/procurement/catalog/count",
},
// ── Paginated list endpoints ────────────────────────────────────────
{
name: "Opportunities list (page 1, size 1)",
method: "get",
url: "/sales/opportunities?page=1&pageSize=1",
},
{
name: "Activities list (page 1, size 1)",
method: "get",
url: "/sales/activities?page=1&pageSize=1",
},
{
name: "Companies list (page 1, size 1)",
method: "get",
url: "/company/companies?page=1&pageSize=1",
},
{
name: "Members list (page 1, size 1)",
method: "get",
url: "/system/members?page=1&pageSize=1",
},
{
name: "Catalog list (page 1, size 1)",
method: "get",
url: "/procurement/catalog?page=1&pageSize=1",
},
{
name: "User-defined fields (page 1)",
method: "get",
url: "/system/userDefinedFields?pageSize=1",
},
// ── Single-resource fetches (need real IDs) ─────────────────────────
{
name: `Opportunity #${oppId}`,
method: "get",
url: `/sales/opportunities/${oppId}`,
allow404: true,
},
{
name: `Opportunity #${oppId} forecast`,
method: "get",
url: `/sales/opportunities/${oppId}/forecast`,
allow404: true,
},
{
name: `Opportunity #${oppId} notes`,
method: "get",
url: `/sales/opportunities/${oppId}/notes`,
allow404: true,
},
{
name: `Opportunity #${oppId} contacts`,
method: "get",
url: `/sales/opportunities/${oppId}/contacts`,
allow404: true,
},
{
name: `Activities for opp #${oppId}`,
method: "get",
url: `/sales/activities/count?conditions=${encodeURIComponent(`opportunity/id=${oppId}`)}`,
allow404: true,
},
{
name: `Procurement products for opp #${oppId}`,
method: "get",
url: `/procurement/products?conditions=${encodeURIComponent(`opportunity/id=${oppId}`)}&fields=id,forecastDetailId,cancelledFlag`,
allow404: true,
},
{
name: `Company #${companyId}`,
method: "get",
url: `/company/companies/${companyId}`,
allow404: true,
},
{
name: `Company #${companyId} sites`,
method: "get",
url: `/company/companies/${companyId}/sites?pageSize=1`,
allow404: true,
},
{
name: `Company #${companyId} configurations`,
method: "get",
url: `/company/configurations?conditions=${encodeURIComponent(`company/id=${companyId}`)}&pageSize=1`,
allow404: true,
},
];
}
// ---------------------------------------------------------------------------
// Runner
// ---------------------------------------------------------------------------
async function main() {
console.log(
"╔══════════════════════════════════════════════════════════════╗",
);
console.log(
"║ ConnectWise API Endpoint Validator ║",
);
console.log(
"╚══════════════════════════════════════════════════════════════╝",
);
console.log();
console.log(`Base URL: ${connectWiseApi.defaults.baseURL}`);
console.log(`Timeout: ${connectWiseApi.defaults.timeout ?? "none"}ms`);
console.log();
const tests = await buildTestList();
let passed = 0;
let failed = 0;
let warned = 0;
for (const test of tests) {
const start = performance.now();
try {
const response = await connectWiseApi.request({
method: test.method,
url: test.url,
timeout: 30_000, // Use a generous timeout for validation
});
const elapsed = Math.round(performance.now() - start);
const statusTag =
elapsed > 5000 ? `⚠️ SLOW (${elapsed}ms)` : `${elapsed}ms`;
console.log(`${test.name}${response.status} [${statusTag}]`);
if (elapsed > 5000) warned++;
passed++;
} catch (err: any) {
const elapsed = Math.round(performance.now() - start);
if (err?.isAxiosError) {
const status = err.response?.status;
const code = err.code;
if (status === 404 && test.allow404) {
console.log(
` ⚠️ ${test.name} — 404 (resource not found, endpoint OK) [${elapsed}ms]`,
);
warned++;
continue;
}
if (code === "ECONNABORTED") {
console.log(`${test.name} — TIMEOUT after ${elapsed}ms`);
} else if (code === "ECONNREFUSED") {
console.log(
`${test.name} — CONNECTION REFUSED (CW server down?)`,
);
} else if (status) {
console.log(
`${test.name} — HTTP ${status} [${elapsed}ms]: ${err.response?.data?.message ?? err.message}`,
);
} else {
console.log(
`${test.name}${code ?? err.message} [${elapsed}ms]`,
);
}
} else {
console.log(`${test.name}${err.message} [${elapsed}ms]`);
}
failed++;
}
}
console.log();
console.log("─".repeat(64));
console.log(
` Results: ${passed} passed, ${warned} warnings, ${failed} failed (${tests.length} total)`,
);
console.log("─".repeat(64));
await prisma.$disconnect();
process.exit(failed > 0 ? 1 : 0);
}
main().catch((err) => {
console.error("Fatal error:", err.message);
process.exit(1);
});