Validators
Validators check messages before they reach destinations. They enforce schemas, required fields, data formats, and business rules — catching problems early so invalid data never leaves your pipeline.
Function Signature
Section titled “Function Signature”Every validator exports a single validate function. Throw an error to reject the message; return normally (or return void) to accept it. Rejected messages are routed to error handling (retry, DLQ, or alert).
export function validate(msg: IntuMessage, ctx?: IntuContext): void;| Parameter | Type | Description |
|---|---|---|
msg | IntuMessage | The message to validate (with body, contentType, transport metadata) |
ctx | IntuContext | Optional pipeline context — map variables, logger, channel config |
// Minimal passing validator — do nothing or returnexport function validate(msg: IntuMessage): void { // Message is accepted}// Reject by throwing; the error message is recorded and the message is routed to error handlingexport function validate(msg: IntuMessage): void { if (msg.body === null || msg.body === undefined) { throw new Error("Message body is empty"); } const resource = msg.body as { resourceType?: string }; if (resource.resourceType !== "Patient") { throw new Error("Expected Patient resource, got: " + resource.resourceType); }}Configuring Validators
Section titled “Configuring Validators”Validators are configured in channel.yaml under the validator key:
validator: entrypoint: validator.tsUse a path relative to the channel directory (e.g. validator.ts) or a path from project root.
Examples
Section titled “Examples”HL7v2 Required Segments
Section titled “HL7v2 Required Segments”Verify that an HL7v2 message contains the segments your downstream system expects:
export function validate(msg: IntuMessage): void { const raw = typeof msg.body === "string" ? msg.body : ""; const errors: string[] = [];
const requiredSegments = ["MSH", "PID", "PV1", "EVN"];
for (const seg of requiredSegments) { const pattern = new RegExp(`^${seg}\\|`, "m"); if (!pattern.test(raw)) { errors.push(`Missing required segment: ${seg}`); } }
const mshFields = raw.split("\r")[0]?.split("|") ?? []; if (!mshFields[8]) errors.push("MSH.9 (Message Type) is empty"); if (!mshFields[9]) errors.push("MSH.10 (Message Control ID) is empty");
if (errors.length > 0) { throw new Error(errors.join("; ")); }}FHIR Patient Required Fields
Section titled “FHIR Patient Required Fields”Ensure a FHIR Patient resource contains the minimum fields before sending to a FHIR server:
export function validate(msg: IntuMessage): void { const patient = (typeof msg.body === "object" && msg.body !== null) ? msg.body as Record<string, unknown> : {}; const errors: string[] = [];
if (patient.resourceType !== "Patient") { errors.push(`Expected resourceType "Patient", got "${patient.resourceType}"`); }
if (!Array.isArray(patient.identifier) || patient.identifier.length === 0) { errors.push("At least one identifier is required"); }
if (!Array.isArray(patient.name) || patient.name.length === 0) { errors.push("At least one name is required"); } else { const name = patient.name[0] as Record<string, unknown>; if (!name.family) errors.push("Patient name.family is required"); if (!Array.isArray(name.given) || name.given.length === 0) errors.push("Patient name.given is required"); }
if (!patient.birthDate) { errors.push("birthDate is required"); } else if (!/^\d{4}-\d{2}-\d{2}$/.test(String(patient.birthDate))) { errors.push(`birthDate must be YYYY-MM-DD format, got "${patient.birthDate}"`); }
if (patient.gender && !["male", "female", "other", "unknown"].includes(String(patient.gender))) { errors.push(`Invalid gender value: "${patient.gender}"`); }
if (errors.length > 0) { throw new Error(errors.join("; ")); }}JSON Schema Validation
Section titled “JSON Schema Validation”Use a JSON schema library for complex structural validation:
import Ajv from "ajv";
const ajv = new Ajv({ allErrors: true });
const schema = { type: "object", required: ["patient_id", "mrn", "last_name", "first_name"], properties: { patient_id: { type: "string", minLength: 1 }, mrn: { type: "string", pattern: "^MRN-\\d{8}$" }, last_name: { type: "string", minLength: 1 }, first_name: { type: "string", minLength: 1 }, dob: { type: "string", format: "date" }, gender: { type: "string", enum: ["M", "F", "U"] }, }, additionalProperties: false,};
const validateSchema = ajv.compile(schema);
export function validate(msg: IntuMessage): void { const data = typeof msg.body === "object" && msg.body !== null ? msg.body : {}; const isValid = validateSchema(data);
if (!isValid && validateSchema.errors?.length) { const messages = validateSchema.errors.map( (err) => `${err.instancePath || "/"} ${err.message}` ); throw new Error(messages.join("; ")); }}Custom Business Rules
Section titled “Custom Business Rules”Enforce organization-specific rules that go beyond structural validation:
export function validate(msg: IntuMessage, ctx: IntuContext): void { const data = (typeof msg.body === "object" && msg.body !== null) ? msg.body as Record<string, unknown> : {}; const errors: string[] = [];
if (data.admitDate && data.dischargeDate) { if (new Date(String(data.dischargeDate)) < new Date(String(data.admitDate))) { errors.push("Discharge date cannot be before admit date"); } }
const validDepartments = ctx.globalMap.validDepartments as Set<string> | undefined; if (validDepartments && data.department && !validDepartments.has(String(data.department))) { errors.push(`Unknown department: "${data.department}"`); }
if (data.age !== undefined && (Number(data.age) < 0 || Number(data.age) > 150)) { errors.push(`Age out of plausible range: ${data.age}`); }
if (errors.length > 0) { throw new Error(errors.join("; ")); }}FHIR Observation Range Check
Section titled “FHIR Observation Range Check”Validate that observation values fall within clinically plausible ranges:
const labRanges: Record<string, { min: number; max: number; unit: string }> = { "2339-0": { min: 30, max: 500, unit: "mg/dL" }, // Glucose "2160-0": { min: 0.1, max: 20, unit: "mg/dL" }, // Creatinine "6690-2": { min: 1000, max: 50000, unit: "10*3/uL" }, // WBC "718-7": { min: 3, max: 25, unit: "g/dL" }, // Hemoglobin};
export function validate(msg: IntuMessage): void { const obs = (typeof msg.body === "object" && msg.body !== null) ? msg.body as Record<string, unknown> : {}; const errors: string[] = [];
if (obs.resourceType !== "Observation") { throw new Error("Expected Observation resource"); }
const coding = (obs.code as { coding?: Array<{ system?: string; code?: string }> })?.coding; const loincCode = coding?.find((c) => c.system === "http://loinc.org")?.code;
if (!loincCode) { errors.push("Missing LOINC code"); } else { const range = labRanges[loincCode]; const vq = obs.valueQuantity as { value?: number } | undefined; if (range && vq?.value !== undefined) { const val = vq.value; if (val < range.min || val > range.max) { errors.push( `Value ${val} ${range.unit} out of plausible range [${range.min}–${range.max}] for LOINC ${loincCode}` ); } } }
if (!(obs.subject as { reference?: string })?.reference) { errors.push("Observation.subject.reference is required"); }
if (!obs.effectiveDateTime) { errors.push("Observation.effectiveDateTime is required"); }
if (errors.length > 0) { throw new Error(errors.join("; ")); }}Validator vs. Filter
Section titled “Validator vs. Filter”Validators and filters both prevent messages from reaching destinations, but they serve different purposes:
| Validator | Filter (return null from transformer) | |
|---|---|---|
| Intent | Reject invalid data | Silently drop unwanted data |
| Feedback | Error record with reasons | No record — message disappears |
| Metrics | Counted as validation errors | Counted as filtered |
| Routing | Error handling / dead-letter queue | Nothing |
Validator — rejecting bad data (throw):
export function validate(msg: IntuMessage): void { const patient = (typeof msg.body === "object" && msg.body !== null) ? msg.body as Record<string, unknown> : {}; if (!Array.isArray(patient.identifier) || patient.identifier.length === 0) { throw new Error("Patient must have at least one identifier"); }}Filter — dropping irrelevant messages in a transformer:
export function transform(msg: IntuMessage, ctx: IntuContext): IntuMessage | null { const raw = typeof msg.body === "string" ? msg.body : ""; const messageType = raw.split("|")[8] ?? ""; if (!messageType.startsWith("ADT^A01")) return null; return msg;}