UNPKG

@sanity/validation

Version:

Validation and warning infrastructure for Sanity projects

1,287 lines (1,286 loc) • 42 kB
import cloneDeep from 'lodash/cloneDeep.js'; import get from 'lodash/get.js'; import { isKeyedObject, isReference, isTypedObject } from '@sanity/types'; import formatDate from 'date-fns/format'; import { lastValueFrom, of, defer, merge, concat, Observable } from 'rxjs'; import { catchError, mergeMap, mergeAll, toArray, map } from 'rxjs/operators'; import flatten from 'lodash/flatten.js'; import uniqBy from 'lodash/uniqBy.js'; import memoize from 'lodash/memoize.js'; var __defProp$1 = Object.defineProperty; var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$1 = (obj, key, value) => { __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; const ValidationError = class ValidationError2 { constructor(message) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; __publicField$1(this, "message"); __publicField$1(this, "paths"); __publicField$1(this, "children"); __publicField$1(this, "operation"); this.message = message; this.paths = options.paths || []; this.children = options.children; this.operation = options.operation; } cloneWithMessage(msg) { return new ValidationError2(msg, { paths: this.paths, children: this.children, operation: this.operation }); } }; var escapeRegex = string => { return string.replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, "\\$&"); }; function pathToString() { let path = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; return path.reduce((target, segment, i) => { const segmentType = typeof segment; if (segmentType === "number") { return "".concat(target, "[").concat(segment, "]"); } if (segmentType === "string") { const separator = i === 0 ? "" : "."; return "".concat(target).concat(separator).concat(segment); } if (isKeyedObject(segment)) { return "".concat(target, '[_key=="').concat(segment._key, '"]'); } throw new Error('Unsupported path segment "'.concat(segment, '"')); }, ""); } function isNonNullable$1(t) { return t !== null || t !== void 0; } function convertToValidationMarker(validatorResult, level, context) { var _a; if (!context) { throw new Error("missing context"); } if (validatorResult === true) return []; if (Array.isArray(validatorResult)) { return validatorResult.flatMap(child => convertToValidationMarker(child, level, context)).filter(isNonNullable$1); } if (typeof validatorResult === "string") { return convertToValidationMarker(new ValidationError(validatorResult), level, context); } if (!(validatorResult instanceof ValidationError)) { if (typeof (validatorResult == null ? void 0 : validatorResult.message) !== "string") { throw new Error("".concat(pathToString(context.path), ": Validator must return 'true' if valid or an error message as a string on errors")); } return convertToValidationMarker(new ValidationError(validatorResult.message, validatorResult), level, context); } const results = []; if (!((_a = validatorResult.paths) == null ? void 0 : _a.length)) { return [{ level: level || "error", item: validatorResult, path: context.path || [] }]; } return results.concat(validatorResult.paths.map(path => ({ path: (context.path || []).concat(path), level: level || "error", item: validatorResult }))); } const _toString = {}.toString; const builtIns = [Object, Function, Array, String, Boolean, Number, Date, RegExp, Error]; function isBuiltIn(_constructor) { for (let i = 0; i < builtIns.length; i++) { if (builtIns[i] === _constructor) return true; } return false; } function typeString(obj) { const stringType = _toString.call(obj).slice(8, -1); if (obj === null || obj === void 0) return stringType.toLowerCase(); const constructorType = obj.constructor; if (constructorType && !isBuiltIn(constructorType)) return constructorType.name; return stringType; } function deepEquals(a, b) { if (a === b) { return true; } if (Array.isArray(a) && Array.isArray(b)) { if (a.length != b.length) return false; for (let i = 0; i < a.length; i++) { if (!deepEquals(a[i], b[i])) { return false; } } return true; } if (Array.isArray(a) != Array.isArray(b)) { return false; } if (a && b && typeof a === "object" && typeof b === "object") { const keys = Object.keys(a); if (keys.length !== Object.keys(b).length) { return false; } if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime(); } if (a instanceof Date != b instanceof Date) { return false; } if (a instanceof RegExp && b instanceof RegExp) { return a.toString() == b.toString(); } if (a instanceof RegExp != b instanceof RegExp) { return false; } for (let i = 0; i < keys.length; i++) { if (keys[i] === "_key") { continue; } if (!Object.prototype.hasOwnProperty.call(b, keys[i])) { return false; } } for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (key === "_key") { continue; } if (!deepEquals(a[key], b[key])) { return false; } } return true; } return false; } const SLOW_VALIDATOR_TIMEOUT = 5e3; const formatValidationErrors = options => { var _a; let message; if (options.message) { message = options.message; } else if (options.results.length === 1) { message = (_a = options.results[0]) == null ? void 0 : _a.item.message; } else { message = "[".concat(options.results.map(err => err.item.message).join(" - ".concat(options.operation, " - ")), "]"); } return new ValidationError(message, { children: options.results.length > 1 ? options.results : void 0, operation: options.operation }); }; const genericValidators = { type: (expected, value, message) => { const actualType = typeString(value); if (actualType !== expected && actualType !== "undefined") { return message || 'Expected type "'.concat(expected, '", got "').concat(actualType, '"'); } return true; }, presence: (expected, value, message) => { if (value === void 0 && expected === "required") { return message || "Value is required"; } return true; }, all: async (children, value, message, context) => { const resolved = await Promise.all(children.map(child => child.validate(value, context))); const results = resolved.flat(); if (!results.length) return true; return formatValidationErrors({ message, results, operation: "AND" }); }, either: async (children, value, message, context) => { const resolved = await Promise.all(children.map(child => child.validate(value, context))); const results = resolved.flat(); if (results.length < children.length) return true; return formatValidationErrors({ message, results, operation: "OR" }); }, valid: (allowedValues, actual, message) => { const valueType = typeof actual; if (valueType === "undefined") { return true; } const value = (valueType === "number" || valueType === "string") && "".concat(actual); const strValue = value && value.length > 30 ? "".concat(value.slice(0, 30), "\u2026") : value; const defaultMessage = value ? 'Value "'.concat(strValue, '" did not match any allowed values') : "Value did not match any allowed values"; return allowedValues.some(expected => deepEquals(expected, actual)) ? true : message || defaultMessage; }, custom: async (fn, value, message, context) => { const slowTimer = setTimeout(() => { console.warn("Custom validator at ".concat(pathToString(context.path), " has taken more than ").concat(SLOW_VALIDATOR_TIMEOUT, "ms to respond")); }, SLOW_VALIDATOR_TIMEOUT); let result; try { result = await fn(value, context); } finally { clearTimeout(slowTimer); } if (typeof result === "string") return message || result; return result; } }; const booleanValidators = { ...genericValidators, presence: (flag, value, message) => { if (flag === "required" && typeof value !== "boolean") { return message || "Required"; } return true; } }; const precisionRx = /(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/; const numberValidators = { ...genericValidators, integer: (_unused, value, message) => { if (!Number.isInteger(value)) { return message || "Must be an integer"; } return true; }, precision: (limit, value, message) => { if (value === void 0) return true; const places = value.toString().match(precisionRx); const decimals = Math.max((places[1] ? places[1].length : 0) - (places[2] ? parseInt(places[2], 10) : 0), 0); if (decimals > limit) { return message || "Max precision is ".concat(limit); } return true; }, min: (minNum, value, message) => { if (value >= minNum) { return true; } return message || "Must be greater than or equal ".concat(minNum); }, max: (maxNum, value, message) => { if (value <= maxNum) { return true; } return message || "Must be less than or equal ".concat(maxNum); }, greaterThan: (num, value, message) => { if (value > num) { return true; } return message || "Must be greater than ".concat(num); }, lessThan: (maxNum, value, message) => { if (value < maxNum) { return true; } return message || "Must be less than ".concat(maxNum); } }; const DUMMY_ORIGIN = "http://sanity"; const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const isRelativeUrl = url => /^\.*\//.test(url); const stringValidators = { ...genericValidators, min: (minLength, value, message) => { if (!value || value.length >= minLength) { return true; } return message || "Must be at least ".concat(minLength, " characters long"); }, max: (maxLength, value, message) => { if (!value || value.length <= maxLength) { return true; } return message || "Must be at most ".concat(maxLength, " characters long"); }, length: (wantedLength, value, message) => { const strValue = value || ""; if (strValue.length === wantedLength) { return true; } return message || "Must be exactly ".concat(wantedLength, " characters long"); }, uri: (constraints, value, message) => { const strValue = value || ""; const { options } = constraints; const { allowCredentials, relativeOnly } = options; const allowRelative = options.allowRelative || relativeOnly; let url; try { url = allowRelative ? new URL(strValue, DUMMY_ORIGIN) : new URL(strValue); } catch (err) { return message || "Not a valid URL"; } if (relativeOnly && url.origin !== DUMMY_ORIGIN) { return message || "Only relative URLs are allowed"; } if (!allowRelative && url.origin === DUMMY_ORIGIN && isRelativeUrl(strValue)) { return message || "Relative URLs are not allowed"; } if (!allowCredentials && (url.username || url.password)) { return message || "Username/password not allowed"; } const urlScheme = url.protocol.replace(/:$/, ""); const matchesAllowedScheme = options.scheme.some(scheme => scheme.test(urlScheme)); if (!matchesAllowedScheme) { return message || "Does not match allowed protocols/schemes"; } return true; }, stringCasing: (casing, value, message) => { const strValue = value || ""; if (casing === "uppercase" && strValue !== strValue.toLocaleUpperCase()) { return message || "Must be all uppercase letters"; } if (casing === "lowercase" && strValue !== strValue.toLocaleLowerCase()) { return message || "Must be all lowercase letters"; } return true; }, presence: (flag, value, message) => { if (flag === "required" && !value) { return message || "Required"; } return true; }, regex: (options, value, message) => { const { pattern, name, invert } = options; const regName = name || '"'.concat(pattern.toString(), '"'); const strValue = value || ""; const matches = pattern.test(strValue); if (!invert && !matches || invert && matches) { const defaultMessage = invert ? "Should not match ".concat(regName, "-pattern") : "Does not match ".concat(regName, "-pattern"); return message || defaultMessage; } return true; }, email: (_unused, value, message) => { const strValue = "".concat(value || "").trim(); if (!strValue || emailRegex.test(strValue)) { return true; } return message || "Must be a valid email address"; } }; const arrayValidators = { ...genericValidators, min: (minLength, value, message) => { if (!value || value.length >= minLength) { return true; } return message || "Must have at least ".concat(minLength, " items"); }, max: (maxLength, value, message) => { if (!value || value.length <= maxLength) { return true; } return message || "Must have at most ".concat(maxLength, " items"); }, length: (wantedLength, value, message) => { if (!value || value.length === wantedLength) { return true; } return message || "Must have exactly ".concat(wantedLength, " items"); }, presence: (flag, value, message) => { if (flag === "required" && !value) { return message || "Required"; } return true; }, valid: (allowedValues, values, message) => { const valueType = typeof values; if (valueType === "undefined") { return true; } const paths = []; for (let i = 0; i < values.length; i++) { const value = values[i]; if (allowedValues.some(expected => deepEquals(expected, value))) { continue; } const pathSegment = value && value._key ? { _key: value._key } : i; paths.push([pathSegment]); } return paths.length === 0 ? true : new ValidationError(message || "Value did not match any allowed values", { paths }); }, unique: (_unused, value, message) => { const dupeIndices = []; if (!value) { return true; } for (let x = 0; x < value.length; x++) { for (let y = x + 1; y < value.length; y++) { const itemA = value[x]; const itemB = value[y]; if (!deepEquals(itemA, itemB)) { continue; } if (dupeIndices.indexOf(x) === -1) { dupeIndices.push(x); } if (dupeIndices.indexOf(y) === -1) { dupeIndices.push(y); } } } const paths = dupeIndices.map(idx => { const item = value[idx]; const pathSegment = item && item._key ? { _key: item._key } : idx; return [pathSegment]; }); return dupeIndices.length > 0 ? new ValidationError(message || "Can't be a duplicate", { paths }) : true; } }; const metaKeys = ["_key", "_type", "_weak"]; const objectValidators = { ...genericValidators, presence: (expected, value, message) => { if (expected !== "required") { return true; } const keys = value && Object.keys(value).filter(key => !metaKeys.includes(key)); if (value === void 0 || keys && keys.length === 0) { return message || "Required"; } return true; }, reference: async (_unused, value, message, context) => { if (!value) { return true; } if (!isReference(value)) { return message || "Must be a reference to a document"; } const { type, getDocumentExists } = context; if (!type) { throw new Error("`type` was not provided in validation context"); } if ("weak" in type && type.weak) { return true; } if (!getDocumentExists) { throw new Error("`getDocumentExists` was not provided in validation context"); } const exists = await getDocumentExists({ id: value._ref }); if (!exists) { return "This reference must be published"; } return true; }, assetRequired: (flag, value, message) => { if (!value || !value.asset || !value.asset._ref) { const assetType = flag.assetType || "Asset"; return message || "".concat(assetType, " required"); } return true; } }; function isRecord$1(obj) { return typeof obj === "object" && obj !== null && !Array.isArray(obj); } const isoDate = /^(?:[-+]\d{2})?(?:\d{4}(?!\d{2}\b))(?:(-?)(?:(?:0[1-9]|1[0-2])(?:\1(?:[12]\d|0[1-9]|3[01]))?|W(?:[0-4]\d|5[0-2])(?:-?[1-7])?|(?:00[1-9]|0[1-9]\d|[12]\d{2}|3(?:[0-5]\d|6[1-6])))(?![T]$|[T][\d]+Z$)(?:[T\s](?:(?:(?:[01]\d|2[0-3])(?:(:?)[0-5]\d)?|24:?00)(?:[.,]\d+(?!:))?)(?:\2[0-5]\d(?:[.,]\d+)?)?(?:[Z]|(?:[+-])(?:[01]\d|2[0-3])(?::?[0-5]\d)?)?)?)?$/; const getFormattedDate = function () { let type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ""; let value = arguments.length > 1 ? arguments[1] : undefined; let options = arguments.length > 2 ? arguments[2] : undefined; let format = "yyyy-MM-dd"; if (options && options.dateFormat) { format = options.dateFormat; } if (type === "date") { return formatDate(new Date(value), format); } if (options && options.timeFormat) { format += " ".concat(options.timeFormat); } else { format += " HH:mm"; } return formatDate(new Date(value), format); }; function parseDate(date) { let throwOnError = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; if (!date) return null; if (date === "now") return /* @__PURE__ */new Date(); const parsed = new Date(date); const isInvalid = isNaN(parsed.getTime()); if (isInvalid && throwOnError) { throw new Error('Unable to parse "'.concat(date, '" to a date')); } return isInvalid ? null : parsed; } const dateValidators = { ...genericValidators, type: (_unused, value, message) => { const strVal = "".concat(value); if (!strVal || isoDate.test(value)) { return true; } return message || "Must be a valid ISO-8601 formatted date string"; }, min: (minDate, value, message, context) => { const dateVal = parseDate(value); if (!dateVal) { return true; } if (!value || dateVal >= parseDate(minDate, true)) { return true; } if (!context.type) { throw new Error("`type` was not provided in validation context."); } const dateTimeOptions = isRecord$1(context.type.options) ? context.type.options : {}; const date = getFormattedDate(context.type.name, minDate, dateTimeOptions); return message || "Must be at or after ".concat(date); }, max: (maxDate, value, message, context) => { const dateVal = parseDate(value); if (!dateVal) { return true; } if (!value || dateVal <= parseDate(maxDate, true)) { return true; } if (!context.type) { throw new Error("`type` was not provided in validation context."); } const dateTimeOptions = isRecord$1(context.type.options) ? context.type.options : {}; const date = getFormattedDate(context.type.name, maxDate, dateTimeOptions); return message || "Must be at or before ".concat(date); } }; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; var _a; const typeValidators = { Boolean: booleanValidators, Number: numberValidators, String: stringValidators, Array: arrayValidators, Object: objectValidators, Date: dateValidators }; const getBaseType = type => { return type && type.type ? getBaseType(type.type) : type; }; const isFieldRef = constraint => { if (typeof constraint !== "object" || !constraint) return false; return constraint.type === Rule.FIELD_REF; }; const EMPTY_ARRAY = []; const FIELD_REF = Symbol("FIELD_REF"); const ruleConstraintTypes$1 = ["Array", "Boolean", "Date", "Number", "Object", "String"]; const Rule = (_a = class { constructor(typeDef) { __publicField(this, "_type"); __publicField(this, "_level"); __publicField(this, "_required"); __publicField(this, "_typeDef"); __publicField(this, "_message"); __publicField(this, "_rules", []); __publicField(this, "_fieldRules"); // Alias to static method, since we often have access to an _instance_ of a rule but not the actual Rule class __publicField(this, "valueOfField", _a.valueOfField.bind(_a)); this._typeDef = typeDef; this.reset(); } _mergeRequired(next) { if (this._required === "required" || next._required === "required") return "required"; if (this._required === "optional" || next._required === "optional") return "optional"; return void 0; } error(message) { const rule = this.clone(); rule._level = "error"; rule._message = message || void 0; return rule; } warning(message) { const rule = this.clone(); rule._level = "warning"; rule._message = message || void 0; return rule; } info(message) { const rule = this.clone(); rule._level = "info"; rule._message = message || void 0; return rule; } reset() { this._type = this._type || void 0; this._rules = (this._rules || []).filter(rule => rule.flag === "type"); this._message = void 0; this._required = void 0; this._level = "error"; this._fieldRules = void 0; return this; } isRequired() { return this._required === "required"; } clone() { const rule = new _a(); rule._type = this._type; rule._message = this._message; rule._required = this._required; rule._rules = cloneDeep(this._rules); rule._level = this._level; rule._fieldRules = this._fieldRules; rule._typeDef = this._typeDef; return rule; } cloneWithRules(rules) { const rule = this.clone(); const newRules = /* @__PURE__ */new Set(); rules.forEach(curr => { if (curr.flag === "type") { rule._type = curr.constraint; } newRules.add(curr.flag); }); rule._rules = rule._rules.filter(curr => { const disallowDuplicate = ["type", "uri", "email"].includes(curr.flag); const isDuplicate = newRules.has(curr.flag); return !(disallowDuplicate && isDuplicate); }).concat(rules); return rule; } merge(rule) { if (this._type && rule._type && this._type !== rule._type) { throw new Error("merge() failed: conflicting types"); } const newRule = this.cloneWithRules(rule._rules); newRule._type = this._type || rule._type; newRule._message = this._message || rule._message; newRule._required = this._mergeRequired(rule); newRule._level = this._level === "error" ? rule._level : this._level; return newRule; } // Validation flag setters type(targetType) { const type = "".concat(targetType.slice(0, 1).toUpperCase()).concat(targetType.slice(1)); if (!ruleConstraintTypes$1.includes(type)) { throw new Error('Unknown type "'.concat(targetType, '"')); } const rule = this.cloneWithRules([{ flag: "type", constraint: type }]); rule._type = type; return rule; } all(children) { return this.cloneWithRules([{ flag: "all", constraint: children }]); } either(children) { return this.cloneWithRules([{ flag: "either", constraint: children }]); } // Shared rules optional() { const rule = this.cloneWithRules([{ flag: "presence", constraint: "optional" }]); rule._required = "optional"; return rule; } required() { const rule = this.cloneWithRules([{ flag: "presence", constraint: "required" }]); rule._required = "required"; return rule; } custom(fn) { return this.cloneWithRules([{ flag: "custom", constraint: fn }]); } min(len) { return this.cloneWithRules([{ flag: "min", constraint: len }]); } max(len) { return this.cloneWithRules([{ flag: "max", constraint: len }]); } length(len) { return this.cloneWithRules([{ flag: "length", constraint: len }]); } valid(value) { const values = Array.isArray(value) ? value : [value]; return this.cloneWithRules([{ flag: "valid", constraint: values }]); } // Numbers only integer() { return this.cloneWithRules([{ flag: "integer" }]); } precision(limit) { return this.cloneWithRules([{ flag: "precision", constraint: limit }]); } positive() { return this.cloneWithRules([{ flag: "min", constraint: 0 }]); } negative() { return this.cloneWithRules([{ flag: "lessThan", constraint: 0 }]); } greaterThan(num) { return this.cloneWithRules([{ flag: "greaterThan", constraint: num }]); } lessThan(num) { return this.cloneWithRules([{ flag: "lessThan", constraint: num }]); } // String only uppercase() { return this.cloneWithRules([{ flag: "stringCasing", constraint: "uppercase" }]); } lowercase() { return this.cloneWithRules([{ flag: "stringCasing", constraint: "lowercase" }]); } regex(pattern, a, b) { var _a2, _b; const name = typeof a === "string" ? a : (_a2 = a == null ? void 0 : a.name) != null ? _a2 : b == null ? void 0 : b.name; const invert = typeof a === "string" ? false : (_b = a == null ? void 0 : a.invert) != null ? _b : b == null ? void 0 : b.invert; const constraint = { pattern, name, invert: invert || false }; return this.cloneWithRules([{ flag: "regex", constraint }]); } email() { return this.cloneWithRules([{ flag: "email" }]); } uri(opts) { const optsScheme = (opts == null ? void 0 : opts.scheme) || ["http", "https"]; const schemes = Array.isArray(optsScheme) ? optsScheme : [optsScheme]; if (!schemes.length) { throw new Error("scheme must have at least 1 scheme specified"); } const constraint = { options: { scheme: schemes.map(scheme => { if (!(scheme instanceof RegExp) && typeof scheme !== "string") { throw new Error("scheme must be a RegExp or a String"); } return typeof scheme === "string" ? new RegExp("^".concat(escapeRegex(scheme), "$")) : scheme; }), allowRelative: (opts == null ? void 0 : opts.allowRelative) || false, relativeOnly: (opts == null ? void 0 : opts.relativeOnly) || false, allowCredentials: (opts == null ? void 0 : opts.allowCredentials) || false } }; return this.cloneWithRules([{ flag: "uri", constraint }]); } // Array only unique() { return this.cloneWithRules([{ flag: "unique" }]); } // Objects only reference() { return this.cloneWithRules([{ flag: "reference" }]); } fields(rules) { if (this._type !== "Object") { throw new Error("fields() can only be called on an object type"); } const rule = this.cloneWithRules([]); rule._fieldRules = rules; return rule; } assetRequired() { const base = getBaseType(this._typeDef); let assetType; if (base && ["image", "file"].includes(base.name)) { assetType = base.name === "image" ? "Image" : "File"; } else { assetType = "Asset"; } return this.cloneWithRules([{ flag: "assetRequired", constraint: { assetType } }]); } async validate(value, context) { if (!context) { throw new Error("missing context"); } const valueIsEmpty = value === null || value === void 0; if (valueIsEmpty && this._required === "optional") { return EMPTY_ARRAY; } const rules = // Run only the _custom_ functions if the rule is not set to required or optional this._required === void 0 && valueIsEmpty ? this._rules.filter(curr => curr.flag === "custom") : this._rules; const validators = this._type && typeValidators[this._type] || genericValidators; const results = await Promise.all(rules.map(async curr => { if (curr.flag === void 0) { throw new Error('Invalid rule, did not contain "flag"-property'); } const validator = validators[curr.flag]; if (!validator) { const forType = this._type ? 'type "'.concat(this._type, '"') : "rule without declared type"; throw new Error('Validator for flag "'.concat(curr.flag, '" not found for ').concat(forType)); } let specConstraint = "constraint" in curr ? curr.constraint : null; if (isFieldRef(specConstraint)) { specConstraint = get(context.parent, specConstraint.path); } let result; try { result = await validator(specConstraint, value, this._message, context); } catch (err) { const errorFromException = new ValidationError("".concat(pathToString(context.path), ": Exception occurred while validating value: ").concat(err.message)); return convertToValidationMarker(errorFromException, "error", context); } return convertToValidationMarker(result, this._level, context); })); return results.flat(); } }, __publicField(_a, "FIELD_REF", FIELD_REF), __publicField(_a, "array", def => new _a(def).type("Array")), __publicField(_a, "object", def => new _a(def).type("Object")), __publicField(_a, "string", def => new _a(def).type("String")), __publicField(_a, "number", def => new _a(def).type("Number")), __publicField(_a, "boolean", def => new _a(def).type("Boolean")), __publicField(_a, "dateTime", def => new _a(def).type("Date")), __publicField(_a, "valueOfField", path => ({ type: FIELD_REF, path })), _a); const requestIdleCallbackShim = function requestIdleCallbackShim2(callback, options) { const start = Date.now(); return window.setTimeout(() => { callback({ didTimeout: false, timeRemaining() { return Math.max(0, Date.now() - start); } }); }, 0); }; const cancelIdleCallbackShim = function cancelIdleCallbackShim2(handle) { return window.clearTimeout(handle); }; const win = typeof window === "undefined" ? void 0 : window; const requestIdleCallback = (win == null ? void 0 : win.requestIdleCallback) || requestIdleCallbackShim; const cancelIdleCallback = (win == null ? void 0 : win.cancelIdleCallback) || cancelIdleCallbackShim; const memoizedWarnOnArraySlug = memoize(warnOnArraySlug); function getDocumentIds(id) { const isDraft = id.indexOf("drafts.") === 0; return { published: isDraft ? id.slice("drafts.".length) : id, draft: isDraft ? id : "drafts.".concat(id) }; } function serializePath(path) { return path.reduce((target, part, i) => { const isIndex = typeof part === "number"; const isKey = isKeyedObject(part); const separator = i === 0 ? "" : "."; const add = isIndex || isKey ? "[]" : "".concat(separator).concat(part); return "".concat(target).concat(add); }, ""); } const defaultIsUnique = (slug, context) => { const { getClient, document, path, type } = context; const schemaOptions = type == null ? void 0 : type.options; if (!document) { throw new Error("`document` was not provided in validation context."); } if (!path) { throw new Error("`path` was not provided in validation context."); } const disableArrayWarning = (schemaOptions == null ? void 0 : schemaOptions.disableArrayWarning) || false; const { published, draft } = getDocumentIds(document._id); const docType = document._type; const atPath = serializePath(path.concat("current")); if (!disableArrayWarning && atPath.includes("[]")) { memoizedWarnOnArraySlug(serializePath(path)); } const constraints = ["_type == $docType", "!(_id in [$draft, $published])", "".concat(atPath, " == $slug")].join(" && "); return getClient({ apiVersion: "2022-09-09" }).fetch("!defined(*[".concat(constraints, "][0]._id)"), { docType, draft, published, slug }, { tag: "validation.slug-is-unique" }); }; function warnOnArraySlug(serializedPath) { console.warn(["Slug field at path ".concat(serializedPath, " is within an array and cannot be automatically checked for uniqueness"), 'If you need to check for uniqueness, provide your own "isUnique" method', "To disable this message, set `disableArrayWarning: true` on the slug `options` field"].join("\n")); } const slugValidator = async (value, context) => { var _a; if (!value) { return true; } if (typeof value !== "object") { return "Slug must be an object"; } const slugValue = value.current; if (!slugValue) { return "Slug must have a value"; } const options = (_a = context == null ? void 0 : context.type) == null ? void 0 : _a.options; const isUnique = (options == null ? void 0 : options.isUnique) || defaultIsUnique; const slugContext = { ...context, parent: context.parent, type: context.type, defaultIsUnique }; const wasUnique = await isUnique(slugValue, slugContext); if (wasUnique) { return true; } return "Slug is already in use"; }; const ruleConstraintTypes = { array: true, boolean: true, date: true, number: true, object: true, string: true }; const isRuleConstraint = typeString => typeString in ruleConstraintTypes; function getTypeChain(type, visited) { if (!type) return []; if (visited.has(type)) return []; visited.add(type); const next = type.type ? getTypeChain(type.type, visited) : []; return [...next, type]; } function baseRuleReducer(inputRule, type) { let baseRule = inputRule; if (isRuleConstraint(type.jsonType)) { baseRule = baseRule.type(type.jsonType); } const typeOptionsList = // if type.options is truthy (type == null ? void 0 : type.options) && // and type.options is an object (non-null from the previous) typeof type.options === "object" && // and if `list` is in options "list" in type.options && // then finally access the list type.options.list; if (Array.isArray(typeOptionsList)) { baseRule = baseRule.valid(typeOptionsList.map(option => extractValueFromListOption(option, type))); } if (type.name === "datetime") return baseRule.type("Date"); if (type.name === "date") return baseRule.type("Date"); if (type.name === "url") return baseRule.uri(); if (type.name === "slug") return baseRule.custom(slugValidator); if (type.name === "reference") return baseRule.reference(); if (type.name === "email") return baseRule.email(); return baseRule; } function hasValueField(typeDef) { if (!typeDef) return false; if (!("fields" in typeDef) && typeDef.type) return hasValueField(typeDef.type); if (!("fields" in typeDef)) return false; if (!Array.isArray(typeDef.fields)) return false; return typeDef.fields.some(field => field.name === "value"); } function extractValueFromListOption(option, typeDef) { if (typeDef.jsonType === "object" && hasValueField(typeDef)) return option; return option.value === void 0 ? option : option.value; } function normalizeValidationRules(typeDef) { if (!typeDef) { return []; } const validation = typeDef.validation; if (Array.isArray(validation)) { return validation.flatMap(i => normalizeValidationRules({ ...typeDef, validation: i })); } if (validation instanceof Rule) { return [validation]; } const baseRule = // using an object + Object.values to de-dupe the type chain by type name Object.values(getTypeChain(typeDef, /* @__PURE__ */new Set()).reduce((acc, type) => { acc[type.name] = type; return acc; }, {})).reduce(baseRuleReducer, new Rule(typeDef)); if (!validation) { return [baseRule]; } return normalizeValidationRules({ ...typeDef, validation: validation(baseRule) }); } const isRecord = maybeRecord => typeof maybeRecord === "object" && maybeRecord !== null && !Array.isArray(maybeRecord); const isNonNullable = value => value !== null && value !== void 0; function resolveTypeForArrayItem(item, candidates) { if (candidates.length === 1) return candidates[0]; const itemType = isTypedObject(item) && item._type; const primitive = item === void 0 || item === null || !itemType && typeString(item).toLowerCase(); if (primitive && primitive !== "object") { return candidates.find(candidate => candidate.jsonType === primitive); } return candidates.find(candidate => { var _a; return ((_a = candidate.type) == null ? void 0 : _a.name) === itemType; }) || candidates.find(candidate => candidate.name === itemType) || candidates.find(candidate => candidate.name === "object" && primitive === "object"); } const EMPTY_MARKERS = []; async function validateDocument(getClient, doc, schema, context) { return lastValueFrom(validateDocumentObservable(getClient, doc, schema, context)); } function validateDocumentObservable(getClient, doc, schema, context) { const documentType = schema.get(doc._type); if (!documentType) { console.warn('Schema type for object type "%s" not found, skipping validation', doc._type); return of(EMPTY_MARKERS); } const validationOptions = { getClient, schema, parent: void 0, value: doc, path: [], document: doc, type: documentType, getDocumentExists: context == null ? void 0 : context.getDocumentExists }; return validateItemObservable(validationOptions).pipe(catchError(err => { console.error(err); return of([{ type: "validation", level: "error", path: [], item: new ValidationError(err == null ? void 0 : err.message) }]); })); } function validateItemObservable(_ref) { let { value, type, path = [], parent, ...restOfContext } = _ref; const rules = normalizeValidationRules(type); const selfChecks = rules.map(rule => defer(() => rule.validate(value, { ...restOfContext, parent, path, type }))); let nestedChecks = []; const selfIsRequired = rules.some(rule => rule.isRequired()); const shouldRunNestedObjectValidation = // run nested validation for objects (type == null ? void 0 : type.jsonType) === "object" && ( // if the value is truthy !!value || // or // (the value is null or undefined) and the top-level value is required (value === null || value === void 0) && selfIsRequired); if (shouldRunNestedObjectValidation) { const fieldTypes = type.fields.reduce((acc, field) => { acc[field.name] = field.type; return acc; }, {}); nestedChecks = nestedChecks.concat(rules.map(rule => rule._fieldRules).filter(isNonNullable).flatMap(fieldResults => Object.entries(fieldResults)).flatMap(_ref2 => { let [name, validation] = _ref2; const fieldType = fieldTypes[name]; return normalizeValidationRules({ ...fieldType, validation }).map(subRule => { const nestedValue = isRecord(value) ? value[name] : void 0; return defer(() => subRule.validate(nestedValue, { ...restOfContext, parent: value, path: path.concat(name), type: fieldType })); }); })); nestedChecks = nestedChecks.concat(type.fields.map(field => validateItemObservable({ ...restOfContext, parent: value, value: isRecord(value) ? value[field.name] : void 0, path: path.concat(field.name), type: field.type }))); } const shouldRunNestedValidationForArrays = (type == null ? void 0 : type.jsonType) === "array" && Array.isArray(value); if (shouldRunNestedValidationForArrays) { nestedChecks = nestedChecks.concat(value.map((item, index) => validateItemObservable({ ...restOfContext, parent: value, value: item, path: path.concat(isKeyedObject(item) ? { _key: item._key } : index), type: resolveTypeForArrayItem(item, type.of) }))); } return defer(() => merge([...selfChecks, ...nestedChecks])).pipe(mergeMap(validateNode => concat(idle(), validateNode), 40), mergeAll(), toArray(), map(flatten), map(results => { if (rules.some(rule => rule._fieldRules)) { return uniqBy(results, rule => JSON.stringify(rule)); } return results; })); } function idle(timeout) { return new Observable(observer => { const handle = requestIdleCallback(() => { observer.complete(); }, timeout ? { timeout } : void 0); return () => cancelIdleCallback(handle); }); } function traverse(typeDef, visited) { if (visited.has(typeDef)) { return; } visited.add(typeDef); typeDef.validation = normalizeValidationRules(typeDef); if ("fields" in typeDef) { for (const field of typeDef.fields) { traverse(field.type, visited); } } if ("of" in typeDef) { for (const candidate of typeDef.of) { traverse(candidate, visited); } } if (typeDef.annotations) { for (const annotation of typeDef.annotations) { traverse(annotation, visited); } } } function inferFromSchemaType(typeDef) { traverse(typeDef, /* @__PURE__ */new Set()); return typeDef; } function inferFromSchema(schema) { const typeNames = schema.getTypeNames(); typeNames.forEach(typeName => { const schemaType = schema.get(typeName); if (schemaType) { inferFromSchemaType(schemaType); } }); return schema; } export { Rule, inferFromSchema, inferFromSchemaType, validateDocument, validateDocumentObservable }; //# sourceMappingURL=index.esm.js.map