refactor(data): establish API contract guardrails
CI / Quality and Bundle Budgets (push) Has been cancelled

This commit is contained in:
2026-07-16 20:09:30 +08:00
parent cc06ed034d
commit 19d36ed5bf
24 changed files with 498 additions and 230 deletions
+51
View File
@@ -0,0 +1,51 @@
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
export async function loadJsonSource(source) {
if (/^https?:\/\//u.test(source)) {
const response = await fetch(source, {
headers: { accept: "application/json" },
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) {
throw new Error(
`Unable to load OpenAPI document: ${response.status} ${response.statusText}`,
);
}
return response.json();
}
const content = await readFile(resolve(source), "utf8");
return JSON.parse(content);
}
export function findMissingOperations(contract, openApiDocument) {
const openApiPaths = openApiDocument?.paths;
if (!openApiPaths || typeof openApiPaths !== "object") {
throw new Error("OpenAPI document does not contain a paths object");
}
const normalizedPaths = new Map(
Object.entries(openApiPaths).map(([path, pathItem]) => [
normalizePath(path),
pathItem,
]),
);
return Object.entries(contract).flatMap(([operationId, operation]) => {
const method = operation.method.toLowerCase();
const pathItem = normalizedPaths.get(normalizePath(operation.path));
if (!pathItem) {
return [{ operationId, ...operation, reason: "path is missing" }];
}
if (!pathItem[method]) {
return [{ operationId, ...operation, reason: "method is missing" }];
}
return [];
});
}
export function normalizePath(path) {
const withoutTrailingSlash = path.length > 1 ? path.replace(/\/+$/u, "") : path;
return withoutTrailingSlash.replace(/\{[^/{}]+\}/gu, "{}");
}