3.0 KiB
schemas — Zod schemas at every public boundary
(no submodules)
Purpose
schemas.ts exposes a Zod schema for every public option bundle in the package so callers can validate untrusted input before passing it into a constructor or public method. The NotificationRouter constructor calls NotificationRouterConfigSchema.parse(...) at the boundary; other classes leave validation as an opt-in for the caller because validating on every send is wasteful — the consumer chooses when to enforce.
Features
- Schemas for every channel config, every public bundle, and every enum:
NotificationChannelSchema,NotificationPrioritySchema,NotificationStatusSchema,RateLimitRuleSchema,ChannelConfigSchema,RoutingRuleSchema,NotificationTemplateSchema,TemplateRegistrationSchema,NotificationInputSchema,NotificationRouterConfigSchema,EmailChannelConfigSchema,SmsChannelConfigSchema,VapidKeysSchema,PushChannelConfigSchema,WebhookChannelConfigSchema,DigestChannelConfigSchema,RetryConfigSchema,QuietHoursConfigSchema,StoredPreferencesSchema,PreferenceManagerOptionsSchema. - Function-valued fields (
sendFn,getSubscriptions,fetchFn,condition,formatDigest,store,clock) are typed asz.unknown()— Zod cannot introspect callables, and the TypeScript surface is the source of truth for shapes. - Boundary defaults are NOT injected — schemas validate shape only, defaults live in the consuming classes.
Usage: Basic
import { NotificationInputSchema } from "@suaveplan/notification-channel";
const input = NotificationInputSchema.parse(req.body);
await router.send(input);
Usage: Advanced
import {
NotificationRouterConfigSchema,
StoredPreferencesSchema,
} from "@suaveplan/notification-channel";
// Validate config loaded from a YAML file:
const cfg = NotificationRouterConfigSchema.parse(yaml.parse(text));
const router = new NotificationRouter(cfg);
// Validate stored prefs at the persistence boundary:
const safe = StoredPreferencesSchema.parse(row);
await store.set(safe.userId, safe);
API Reference
Each named export is a z.ZodType. Use .parse(value) to validate-or-throw, .safeParse(value) to validate-or-result. The schemas describe shape only — runtime behaviour, defaults, and engine-pluggable callables remain owned by the consuming classes.
Implementation Notes
exactOptionalPropertyTypes: true in tsconfig.base.json makes Zod's .optional() behave correctly: an explicit undefined value is rejected when the field is absent in the input record, but is accepted on output because the consumer might spread defaults. We use z.record(z.string(), z.unknown()) for metadata-style fields because notification metadata is genuinely free-form. The RoutingRule.condition and every sendFn / fetchFn are z.unknown() so YAML-loaded configs (which cannot carry callables) parse cleanly — TypeScript will still complain at the call site if the values are not callable.