core/validators/link.validator.ts
core/validators/link.validator.ts is a file in Bane's Lab Build Scripts. 174 lines of code and 30 definitions.
import {
EVIDENCE_IN_BOTH,
EVIDENCE_IN_NEITHER,
LEAF_REVERSE_MESSAGES,
NO_CLOSURE_REPORT,
missingReverse,
noLeafReverseMessage,
unresolvedLink,
} from "#configuration/strings/link.strings";
import {
EVIDENCE_RELATION_ID,
LEAF_RELATIONS,
LINKED_FROM_RELATION,
isListingRef,
} from "@banes-lab/web/constants/graph.constants";
import { existsSync, readFileSync } from "node:fs";
import { fileOfAddress, localAddress } from "#core/resolvers/catalog.resolver";
import type { Finding } from "#types/validation.types";
import { JSON_ROUTE } from "#configuration/constants/site.constants";
import type { Unresolved } from "#types/catalog.types";
import { isRecord } from "#core/selectors/base.selector";
import { join } from "node:path";
import { relationLabelOf } from "@banes-lab/web/strings/reference.strings";
import { walk } from "#core/loaders/asset.loader";
const JSON_EXTENSION = ".json";
interface Edge {
readonly json: string | null;
readonly ref: string | null;
}
interface Reverse {
readonly forward: string;
readonly message: (target: string) => string;
readonly reverse: string;
}
const REVERSES: readonly Reverse[] = LEAF_RELATIONS.map((relation) => {
const message = LEAF_REVERSE_MESSAGES.get(relation.forward);
if (message === undefined) {
throw new Error(noLeafReverseMessage(relation.forward));
}
return { forward: relation.forward, message, reverse: relation.reverse };
});
const LEAF_PAIRED: ReadonlySet<string> = new Set(REVERSES.flatMap((pair) => [pair.forward, pair.reverse]));
interface HeldRelation {
readonly edges: readonly Edge[];
readonly relation: string;
}
type Leaves = ReadonlyMap<string, Record<string, unknown>>;
const isNullableText = function isNullableText(value: unknown): boolean {
return value === null || typeof value === "string";
};
const isUnresolved = function isUnresolved(value: unknown): value is Unresolved {
return (
isRecord(value) &&
typeof value["label"] === "string" &&
typeof value["target"] === "string" &&
isNullableText(value["from"])
);
};
export const closureFindings = function closureFindings(report: string): Finding[] {
if (!existsSync(report)) {
return [{ file: report, message: NO_CLOSURE_REPORT }];
}
const parsed: unknown = JSON.parse(readFileSync(report, "utf8"));
const rows = Array.isArray(parsed) ? parsed.filter(isUnresolved) : [];
return rows.map((row) => ({ file: row.from ?? row.target, message: unresolvedLink(row.label, row.target) }));
};
const edgesIn = function edgesIn(value: unknown): readonly Edge[] {
return Array.isArray(value)
? value
.filter(isRecord)
.map((edge) => ({
json: typeof edge["json"] === "string" ? edge["json"] : null,
ref: typeof edge["ref"] === "string" ? edge["ref"] : null,
}))
: [];
};
const leavesOf = function leavesOf(outDir: string): Leaves {
const folder = join(outDir, JSON_ROUTE.slice(1));
if (!existsSync(folder)) {
return new Map();
}
return new Map(
walk(outDir, folder)
.filter((file) => file.endsWith(JSON_EXTENSION))
.flatMap((file) => {
const parsed: unknown = JSON.parse(readFileSync(join(outDir, file), "utf8"));
return isRecord(parsed) && typeof parsed["ref"] === "string" ? [[file, parsed] as const] : [];
}),
);
};
const targetOf = function targetOf(edge: Edge, leaves: Leaves, site: string): Record<string, unknown> | undefined {
return edge.json === null ? undefined : leaves.get(fileOfAddress(localAddress(site, edge.json)));
};
const relationsIn = function relationsIn(value: unknown): readonly HeldRelation[] {
return Array.isArray(value)
? value
.filter(isRecord)
.map((held) => ({
edges: edgesIn(held["links"]),
relation: typeof held["relation"] === "string" ? held["relation"] : "",
}))
: [];
};
const groupOf = function groupOf(leaf: Record<string, unknown>, relation: string): readonly Edge[] {
return relationsIn(leaf["relations"])
.filter((held) => held.relation === relation)
.flatMap((held) => held.edges);
};
const reverseFindings = function reverseFindings(
file: string,
leaf: Record<string, unknown>,
leaves: Leaves,
site: string,
): Finding[] {
return REVERSES.flatMap((pair) =>
groupOf(leaf, pair.forward).flatMap((edge) => {
const target = targetOf(edge, leaves, site);
if (target === undefined) {
return [];
}
const back = groupOf(target, pair.reverse).some((reverse) => reverse.ref === leaf["ref"]);
return back ? [] : [{ file, message: pair.message(edge.ref ?? edge.json ?? "") }];
}),
);
};
const recordFindings = function recordFindings(
file: string,
leaf: Record<string, unknown>,
leaves: Leaves,
site: string,
): Finding[] {
return relationsIn(leaf["relations"])
.filter((held) => !LEAF_PAIRED.has(held.relation))
.flatMap((held) =>
held.edges.flatMap((edge) => {
const target = targetOf(edge, leaves, site);
if (target === undefined || !Array.isArray(target["relations"])) {
return [];
}
const back = relationsIn(target["relations"]).some((reverse) =>
reverse.edges.some((listed) => listed.ref === leaf["ref"]),
);
const label = relationLabelOf(held.relation);
return back ? [] : [{ file, message: missingReverse(label, edge.ref ?? edge.json ?? "") }];
}),
);
};
const groundingFindings = function groundingFindings(
file: string,
leaf: Record<string, unknown>,
absent: ReadonlySet<string>,
): Finding[] {
const ref = String(leaf["ref"]);
const grounded = groupOf(leaf, EVIDENCE_RELATION_ID).length > 0;
if (grounded && absent.has(ref)) {
return [{ file, message: EVIDENCE_IN_BOTH }];
}
const named =
groupOf(leaf, LINKED_FROM_RELATION).some((edge) => edge.ref !== null && !isListingRef(edge.ref)) &&
leaf["collection"] !== undefined;
return named && !grounded && !absent.has(ref) ? [{ file, message: EVIDENCE_IN_NEITHER }] : [];
};
export const lookbackFindings = function lookbackFindings(
outDir: string,
site: string,
absent: ReadonlySet<string>,
): Finding[] {
const leaves = leavesOf(outDir);
return [...leaves.entries()].flatMap(([file, leaf]) => [
...reverseFindings(file, leaf, leaves, site),
...recordFindings(file, leaf, leaves, site),
...groundingFindings(file, leaf, absent),
]);
};