@sanity/validation
Version:
Validation and warning infrastructure for Sanity projects
1,287 lines (1,286 loc) • 42 kB
JavaScript
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