Files
WRNexusJS/packages/validation/src/contracts.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

213 lines
6.1 KiB
TypeScript

import type { ObjectSchema, SchemaDescriptor } from "./index.ts";
export type ContractKind =
| "api"
| "action"
| "webhook"
| "realtime"
| "queue"
| "cron"
| "pubsub"
| "plugin"
| "config"
| "env";
export interface ContractDefinition<T extends object = Record<string, unknown>> {
kind: ContractKind;
name: string;
version: number;
payload: ObjectSchema<T> | SchemaDescriptor;
consumers?: string[];
description?: string;
}
export interface ContractRecord {
kind: ContractKind;
name: string;
version: number;
payload: SchemaDescriptor;
consumers: string[];
description?: string;
}
export interface ContractSnapshot {
format: 1;
contracts: ContractRecord[];
}
export interface ContractIssue {
code:
| "WRN-CONTRACT-REMOVED"
| "WRN-CONTRACT-FIELD-REMOVED"
| "WRN-CONTRACT-FIELD-REQUIRED"
| "WRN-CONTRACT-FIELD-TYPE"
| "WRN-CONTRACT-RULE-TIGHTENED";
contract: string;
field?: string;
message: string;
consumers: string[];
}
function isDescriptor(value: ObjectSchema<object> | SchemaDescriptor): value is SchemaDescriptor {
return "type" in value && value.type === "object" && "fields" in value;
}
function keyOf(contract: Pick<ContractRecord, "kind" | "name" | "version">): string {
return `${contract.kind}:${contract.name}@${contract.version}`;
}
export function defineContract<T extends object>(
definition: ContractDefinition<T>,
): ContractDefinition<T> {
if (!definition.name.trim()) throw new TypeError("Contract name is required.");
if (!Number.isInteger(definition.version) || definition.version < 1) {
throw new TypeError("Contract version must be a positive integer.");
}
return definition;
}
export function defineEvent<T extends object>(
definition: Omit<ContractDefinition<T>, "kind"> & { kind?: "realtime" | "pubsub" },
): ContractDefinition<T> {
return defineContract({ ...definition, kind: definition.kind ?? "pubsub" });
}
export class ContractRegistry {
private readonly records = new Map<string, ContractRecord>();
register<T extends object>(definition: ContractDefinition<T>): this {
const valid = defineContract(definition);
const record: ContractRecord = {
kind: valid.kind,
name: valid.name,
version: valid.version,
payload: isDescriptor(valid.payload)
? structuredClone(valid.payload)
: valid.payload.describe(),
consumers: [...new Set(valid.consumers ?? [])].sort(),
description: valid.description,
};
const key = keyOf(record);
if (this.records.has(key)) throw new Error(`Duplicate contract: ${key}`);
this.records.set(key, record);
return this;
}
snapshot(): ContractSnapshot {
return {
format: 1,
contracts: [...this.records.values()]
.map((record) => structuredClone(record))
.sort((left, right) => keyOf(left).localeCompare(keyOf(right))),
};
}
}
function ruleStrength(
rule: SchemaDescriptor["fields"][string]["rules"][number],
): number | undefined {
if (rule.kind === "min" || rule.kind === "length") return rule.n;
if (rule.kind === "max") return -rule.n;
return undefined;
}
export function checkContractCompatibility(
previous: ContractSnapshot,
current: ContractSnapshot,
): ContractIssue[] {
const now = new Map(current.contracts.map((contract) => [keyOf(contract), contract]));
const issues: ContractIssue[] = [];
for (const before of previous.contracts) {
const contract = keyOf(before);
const after = now.get(contract);
if (!after) {
issues.push({
code: "WRN-CONTRACT-REMOVED",
contract,
message: `Contract removed: ${contract}`,
consumers: before.consumers,
});
continue;
}
for (const [field, oldField] of Object.entries(before.payload.fields)) {
const newField = after.payload.fields[field];
if (!newField) {
issues.push({
code: "WRN-CONTRACT-FIELD-REMOVED",
contract,
field,
message: `Field removed: ${field}`,
consumers: before.consumers,
});
continue;
}
if (oldField.type !== newField.type) {
issues.push({
code: "WRN-CONTRACT-FIELD-TYPE",
contract,
field,
message: `Field type changed: ${field} (${oldField.type} -> ${newField.type})`,
consumers: before.consumers,
});
}
if (oldField.optional && !newField.optional) {
issues.push({
code: "WRN-CONTRACT-FIELD-REQUIRED",
contract,
field,
message: `Optional field became required: ${field}`,
consumers: before.consumers,
});
}
for (const newRule of newField.rules) {
const oldRule = oldField.rules.find((rule) => rule.kind === newRule.kind);
const newStrength = ruleStrength(newRule);
const oldStrength = oldRule && ruleStrength(oldRule);
const enumNarrowed =
newRule.kind === "oneOf" &&
oldRule?.kind === "oneOf" &&
oldRule.values.some((value) => !newRule.values.includes(value));
const constraintAdded =
!oldRule &&
[
"email",
"url",
"uuid",
"date",
"pattern",
"integer",
"min",
"max",
"length",
"oneOf",
].includes(newRule.kind);
if (
constraintAdded ||
enumNarrowed ||
(newStrength !== undefined && oldStrength !== undefined && newStrength > oldStrength)
) {
issues.push({
code: "WRN-CONTRACT-RULE-TIGHTENED",
contract,
field,
message: `Validation tightened: ${field}.${newRule.kind}`,
consumers: before.consumers,
});
}
}
}
for (const [field, descriptor] of Object.entries(after.payload.fields)) {
if (!before.payload.fields[field] && !descriptor.optional) {
issues.push({
code: "WRN-CONTRACT-FIELD-REQUIRED",
contract,
field,
message: `Required field added: ${field}`,
consumers: before.consumers,
});
}
}
}
return issues;
}