@plasius/schema
Version:
Entity schema definition & validation helpers for Plasius ecosystem
1,766 lines (1,749 loc) • 53.4 kB
JavaScript
// src/field.builder.ts
var FieldBuilder = class _FieldBuilder {
constructor(type, options = {}) {
this.type = type;
this._shape = options.shape;
this.itemType = options.itemType;
this.refType = options.refType;
}
_type;
_storageType;
isSystem = false;
isImmutable = false;
isRequired = true;
_validator;
_description = "";
_version = "1.0.0";
_default;
_upgrade;
_shape;
itemType;
refType;
_pii = {
classification: "none",
action: "none",
logHandling: "plain",
purpose: "an ordinary value"
};
enumValues;
immutable() {
this.isImmutable = true;
return this;
}
system() {
this.isSystem = true;
return this;
}
required() {
this.isRequired = true;
return this;
}
optional() {
this.isRequired = false;
return this;
}
validator(fn) {
this._validator = fn;
return this;
}
description(desc) {
this._description = desc;
return this;
}
default(value) {
this._default = value;
this.isRequired = false;
return this;
}
/**
* Configure an upgrader used when validating older entities against a newer schema.
* The upgrader receives the current field value and version context, and should
* return { ok: true, value } with the upgraded value, or { ok: false, error }.
*/
upgrade(fn) {
this._upgrade = fn;
return this;
}
getDefault() {
const v = this._default;
return typeof v === "function" ? v() : v;
}
version(ver) {
this._version = ver;
return this;
}
/// PID informs the schema PII handling of the manner in
/// which to handle data relating to this field.
PID(pii) {
this._pii = pii;
return this;
}
min(min) {
if (this.type === "number") {
const prevValidator = this._validator;
this._validator = (value) => {
const valid = typeof value === "number" && value >= min;
return prevValidator ? prevValidator(value) && valid : valid;
};
} else if (this.type === "string") {
const prevValidator = this._validator;
this._validator = (value) => {
const valid = typeof value === "string" && value.length >= min;
return prevValidator ? prevValidator(value) && valid : valid;
};
} else if (this.type === "array") {
const prevValidator = this._validator;
this._validator = (value) => {
const valid = Array.isArray(value) && value.length >= min;
return prevValidator ? prevValidator(value) && valid : valid;
};
} else {
throw new Error(
"Min is only supported on number, string, or array fields."
);
}
return this;
}
max(max) {
if (this.type === "number") {
const prevValidator = this._validator;
this._validator = (value) => {
const valid = typeof value === "number" && value <= max;
return prevValidator ? prevValidator(value) && valid : valid;
};
} else if (this.type === "string") {
const prevValidator = this._validator;
this._validator = (value) => {
const valid = typeof value === "string" && value.length <= max;
return prevValidator ? prevValidator(value) && valid : valid;
};
} else if (this.type === "array") {
const prevValidator = this._validator;
this._validator = (value) => {
const valid = Array.isArray(value) && value.length <= max;
return prevValidator ? prevValidator(value) && valid : valid;
};
} else {
throw new Error(
"Max is only supported on number, string, or array fields."
);
}
return this;
}
pattern(regex) {
if (this.type !== "string") {
throw new Error("Pattern is only supported on string fields.");
}
const prevValidator = this._validator;
this._validator = (value) => {
const valid = typeof value === "string" && regex.test(value);
return prevValidator ? prevValidator(value) && valid : valid;
};
return this;
}
enum(values) {
if (this.type !== "string" && this.type !== "number" && !(this.type === "array" && (this.itemType?.type === "string" || this.itemType?.type === "number"))) {
throw new Error(
"Enums are only supported on string or number fields or arrays of strings or numbers."
);
}
this.enumValues = values;
return this;
}
/**
* Create a shallow clone with a different external type parameter.
* Note: shape and itemType are passed by reference (shallow). If you need
* deep isolation of nested FieldBuilders, clone them explicitly.
*/
as() {
const clone = new _FieldBuilder(this.type, {
shape: this._shape,
itemType: this.itemType,
refType: this.refType
});
clone.enumValues = this.enumValues;
clone.isImmutable = this.isImmutable;
clone.isSystem = this.isSystem;
clone.isRequired = this.isRequired;
clone._description = this._description;
clone._version = this._version;
clone._pii = this._pii;
clone._validator = this._validator;
clone._default = this._default;
clone._upgrade = this._upgrade;
return clone;
}
};
// src/validation/email.RFC5322.ts
var validateEmail = (value) => {
if (typeof value !== "string") return false;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
};
// src/validation/phone.E.164.ts
var validatePhone = (value) => {
if (typeof value !== "string") return false;
const phoneRegex = /^\+[1-9]\d{1,14}$/;
return phoneRegex.test(value);
};
// src/validation/url.WHATWG.ts
var validateUrl = (value) => {
if (typeof value !== "string") return false;
try {
const url = new URL(value);
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
return true;
} catch {
return false;
}
};
// src/validation/uuid.RFC4122.ts
var validateUUID = (value) => {
if (typeof value !== "string") return false;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(value);
};
// src/validation/dateTime.ISO8601.ts
var validateDateTimeISO = (value, options) => {
const mode = options?.mode ?? "datetime";
if (typeof value !== "string") return false;
if (mode === "datetime") {
const isoDateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
if (!isoDateTimeRegex.test(value)) return false;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return false;
return true;
}
if (mode === "date") {
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(value)) return false;
const date = new Date(value);
if (isNaN(date.getTime())) return false;
const [year, month, day] = value.split("-").map(Number);
return date.getUTCFullYear() === year && date.getUTCMonth() + 1 === month && date.getUTCDate() === day;
}
if (mode === "time") {
const timeRegex = /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?Z?$/;
return timeRegex.test(value);
}
return false;
};
// src/validation/countryCode.ISO3166.ts
var isoCountryCodes = /* @__PURE__ */ new Set([
"AD",
"AE",
"AF",
"AG",
"AI",
"AL",
"AM",
"AO",
"AQ",
"AR",
"AS",
"AT",
"AU",
"AW",
"AX",
"AZ",
"BA",
"BB",
"BD",
"BE",
"BF",
"BG",
"BH",
"BI",
"BJ",
"BL",
"BM",
"BN",
"BO",
"BQ",
"BR",
"BS",
"BT",
"BV",
"BW",
"BY",
"BZ",
"CA",
"CC",
"CD",
"CF",
"CG",
"CH",
"CI",
"CK",
"CL",
"CM",
"CN",
"CO",
"CR",
"CU",
"CV",
"CW",
"CX",
"CY",
"CZ",
"DE",
"DJ",
"DK",
"DM",
"DO",
"DZ",
"EC",
"EE",
"EG",
"EH",
"ER",
"ES",
"ET",
"FI",
"FJ",
"FM",
"FO",
"FR",
"GA",
"GB",
"GD",
"GE",
"GF",
"GG",
"GH",
"GI",
"GL",
"GM",
"GN",
"GP",
"GQ",
"GR",
"GT",
"GU",
"GW",
"GY",
"HK",
"HM",
"HN",
"HR",
"HT",
"HU",
"ID",
"IE",
"IL",
"IM",
"IN",
"IO",
"IQ",
"IR",
"IS",
"IT",
"JE",
"JM",
"JO",
"JP",
"KE",
"KG",
"KH",
"KI",
"KM",
"KN",
"KP",
"KR",
"KW",
"KY",
"KZ",
"LA",
"LB",
"LC",
"LI",
"LK",
"LR",
"LS",
"LT",
"LU",
"LV",
"LY",
"MA",
"MC",
"MD",
"ME",
"MF",
"MG",
"MH",
"MK",
"ML",
"MM",
"MN",
"MO",
"MP",
"MQ",
"MR",
"MS",
"MT",
"MU",
"MV",
"MW",
"MX",
"MY",
"MZ",
"NA",
"NC",
"NE",
"NF",
"NG",
"NI",
"NL",
"NO",
"NP",
"NR",
"NU",
"NZ",
"OM",
"PA",
"PE",
"PF",
"PG",
"PH",
"PK",
"PL",
"PM",
"PN",
"PR",
"PT",
"PW",
"PY",
"QA",
"RE",
"RO",
"RS",
"RU",
"RW",
"SA",
"SB",
"SC",
"SD",
"SE",
"SG",
"SH",
"SI",
"SJ",
"SK",
"SL",
"SM",
"SN",
"SO",
"SR",
"SS",
"ST",
"SV",
"SX",
"SY",
"SZ",
"TC",
"TD",
"TF",
"TG",
"TH",
"TJ",
"TK",
"TL",
"TM",
"TN",
"TO",
"TR",
"TT",
"TV",
"TZ",
"UA",
"UG",
"UM",
"US",
"UY",
"UZ",
"VA",
"VC",
"VE",
"VG",
"VI",
"VN",
"VU",
"WF",
"WS",
"YE",
"YT",
"ZA",
"ZM",
"ZW"
]);
var validateCountryCode = (value) => {
if (typeof value !== "string") return false;
return isoCountryCodes.has(value.toUpperCase());
};
// src/validation/currencyCode.ISO4217.ts
var isoCurrencyCodes = /* @__PURE__ */ new Set([
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BOV",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BZD",
"CAD",
"CDF",
"CHE",
"CHF",
"CHW",
"CLF",
"CLP",
"CNY",
"COP",
"COU",
"CRC",
"CUC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRU",
"MUR",
"MVR",
"MWK",
"MXN",
"MXV",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STN",
"SVC",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"USN",
"UYI",
"UYU",
"UYW",
"UZS",
"VES",
"VND",
"VUV",
"WST",
"XAF",
"XAG",
"XAU",
"XBA",
"XBB",
"XBC",
"XBD",
"XCD",
"XDR",
"XOF",
"XPD",
"XPF",
"XPT",
"XSU",
"XTS",
"XUA",
"XXX",
"YER",
"ZAR",
"ZMW",
"ZWL"
]);
var validateCurrencyCode = (value) => {
if (typeof value !== "string") return false;
return isoCurrencyCodes.has(value.toUpperCase());
};
// src/validation/generalText.OWASP.ts
function validateSafeText(value) {
if (typeof value !== "string") return false;
const trimmed = value.trim();
if (trimmed.length === 0) return false;
for (let i = 0; i < trimmed.length; i++) {
const code = trimmed.codePointAt(i);
if (code !== void 0 && (code >= 0 && code <= 31 || code === 127)) {
return false;
}
}
if (/['"<>\\{}();]/.test(trimmed)) return false;
if (/(--|\b(SELECT|UPDATE|DELETE|INSERT|DROP|ALTER|EXEC|UNION|GRANT|REVOKE)\b|\/\*|\*\/|@@)/i.test(
trimmed
))
return false;
if (trimmed.includes("\0")) return false;
if (trimmed.length > 1024) return false;
return true;
}
// src/validation/version.SEMVER2.0.0.ts
function validateSemVer(value) {
if (typeof value !== "string") return false;
return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/.test(
value
);
}
// src/validation/percentage.ISO80000-1.ts
function validatePercentage(value) {
if (typeof value !== "number") return false;
return value >= 0 && value <= 100;
}
// src/validation/richtext.OWASP.ts
function validateRichText(value) {
if (typeof value !== "string") return false;
const trimmed = value.trim();
if (trimmed.length === 0) return true;
if (/<(script|iframe|object|embed|style|link|meta|base|form|input|button|textarea|select)\b/i.test(
trimmed
)) {
return false;
}
if (/javascript:/i.test(trimmed)) {
return false;
}
if (/on\w+=["']?/i.test(trimmed)) {
return false;
}
if (trimmed.length > 1e4) return false;
return true;
}
// src/validation/name.OWASP.ts
function validateName(value) {
if (typeof value !== "string") return false;
const trimmed = value.trim();
if (trimmed.length === 0) return false;
if (trimmed.length > 256) return false;
for (const ch of trimmed) {
const cp = ch.codePointAt(0);
if (cp >= 0 && cp <= 31 || cp === 127) return false;
}
const namePattern = /^[\p{L}\p{M}'\- ]+$/u;
if (!namePattern.test(trimmed)) return false;
return true;
}
// src/validation/user.MS-GOOGLE-APPLE.ts
function validateUserId(value) {
if (typeof value !== "string") return false;
const trimmed = value.trim();
if (trimmed.length === 0) return false;
const googlePattern = /^\d{21,22}$/;
const microsoftPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const applePattern = /^[\w\-.]{6,255}$/;
return googlePattern.test(trimmed) || microsoftPattern.test(trimmed) || applePattern.test(trimmed);
}
function validateUserIdArray(value) {
if (!Array.isArray(value)) return false;
return value.every(validateUserId);
}
// src/validation/languageCode.BCP47.ts
var IsoLanguageCode = /* @__PURE__ */ ((IsoLanguageCode2) => {
IsoLanguageCode2["Afar"] = "aa";
IsoLanguageCode2["Abkhazian"] = "ab";
IsoLanguageCode2["Afrikaans"] = "af";
IsoLanguageCode2["Akan"] = "ak";
IsoLanguageCode2["Albanian"] = "sq";
IsoLanguageCode2["Amharic"] = "am";
IsoLanguageCode2["Arabic"] = "ar";
IsoLanguageCode2["Aragonese"] = "an";
IsoLanguageCode2["Armenian"] = "hy";
IsoLanguageCode2["Assamese"] = "as";
IsoLanguageCode2["Avaric"] = "av";
IsoLanguageCode2["Aymara"] = "ay";
IsoLanguageCode2["Azerbaijani"] = "az";
IsoLanguageCode2["Bashkir"] = "ba";
IsoLanguageCode2["Bambara"] = "bm";
IsoLanguageCode2["Basque"] = "eu";
IsoLanguageCode2["Belarusian"] = "be";
IsoLanguageCode2["Bengali"] = "bn";
IsoLanguageCode2["Bislama"] = "bi";
IsoLanguageCode2["Bosnian"] = "bs";
IsoLanguageCode2["Breton"] = "br";
IsoLanguageCode2["Bulgarian"] = "bg";
IsoLanguageCode2["Burmese"] = "my";
IsoLanguageCode2["Catalan"] = "ca";
IsoLanguageCode2["Chamorro"] = "ch";
IsoLanguageCode2["Chechen"] = "ce";
IsoLanguageCode2["Chinese"] = "zh";
IsoLanguageCode2["ChurchSlavic"] = "cu";
IsoLanguageCode2["Chuvash"] = "cv";
IsoLanguageCode2["Cornish"] = "kw";
IsoLanguageCode2["Corsican"] = "co";
IsoLanguageCode2["Cree"] = "cr";
IsoLanguageCode2["Croatian"] = "hr";
IsoLanguageCode2["Czech"] = "cs";
IsoLanguageCode2["Danish"] = "da";
IsoLanguageCode2["Divehi"] = "dv";
IsoLanguageCode2["Dutch"] = "nl";
IsoLanguageCode2["Dzongkha"] = "dz";
IsoLanguageCode2["English"] = "en";
IsoLanguageCode2["Esperanto"] = "eo";
IsoLanguageCode2["Estonian"] = "et";
IsoLanguageCode2["Ewe"] = "ee";
IsoLanguageCode2["Faroese"] = "fo";
IsoLanguageCode2["Fijian"] = "fj";
IsoLanguageCode2["Finnish"] = "fi";
IsoLanguageCode2["French"] = "fr";
IsoLanguageCode2["WesternFrisian"] = "fy";
IsoLanguageCode2["Fulah"] = "ff";
IsoLanguageCode2["Gaelic"] = "gd";
IsoLanguageCode2["Galician"] = "gl";
IsoLanguageCode2["Ganda"] = "lg";
IsoLanguageCode2["Georgian"] = "ka";
IsoLanguageCode2["German"] = "de";
IsoLanguageCode2["Greek"] = "el";
IsoLanguageCode2["Kalaallisut"] = "kl";
IsoLanguageCode2["Guarani"] = "gn";
IsoLanguageCode2["Gujarati"] = "gu";
IsoLanguageCode2["Haitian"] = "ht";
IsoLanguageCode2["Hausa"] = "ha";
IsoLanguageCode2["Hebrew"] = "he";
IsoLanguageCode2["Herero"] = "hz";
IsoLanguageCode2["Hindi"] = "hi";
IsoLanguageCode2["HiriMotu"] = "ho";
IsoLanguageCode2["Hungarian"] = "hu";
IsoLanguageCode2["Icelandic"] = "is";
IsoLanguageCode2["Ido"] = "io";
IsoLanguageCode2["Igbo"] = "ig";
IsoLanguageCode2["Indonesian"] = "id";
IsoLanguageCode2["Interlingua"] = "ia";
IsoLanguageCode2["Interlingue"] = "ie";
IsoLanguageCode2["Inuktitut"] = "iu";
IsoLanguageCode2["Inupiaq"] = "ik";
IsoLanguageCode2["Irish"] = "ga";
IsoLanguageCode2["Italian"] = "it";
IsoLanguageCode2["Japanese"] = "ja";
IsoLanguageCode2["Javanese"] = "jv";
IsoLanguageCode2["Kannada"] = "kn";
IsoLanguageCode2["Kanuri"] = "kr";
IsoLanguageCode2["Kashmiri"] = "ks";
IsoLanguageCode2["Kazakh"] = "kk";
IsoLanguageCode2["CentralKhmer"] = "km";
IsoLanguageCode2["Kikuyu"] = "ki";
IsoLanguageCode2["Kinyarwanda"] = "rw";
IsoLanguageCode2["Kyrgyz"] = "ky";
IsoLanguageCode2["Komi"] = "kv";
IsoLanguageCode2["Kongo"] = "kg";
IsoLanguageCode2["Korean"] = "ko";
IsoLanguageCode2["Kuanyama"] = "kj";
IsoLanguageCode2["Kurdish"] = "ku";
IsoLanguageCode2["Lao"] = "lo";
IsoLanguageCode2["Latin"] = "la";
IsoLanguageCode2["Latvian"] = "lv";
IsoLanguageCode2["Limburgan"] = "li";
IsoLanguageCode2["Lingala"] = "ln";
IsoLanguageCode2["Lithuanian"] = "lt";
IsoLanguageCode2["LubaKatanga"] = "lu";
IsoLanguageCode2["Luxembourgish"] = "lb";
IsoLanguageCode2["Macedonian"] = "mk";
IsoLanguageCode2["Malagasy"] = "mg";
IsoLanguageCode2["Malay"] = "ms";
IsoLanguageCode2["Malayalam"] = "ml";
IsoLanguageCode2["Maltese"] = "mt";
IsoLanguageCode2["Manx"] = "gv";
IsoLanguageCode2["Maori"] = "mi";
IsoLanguageCode2["Marathi"] = "mr";
IsoLanguageCode2["Marshallese"] = "mh";
IsoLanguageCode2["Mongolian"] = "mn";
IsoLanguageCode2["Nauru"] = "na";
IsoLanguageCode2["Navajo"] = "nv";
IsoLanguageCode2["NorthNdebele"] = "nd";
IsoLanguageCode2["SouthNdebele"] = "nr";
IsoLanguageCode2["Ndonga"] = "ng";
IsoLanguageCode2["Nepali"] = "ne";
IsoLanguageCode2["Norwegian"] = "no";
IsoLanguageCode2["NorwegianBokmal"] = "nb";
IsoLanguageCode2["NorwegianNynorsk"] = "nn";
IsoLanguageCode2["SichuanYi"] = "ii";
IsoLanguageCode2["Occitan"] = "oc";
IsoLanguageCode2["Ojibwa"] = "oj";
IsoLanguageCode2["Oriya"] = "or";
IsoLanguageCode2["Oromo"] = "om";
IsoLanguageCode2["Ossetian"] = "os";
IsoLanguageCode2["Pali"] = "pi";
IsoLanguageCode2["Pashto"] = "ps";
IsoLanguageCode2["Persian"] = "fa";
IsoLanguageCode2["Polish"] = "pl";
IsoLanguageCode2["Portuguese"] = "pt";
IsoLanguageCode2["Punjabi"] = "pa";
IsoLanguageCode2["Quechua"] = "qu";
IsoLanguageCode2["Romansh"] = "rm";
IsoLanguageCode2["Romanian"] = "ro";
IsoLanguageCode2["Rundi"] = "rn";
IsoLanguageCode2["Russian"] = "ru";
IsoLanguageCode2["Samoan"] = "sm";
IsoLanguageCode2["Sango"] = "sg";
IsoLanguageCode2["Sanskrit"] = "sa";
IsoLanguageCode2["Sardinian"] = "sc";
IsoLanguageCode2["Serbian"] = "sr";
IsoLanguageCode2["Shona"] = "sn";
IsoLanguageCode2["Sindhi"] = "sd";
IsoLanguageCode2["Sinhala"] = "si";
IsoLanguageCode2["Slovak"] = "sk";
IsoLanguageCode2["Slovenian"] = "sl";
IsoLanguageCode2["Somali"] = "so";
IsoLanguageCode2["SouthernSotho"] = "st";
IsoLanguageCode2["Spanish"] = "es";
IsoLanguageCode2["Sundanese"] = "su";
IsoLanguageCode2["Swahili"] = "sw";
IsoLanguageCode2["Swati"] = "ss";
IsoLanguageCode2["Swedish"] = "sv";
IsoLanguageCode2["Tagalog"] = "tl";
IsoLanguageCode2["Tahitian"] = "ty";
IsoLanguageCode2["Tajik"] = "tg";
IsoLanguageCode2["Tamil"] = "ta";
IsoLanguageCode2["Tatar"] = "tt";
IsoLanguageCode2["Telugu"] = "te";
IsoLanguageCode2["Thai"] = "th";
IsoLanguageCode2["Tibetan"] = "bo";
IsoLanguageCode2["Tigrinya"] = "ti";
IsoLanguageCode2["Tonga"] = "to";
IsoLanguageCode2["Tsonga"] = "ts";
IsoLanguageCode2["Tswana"] = "tn";
IsoLanguageCode2["Turkish"] = "tr";
IsoLanguageCode2["Turkmen"] = "tk";
IsoLanguageCode2["Twi"] = "tw";
IsoLanguageCode2["Uighur"] = "ug";
IsoLanguageCode2["Ukrainian"] = "uk";
IsoLanguageCode2["Urdu"] = "ur";
IsoLanguageCode2["Uzbek"] = "uz";
IsoLanguageCode2["Venda"] = "ve";
IsoLanguageCode2["Vietnamese"] = "vi";
IsoLanguageCode2["Volapuk"] = "vo";
IsoLanguageCode2["Walloon"] = "wa";
IsoLanguageCode2["Welsh"] = "cy";
IsoLanguageCode2["Wolof"] = "wo";
IsoLanguageCode2["Xhosa"] = "xh";
IsoLanguageCode2["Yiddish"] = "yi";
IsoLanguageCode2["Yoruba"] = "yo";
IsoLanguageCode2["Zhuang"] = "za";
IsoLanguageCode2["Zulu"] = "zu";
return IsoLanguageCode2;
})(IsoLanguageCode || {});
var ISO_LANGUAGE_SET = new Set(
Object.values(IsoLanguageCode)
);
function isIsoLanguageCode(value) {
return typeof value === "string" && ISO_LANGUAGE_SET.has(value.toLowerCase());
}
function isRegionSubtag(value) {
return /^[A-Z]{2}$/.test(value) || /^\d{3}$/.test(value);
}
function isScriptSubtag(value) {
return /^[A-Z][a-z]{3}$/.test(value);
}
function isVariantSubtag(value) {
return /^([0-9][A-Za-z0-9]{3}|[A-Za-z0-9]{5,8})$/.test(value);
}
function isExtensionSingleton(value) {
return /^[0-9A-WY-Za-wy-z]$/.test(value);
}
function isExtensionSubtag(value) {
return /^[A-Za-z0-9]{2,8}$/.test(value);
}
function isPrivateUseSingleton(value) {
return value.toLowerCase() === "x";
}
function isPrivateUseSubtag(value) {
return /^[A-Za-z0-9]{1,8}$/.test(value);
}
function validateLanguage(value) {
if (typeof value !== "string" || value.length === 0) return false;
const parts = value.split("-");
let i = 0;
const lang = parts[i];
if (!lang || !isIsoLanguageCode(lang)) return false;
i += 1;
if (i < parts.length && isScriptSubtag(parts[i])) {
i += 1;
}
if (i < parts.length && isRegionSubtag(parts[i].toUpperCase())) {
i += 1;
}
while (i < parts.length && isVariantSubtag(parts[i])) {
i += 1;
}
while (i < parts.length && isExtensionSingleton(parts[i])) {
i += 1;
if (!(i < parts.length && isExtensionSubtag(parts[i]))) return false;
while (i < parts.length && isExtensionSubtag(parts[i])) {
i += 1;
}
}
if (i < parts.length && isPrivateUseSingleton(parts[i])) {
i += 1;
if (!(i < parts.length && isPrivateUseSubtag(parts[i]))) return false;
while (i < parts.length && isPrivateUseSubtag(parts[i])) {
i += 1;
}
}
return i === parts.length;
}
// src/field.ts
var field = {
string: () => new FieldBuilder("string"),
number: () => new FieldBuilder("number"),
boolean: () => new FieldBuilder("boolean"),
object: (fields) => new FieldBuilder("object", { shape: fields }),
array: (itemType) => new FieldBuilder("array", { itemType }),
ref: (refType) => new FieldBuilder("ref", { refType }),
email: () => new FieldBuilder("string").validator(validateEmail).PID({
classification: "high",
action: "encrypt",
logHandling: "redact",
purpose: "an email address"
}).description("An email address"),
phone: () => new FieldBuilder("string").validator(validatePhone).PID({
classification: "high",
action: "encrypt",
logHandling: "redact",
purpose: "a phone number"
}).description("A phone number"),
url: () => new FieldBuilder("string").validator(validateUrl).PID({
classification: "low",
action: "hash",
logHandling: "pseudonym",
purpose: "a URL"
}).description("A URL"),
uuid: () => new FieldBuilder("string").PID({
classification: "low",
action: "hash",
logHandling: "pseudonym",
purpose: "a UUID"
}).validator(validateUUID).description("A UUID"),
dateTimeISO: () => new FieldBuilder("string").PID({
classification: "none",
action: "none",
logHandling: "plain",
purpose: "a date string"
}).validator(validateDateTimeISO).description("A date string in ISO 8601 format"),
dateISO: () => new FieldBuilder("string").PID({
classification: "none",
action: "none",
logHandling: "plain",
purpose: "a date string"
}).validator((s) => validateDateTimeISO(s, { mode: "date" })).description("A date string in ISO 8601 format (date only)"),
timeISO: () => new FieldBuilder("string").PID({
classification: "none",
action: "none",
logHandling: "plain",
purpose: "a time string"
}).validator((s) => validateDateTimeISO(s, { mode: "time" })).description("A time string in ISO 8601 format (time only)"),
richText: () => new FieldBuilder("string").PID({
classification: "low",
action: "clear",
logHandling: "omit",
purpose: "rich text content"
}).validator(validateRichText).description("Rich text content, may include basic HTML formatting"),
generalText: () => new FieldBuilder("string").PID({
classification: "none",
action: "none",
logHandling: "plain",
purpose: "Plain text content"
}).validator(validateSafeText).description("Standard text content, no HTML allowed"),
latitude: () => new FieldBuilder("number").PID({
classification: "low",
action: "clear",
logHandling: "omit",
purpose: "Latitude in decimal degrees, WGS 84 (ISO 6709)"
}).min(-90).max(90).description("Latitude in decimal degrees, WGS 84 (ISO 6709)"),
longitude: () => new FieldBuilder("number").PID({
classification: "low",
action: "clear",
logHandling: "omit",
purpose: "Longitude in decimal degrees, WGS 84 (ISO 6709)"
}).min(-180).max(180).description("Longitude in decimal degrees, WGS 84 (ISO 6709)"),
version: () => new FieldBuilder("string").validator(validateSemVer).description("A semantic version string, e.g. '1.0.0'"),
countryCode: () => new FieldBuilder("string").validator(validateCountryCode).description("An ISO 3166 country code, e.g. 'US', 'GB', 'FR'"),
languageCode: () => new FieldBuilder("string").validator(validateLanguage).description(
"An BCP 47 structured language code, primarily ISO 639-1 and optionally with ISO 3166-1 alpha-2 country code, e.g. 'en', 'en-US', 'fr', 'fr-FR'"
)
};
// src/pii.ts
function enforcePIIField(parentKey, key, value, def, enforcement = "none", errors, logger) {
const path = parentKey ? `${parentKey}.${key}` : key;
if (def?._pii?.classification === "high" && (def?.isRequired ?? true)) {
const missing = value === void 0 || value === null || value === "";
if (missing) {
const msg = `High PII field must not be empty: ${path}`;
if (enforcement === "strict") {
errors?.push(msg);
return { shortCircuit: true };
}
if (enforcement === "warn") {
logger?.warn?.(`WARN (PII Enforcement): ${msg}`);
}
}
}
return { shortCircuit: false };
}
function prepareForStorage(shape, input, encryptFn, hashFn) {
const result = {};
for (const key in shape) {
const def = shape[key];
if (!def) continue;
const value = input[key];
if (def._pii?.action === "encrypt") {
result[key + "Encrypted"] = encryptFn(value);
} else if (def._pii?.action === "hash") {
result[key + "Hash"] = hashFn(value);
} else {
result[key] = value;
}
}
return result;
}
function prepareForRead(shape, stored, decryptFn) {
const result = {};
for (const key in shape) {
const def = shape[key];
if (!def) continue;
if (def._pii?.action === "encrypt") {
result[key] = decryptFn(stored[key + "Encrypted"]);
} else {
result[key] = stored[key];
}
}
return result;
}
function sanitizeForLog(shape, data, pseudonymFn) {
const output = {};
for (const key in shape) {
const def = shape[key];
if (!def) continue;
const value = data[key];
const handling = def._pii?.logHandling;
if (handling === "omit") continue;
if (handling === "redact") {
output[key] = "[REDACTED]";
} else if (handling === "pseudonym") {
output[key] = pseudonymFn(value);
} else {
output[key] = value;
}
}
return output;
}
function getPiiAudit(shape) {
const piiFields = [];
for (const key in shape) {
const def = shape[key];
if (!def) continue;
if (def._pii && def._pii.classification !== "none") {
piiFields.push({
field: key,
classification: def._pii.classification,
action: def._pii.action,
logHandling: def._pii.logHandling,
purpose: def._pii.purpose
});
}
}
return piiFields;
}
function scrubPiiForDelete(shape, stored) {
const result = { ...stored };
for (const key in shape) {
const def = shape[key];
if (!def) continue;
if (def._pii?.action === "encrypt") {
result[key + "Encrypted"] = null;
} else if (def._pii?.action === "hash") {
result[key + "Hash"] = null;
} else if (def._pii?.action === "clear") {
result[key] = null;
}
}
return result;
}
// src/schema.ts
var globalSchemaRegistry = /* @__PURE__ */ new Map();
function cmpSemver(a, b) {
const pa = a.split(".").map((n) => parseInt(n, 10));
const pb = b.split(".").map((n) => parseInt(n, 10));
for (let i = 0; i < 3; i++) {
const ai = pa[i] ?? 0;
const bi = pb[i] ?? 0;
if (ai > bi) return 1;
if (ai < bi) return -1;
}
return 0;
}
function applySchemaUpgrade(spec, input, ctx) {
if (typeof spec === "function") {
return spec(input, ctx);
}
const steps = [...spec].sort((s1, s2) => cmpSemver(s1.to, s2.to));
let working = { ...input };
let fromVersion = ctx.from;
for (const step of steps) {
if (cmpSemver(fromVersion, step.to) < 0 && cmpSemver(step.to, ctx.to) <= 0) {
ctx.log?.(`Upgrading entity from v${fromVersion} \u2192 v${step.to}`);
const res = step.run(working, { ...ctx, from: fromVersion, to: step.to });
if (!res || res.ok !== true || !res.value) {
return {
ok: false,
errors: res?.errors?.length ? res.errors : [`Failed to upgrade entity from v${fromVersion} to v${step.to}`]
};
}
working = res.value;
fromVersion = step.to;
}
}
return { ok: true, value: working };
}
function validateEnum(parentKey, value, enumValues) {
if (!enumValues) return;
const values = Array.isArray(enumValues) ? enumValues : Array.from(enumValues);
if (Array.isArray(value)) {
const invalid = value.filter((v) => !values.includes(v));
if (invalid.length > 0) {
return `Field ${parentKey} contains invalid enum values: ${invalid.join(
", "
)}`;
}
} else {
if (!values.includes(value)) {
return `Field ${parentKey} must be one of: ${values.join(", ")}`;
}
}
}
function getEnumValues(def) {
const src = Array.isArray(def?.enum) ? def.enum : Array.isArray(def?.enumValues) ? def.enumValues : Array.isArray(def?._enum) ? def._enum : Array.isArray(def?._enumValues) ? def._enumValues : def?._enumSet instanceof Set ? Array.from(def._enumSet) : void 0;
if (!src) return void 0;
const ok = src.every(
(v) => typeof v === "string" || typeof v === "number"
);
return ok ? src : void 0;
}
function isOptional(def) {
return (def?.isRequired ?? false) === false;
}
function getValidator(def) {
return def?._validator ?? void 0;
}
function getShape(def) {
return def?._shape ?? def?.shape ?? void 0;
}
function checkMissingRequired(parentKey, key, value, def, errors) {
if (value === void 0 || value === null) {
const path = parentKey ? `${parentKey}.${key}` : key;
if (!isOptional(def)) {
errors.push(`Missing required field: ${path}`);
}
return { missing: true };
}
return { missing: false };
}
function checkImmutable(parentKey, key, value, def, existing, errors) {
if (def.isImmutable && existing && existing[key] !== void 0 && value !== existing[key]) {
const path = parentKey ? `${parentKey}.${key}` : key;
errors.push(`Field is immutable: ${path}`);
return { immutableViolation: true };
}
return { immutableViolation: false };
}
function runCustomValidator(parentKey, key, value, def, errors) {
const validator = getValidator(def);
if (validator && value !== void 0 && value !== null) {
const valid = validator(value);
if (!valid) {
const path = parentKey ? `${parentKey}.${key}` : key;
errors.push(`Invalid value for field: ${path}`);
return { invalid: true };
}
}
return { invalid: false };
}
function validateStringField(parentKey, key, value, def, errors) {
const path = parentKey ? `${parentKey}.${key}` : key;
if (typeof value !== "string") {
errors.push(`Field ${path} must be string`);
return;
}
const enumValues = getEnumValues(def);
if (Array.isArray(enumValues)) {
const enumError = validateEnum(path, value, enumValues);
if (enumError) {
errors.push(enumError);
}
}
}
function validateNumberField(parentKey, key, value, _def, errors) {
const enumPath = parentKey ? `${parentKey}.${key}` : key;
if (typeof value !== "number") {
errors.push(`Field ${enumPath} must be number`);
}
}
function validateBooleanField(parentKey, key, value, _def, errors) {
const path = parentKey ? `${parentKey}.${key}` : key;
if (typeof value !== "boolean") {
errors.push(`Field ${path} must be boolean`);
}
}
function validateObjectChildren(parentKey, obj, shape, errors) {
for (const [childKey, childDef] of Object.entries(shape)) {
const childValue = obj[childKey];
const { missing } = checkMissingRequired(
parentKey,
childKey,
childValue,
childDef,
errors
);
if (missing) continue;
const { invalid } = runCustomValidator(
parentKey,
childKey,
childValue,
childDef,
errors
);
if (invalid) continue;
validateByType(parentKey, childKey, childValue, childDef, errors);
}
}
function validateObjectField(parentKey, key, value, def, errors) {
const path = parentKey ? `${parentKey}.${key}` : key;
if (typeof value !== "object" || value === null || Array.isArray(value)) {
errors.push(`Field ${path} must be object`);
return;
}
const objShape = getShape(def);
if (objShape) validateObjectChildren(path, value, objShape, errors);
}
function validateArrayOfStrings(parentKey, key, arr, itemDef, errors) {
const path = parentKey ? `${parentKey}.${key}` : key;
if (!arr.every((v) => typeof v === "string")) {
errors.push(`Field ${path} must be string[]`);
return;
}
const enumValues = getEnumValues(itemDef);
if (Array.isArray(enumValues)) {
const enumError = validateEnum(path, arr, enumValues);
if (enumError) {
errors.push(enumError);
}
}
}
function validateArrayOfNumbers(parentKey, key, arr, itemDef, errors) {
const path = parentKey ? `${parentKey}.${key}` : key;
if (!arr.every((v) => typeof v === "number")) {
errors.push(`Field ${path} must be number[]`);
}
const enumValues = getEnumValues(itemDef);
if (Array.isArray(enumValues)) {
const enumError = validateEnum(path, arr, enumValues);
if (enumError) {
errors.push(enumError);
}
}
}
function validateArrayOfBooleans(parentKey, key, arr, _itemDef, errors) {
const path = parentKey ? `${parentKey}.${key}` : key;
if (!arr.every((v) => typeof v === "boolean")) {
errors.push(`Field ${path} must be boolean[]`);
}
}
function validateArrayOfObjects(parentKey, key, arr, itemDef, errors) {
const path = parentKey ? `${parentKey}.${key}` : key;
if (!Array.isArray(arr) || !arr.every((v) => typeof v === "object" && v !== null && !Array.isArray(v))) {
errors.push(`Field ${path} must be object[]`);
return;
}
const itemShape = getShape(itemDef);
if (!itemShape) return;
arr.forEach((item, idx) => {
const itemParent = `${path}[${idx}]`;
for (const [childKey, childDef] of Object.entries(itemShape)) {
const childValue = item[childKey];
const { missing } = checkMissingRequired(
itemParent,
childKey,
childValue,
childDef,
errors
);
if (missing) continue;
const { invalid } = runCustomValidator(
itemParent,
childKey,
childValue,
childDef,
errors
);
if (invalid) continue;
validateByType(itemParent, childKey, childValue, childDef, errors);
}
});
}
function validateArrayField(parentKey, key, value, def, errors) {
const path = parentKey ? `${parentKey}.${key}` : key;
if (!Array.isArray(value)) {
errors.push(`Field ${key} must be an array`);
return;
}
const itemType = def.itemType?.type;
if (itemType === "string")
return validateArrayOfStrings(parentKey, key, value, def.itemType, errors);
if (itemType === "number")
return validateArrayOfNumbers(parentKey, key, value, def.itemType, errors);
if (itemType === "boolean")
return validateArrayOfBooleans(parentKey, key, value, def.itemType, errors);
if (itemType === "object")
return validateArrayOfObjects(parentKey, key, value, def.itemType, errors);
if (itemType === "ref") {
const expectedType = def.itemType.refType;
value.forEach((ref, idx) => {
if (!ref || typeof ref !== "object" || ref === null || typeof ref.type !== "string" || typeof ref.id !== "string" || expectedType && ref.type !== expectedType) {
errors.push(
`Field ${path}[${idx}] must be a reference object with type: ${expectedType}`
);
}
});
const refShape = getShape(def.itemType);
if (refShape) {
value.forEach((ref, idx) => {
if (ref && typeof ref === "object" && ref !== null) {
for (const [childKey, childDef] of Object.entries(refShape)) {
const childValue = ref[childKey];
if ((childValue === void 0 || childValue === null) && !isOptional(childDef)) {
errors.push(
`Missing required field: ${path}[${idx}].${childKey}`
);
continue;
}
const childValidator = getValidator(childDef);
if (childValidator && childValue !== void 0 && childValue !== null) {
const valid = childValidator(childValue);
if (!valid)
errors.push(
`Invalid value for field: ${path}[${idx}].${childKey}`
);
}
}
}
});
}
return;
}
errors.push(`Field ${path} has unsupported array item type`);
}
function validateRefField(parentKey, key, value, _def, errors) {
if (typeof value !== "object" || value === null || typeof value.type !== "string" || typeof value.id !== "string") {
const path = parentKey ? `${parentKey}.${key}` : key;
errors.push(`Field ${path} must be { type: string; id: string }`);
}
}
function validateByType(parentKey, key, value, def, errors) {
const path = parentKey ? `${parentKey}.${key}` : key;
switch (def.type) {
case "string":
return validateStringField(parentKey, key, value, def, errors);
case "number":
return validateNumberField(parentKey, key, value, def, errors);
case "boolean":
return validateBooleanField(parentKey, key, value, def, errors);
case "object":
return validateObjectField(parentKey, key, value, def, errors);
case "array":
return validateArrayField(parentKey, key, value, def, errors);
case "ref":
return validateRefField(parentKey, key, value, def, errors);
default:
errors.push(`Unknown type for field ${path}: ${def.type}`);
}
}
function createSchema(_shape, entityType, options = {
version: "1.0.0",
table: "",
schemaValidator: () => true,
piiEnforcement: "none",
schemaUpgrade: void 0
}) {
const systemFields = {
type: field.string().immutable().system(),
version: field.string().immutable().system().validator(validateSemVer)
};
const version = options.version || "1.0.0";
const store = options.table || "";
const schemaUpgrade = options.schemaUpgrade;
const schema = {
// 🔗 Define the schema shape
_shape: {
...systemFields,
..._shape
},
// 🔗 Metadata about the schema
meta: { entityType, version },
// 🔗 Validate input against the schema
validate(input, existing) {
const errors = [];
const result = {};
if (typeof input !== "object" || input === null) {
return { valid: false, errors: ["Input must be an object"] };
}
const working = { ...input };
if (working.type == null) working.type = entityType;
if (working.version == null) working.version = version;
const fromVersion = String(working.version ?? "0.0.0");
const toVersion = String(version);
if (schemaUpgrade && cmpSemver(fromVersion, toVersion) < 0) {
const upgradeRes = applySchemaUpgrade(schemaUpgrade, { ...working }, {
from: fromVersion,
to: toVersion,
entityType,
describe: () => schema.describe()
});
if (!upgradeRes || upgradeRes.ok !== true || !upgradeRes.value) {
const errs = upgradeRes?.errors?.length ? upgradeRes.errors : [`Failed to upgrade entity from v${fromVersion} to v${toVersion}`];
errors.push(...errs);
return { valid: false, errors };
}
for (const k of Object.keys(working)) delete working[k];
Object.assign(working, upgradeRes.value);
working.type = entityType;
working.version = toVersion;
}
for (const key in schema._shape) {
const def = schema._shape[key];
const value = working[key];
if (!def) {
errors.push(`Field definition missing for: ${key}`);
continue;
}
const { missing } = checkMissingRequired("", key, value, def, errors);
if (missing) continue;
const { immutableViolation } = checkImmutable(
"",
key,
value,
def,
existing,
errors
);
if (immutableViolation) continue;
const { shortCircuit } = enforcePIIField(
"",
key,
value,
def,
options.piiEnforcement ?? "none",
errors,
console
);
if (shortCircuit) continue;
const validateField = (val) => {
const localErrors = [];
const { invalid } = runCustomValidator(
"",
key,
val,
def,
localErrors
);
if (!invalid) {
validateByType("", key, val, def, localErrors);
}
return localErrors;
};
let fieldValue = value;
let fieldErrors = validateField(fieldValue);
const entityFrom = String(working.version ?? "0.0.0");
const entityTo = String(version);
const fieldTo = String(def._version ?? entityTo);
const hasUpgrader = typeof def._upgrade === "function";
if (fieldErrors.length > 0 && hasUpgrader && cmpSemver(entityFrom, entityTo) < 0) {
const up = def._upgrade;
const res = up(fieldValue, {
entityFrom,
entityTo,
fieldTo,
fieldName: key
});
if (res && res.ok) {
fieldValue = res.value;
fieldErrors = validateField(fieldValue);
} else {
fieldErrors.push(
res?.error || `Failed to upgrade field ${key} from v${entityFrom} to v${entityTo}`
);
}
}
result[key] = fieldValue;
if (fieldErrors.length !== 0) {
errors.push(...fieldErrors);
continue;
}
}
if (errors.length === 0 && options.schemaValidator) {
const castValue = result;
if (!options.schemaValidator(castValue)) {
errors.push("Schema-level validation failed.");
}
}
return {
valid: errors.length === 0,
value: result,
errors
};
},
// specific validator for a schema to allow conditional validation
schemaValidator: options.schemaValidator,
// <== expose it here!
/**
* Runs the optional schema-level upgrade function once, without validating.
* Useful for offline migrations or testing migration logic.
*/
upgrade(input, log) {
const fromVersion = String(input?.version ?? "0.0.0");
const toVersion = String(version);
if (!schemaUpgrade || cmpSemver(fromVersion, toVersion) >= 0) {
return {
ok: true,
value: { ...input, type: entityType, version: toVersion }
};
}
const res = applySchemaUpgrade(
schemaUpgrade,
{ ...input },
{
from: fromVersion,
to: toVersion,
entityType,
describe: () => schema.describe(),
log
}
);
if (res && res.ok && res.value) {
const out = { ...res.value, type: entityType, version: toVersion };
return { ok: true, value: out };
}
return {
ok: false,
errors: res?.errors?.length ? res.errors : [`Failed to upgrade entity from v${fromVersion} to v${toVersion}`]
};
},
/**
* Recursively validates entity references defined in this schema.
*
* Traverses fields of type `ref` and arrays of `ref` and resolves each target
* entity using the provided `resolveEntity` function. When `autoValidate` is
* enabled (default) and the field's `refPolicy` is `eager`, the referenced
* entity's schema is fetched and validated via `validateComposition` up to
* `maxDepth` levels.
*
* Skips fields not listed in `onlyFields` when provided. Prevents cycles via
* a `visited` set in `validatorContext`.
*
* @param entity The root entity to validate (must include `type` and `id`).
* @param options Options controlling traversal and resolution behavior.
* @param options.resolveEntity Function to resolve a referenced entity by type and id.
* @param options.validatorContext Internal context (visited set) to prevent cycles.
* @param options.maxDepth Maximum depth for recursive validation (default: 5).
* @param options.onlyFields Optional whitelist of field names to validate.
* @param options.log Optional logger for traversal/debug output.
*
* @throws Error if a broken reference is encountered (target cannot be resolved).
*/
async validateComposition(entity, options2) {
const {
resolveEntity,
validatorContext = { visited: /* @__PURE__ */ new Set() },
maxDepth = 5,
log
} = options2;
const entityKey = `${entity.type}:${entity.id}`;
if (validatorContext.visited.has(entityKey)) {
log?.(`Skipping already visited entity ${entityKey}`);
return;
}
validatorContext.visited.add(entityKey);
log?.(`Validating composition for entity ${entityKey}`);
for (const [key, def] of Object.entries(schema._shape)) {
if (options2.onlyFields && !options2.onlyFields.includes(key)) {
log?.(`Skipping field ${key} (not in onlyFields)`);
continue;
}
const refType = def.refType;
const autoValidate = def.autoValidate !== false;
const refPolicy = def.refPolicy ?? "eager";
const value = entity[key];
if (!value) continue;
if (def.type === "ref") {
const ref = value;
const target = await options2.resolveEntity(refType, ref.id);
if (!target)
throw new Error(
`Broken reference: ${refType} ${ref.id} in field ${key}`
);
log?.(`Resolved ${refType} ${ref.id} from field ${key}`);
if (autoValidate && refPolicy === "eager") {
const targetSchema = getSchemaForType(refType);
if (options2.maxDepth > 0 && targetSchema) {
await targetSchema.validateComposition(target, {
...options2,
maxDepth: options2.maxDepth - 1
});
}
}
} else if (def.type === "array" && def.itemType?.type === "ref") {
const refs = value;
for (const ref of refs) {
const target = await options2.resolveEntity(refType, ref.id);
if (!target)
throw new Error(
`Broken reference: ${refType} ${ref.id} in field ${key}`
);
log?.(`Resolved ${refType} ${ref.id} from field ${key}`);
if (autoValidate && refPolicy === "eager") {
const targetSchema = getSchemaForType(refType);
if (options2.maxDepth > 0 && targetSchema) {
await targetSchema.validateComposition(target, {
...options2,
maxDepth: options2.maxDepth - 1
});
}
}
}
}
}
},
/**
* Returns the configured table name for this schema.
*
* @throws Error if no store/table name has been defined for this schema.
*/
tableName() {
if (!store || store === "") {
throw new Error("Store is not defined for this schema");
}
return store;
},
/**
* Transforms an input object for persistence by applying PII protection
* according to field annotations (e.g., encryption and hashing).
*
* @param input The raw entity data.
* @param encryptFn Function used to encrypt sensitive values.
* @param hashFn Function used to hash sensitive values.
* @returns A new object safe to store.
*/
prepareForStorage(input, encryptFn, hashFn) {
return prepareForStorage(_shape, input, encryptFn, hashFn);
},
/**
* Reverses storage transformations for read paths (e.g., decrypts values)
* according to PII annotations, returning a consumer-friendly object.
*
* @param stored Data retrieved from storage.
* @param decryptFn Functio