core/validators/field.validator.ts
core/validators/field.validator.ts is a file in GovLab Context. 78 lines of code and 11 definitions.
import type { DanglingField, FieldSpec, FieldType, KindSchema } from "#types/field.types";
import {
NO_ID,
SHAPE_NAMES,
misshapenField,
missingField,
nonKebabId,
refusedRecord,
} from "#configuration/strings/field.strings";
import { fieldValue, valuesAt } from "#core/selectors/field.selector";
import { isKebabId, lacksEntry } from "#core/predicates/field.predicate";
import { isPlainRecord, isStringList } from "#core/predicates/record.predicate";
import { ID_FIELD } from "#configuration/constants/field.constants";
const hasShape = function hasShape(type: FieldType, value: unknown): boolean {
if (type === "flag") {
return typeof value === "boolean";
}
if (type === "object") {
return isPlainRecord(value);
}
if (type === "records") {
return Array.isArray(value) && value.every(isPlainRecord);
}
if (type === "labels" || type === "refs") {
return isStringList(value);
}
return typeof value === "string";
};
const fieldDefect = function fieldDefect(path: string, spec: FieldSpec, value: unknown): string | null {
if (value === undefined) {
return spec.required ? missingField(path) : null;
}
return hasShape(spec.type, value) ? null : misshapenField(path, SHAPE_NAMES[spec.type]);
};
export const emptyFields = function emptyFields(schema: KindSchema, record: unknown): string[] {
return Object.entries(schema)
.filter(([path, spec]) => spec.required && lacksEntry(fieldValue(record, path)))
.map(([path]) => path);
};
export const recordDefects = function recordDefects(schema: KindSchema, raw: Record<string, unknown>): string[] {
const defects = Object.entries(schema).flatMap(([path, spec]) => {
const defect = fieldDefect(path, spec, fieldValue(raw, path));
return defect === null ? [] : [defect];
});
const id = raw[ID_FIELD];
return typeof id === "string" && !isKebabId(id) ? [...defects, nonKebabId(id)] : defects;
};
export const refuseRecord = function refuseRecord(
subject: string,
kind: string,
schema: KindSchema,
raw: Record<string, unknown>,
): void {
const defects = recordDefects(schema, raw);
if (defects.length > 0) {
const id = typeof raw[ID_FIELD] === "string" ? raw[ID_FIELD] : NO_ID;
throw new Error(refusedRecord(subject, kind, id, defects));
}
};
export const danglingFields = function danglingFields(
kind: string,
schema: KindSchema,
records: readonly { id: string }[],
members: (target: string) => ReadonlySet<string> | undefined,
): DanglingField[] {
return Object.entries(schema).flatMap(([field, spec]) => {
const { target } = spec;
const known = target === undefined ? undefined : members(target);
if (known === undefined || target === undefined) {
return [];
}
return records.flatMap((record) =>
valuesAt(record, field)
.filter((value) => !known.has(value))
.map((value) => ({ field, id: record.id, kind, target, value })),
);
});
};