@sanity/schema
Version:
- **`Schema`** A collection of types - **`Type`** A specification of a data structure. Available through schema lookup. - **`Member type`** A member type is a type contained by a schema type. For example, an array may specify the allowed item types by def
1,430 lines • 97.3 kB
JavaScript
import { SetBuilder, processSetSynchronization } from "@sanity/descriptors";
import isEqual from "lodash/isEqual.js";
import isObject from "lodash/isObject.js";
import { OWN_PROPS_NAME, Rule, Schema } from "./_chunks-es/Rule.mjs";
import { ALL_FIELDS_GROUP_NAME, DEFAULT_MAX_FIELD_DEPTH, resolveSearchConfig, resolveSearchConfigForBaseFieldPaths } from "./_chunks-es/Rule.mjs";
import difference from "lodash/difference.js";
import cloneDeep from "lodash/cloneDeep.js";
import flatten from "lodash/flatten.js";
import get from "lodash/get.js";
import uniq from "lodash/uniq.js";
import humanizeList from "humanize-list";
import partition from "lodash/partition.js";
import isPlainObject from "lodash/isPlainObject.js";
import omit from "lodash/omit.js";
import leven from "leven";
import inspect from "object-inspect";
import { createReferenceTypeNode } from "groq-js";
const MAX_DEPTH_UKNOWN = 5;
class DescriptorConverter {
opts;
cache = /* @__PURE__ */ new WeakMap();
constructor(opts) {
this.opts = opts;
}
/**
* Returns a synchronization object for a schema.
*
* This is automatically cached in a weak map.
*/
get(schema) {
let value = this.cache.get(schema);
if (value) return value;
const builder = new SetBuilder();
for (const name of schema.getLocalTypeNames()) {
const typeDef = convertTypeDef(schema.get(name));
builder.addObject("sanity.schema.namedType", { name, typeDef });
}
return schema.parent && builder.addSet(this.get(schema.parent)), value = builder.build("sanity.schema.registry"), this.cache.set(schema, value), value;
}
}
function convertCommonTypeDef(schemaType, opts) {
const ownProps = OWN_PROPS_NAME in schemaType ? schemaType[OWN_PROPS_NAME] : schemaType;
let fields;
Array.isArray(ownProps.fields) && (fields = ownProps.fields.map(
({ name, group, fieldset, type }) => ({
name,
typeDef: convertTypeDef(type),
groups: arrayifyString(group),
fieldset
})
));
let fieldsets;
Array.isArray(ownProps.fieldsets) && (fieldsets = filterStringKey(
"name",
ownProps.fieldsets.map(
({ name, title, description, group, hidden, readOnly, options }) => ({
name,
title: maybeString(title),
description: maybeString(description),
group: maybeString(group),
hidden: conditionalTrue(hidden),
readOnly: conditionalTrue(readOnly),
options: convertUnknown(options)
})
)
));
let groups;
Array.isArray(ownProps.groups) && (groups = filterStringKey(
"name",
ownProps.groups.map(
({ name, title, hidden, default: def, i18n }) => ({
name,
title: maybeString(title),
hidden: conditionalTrue(hidden),
default: maybeTrue(def),
i18n: maybeI18n(i18n)
})
)
));
const reason = ownProps.deprecated?.reason;
let orderings;
return Array.isArray(ownProps.orderings) && (orderings = ownProps.orderings.map(maybeOrdering).filter((o) => o !== void 0)), {
title: maybeString(ownProps.title),
description: maybeStringOrJSX(ownProps.description),
readOnly: conditionalTrue(ownProps.readOnly),
hidden: conditionalTrue(ownProps.hidden),
liveEdit: maybeTrue(ownProps.liveEdit),
options: convertUnknown(ownProps.options),
initialValue: convertUnknown(ownProps.initialValue),
deprecated: typeof reason == "string" ? { reason } : void 0,
placeholder: maybeString(ownProps.placeholder),
rows: maybeNumberAsString(ownProps.rows),
fields,
fieldsets,
groups,
validation: maybeValidations(ownProps),
orderings
};
}
function convertTypeDef(schemaType, opts) {
const common2 = convertCommonTypeDef(schemaType);
if (!schemaType.type)
return {
extends: null,
jsonType: schemaType.jsonType,
...common2
};
switch (schemaType.type.name) {
case "array":
return {
extends: "array",
of: schemaType.of.map((ofType) => ({
name: ofType.name,
typeDef: convertTypeDef(ofType)
})),
...common2
};
case "reference":
case "globalDocumentReference":
case "crossDatasetReference":
return {
extends: schemaType.type.name,
to: filterStringKey(
"name",
schemaType.to.map((toType) => ({ name: toType.name || toType.type?.name || toType.type }))
),
...common2
};
default:
return { extends: schemaType.type.name, ...common2 };
}
}
function maybeString(val) {
return typeof val == "string" ? val : void 0;
}
function maybeNumberAsString(val) {
return typeof val == "number" ? val.toString() : void 0;
}
function maybeTrue(val) {
return val === !0 ? !0 : void 0;
}
function conditionalTrue(val) {
return typeof val == "function" ? FUNCTION_MARKER : maybeTrue(val);
}
function filterStringKey(key, arr) {
return arr.filter((obj) => typeof obj[key] == "string");
}
function arrayifyString(val) {
if (typeof val == "string")
return [val];
if (Array.isArray(val))
return val.filter((elem) => typeof elem == "string");
}
const FUNCTION_MARKER = { __type: "function" }, UNKNOWN_MARKER = { __type: "unknown" }, UNDEFINED_MARKER = { __type: "undefined" }, CYCLIC_MARKER = { __type: "cyclic" }, MAX_DEPTH_MARKER = { __type: "maxDepth" };
function convertUnknown(val, seen = /* @__PURE__ */ new Set(), maxDepth = MAX_DEPTH_UKNOWN) {
if (maxDepth === 0) return MAX_DEPTH_MARKER;
if (typeof val == "string" || typeof val == "boolean" || val === null || val === void 0)
return val;
if (typeof val == "number")
return { __type: "number", value: val.toString() };
if (typeof val == "function") return FUNCTION_MARKER;
if (seen.has(val))
return CYCLIC_MARKER;
if (seen.add(val), isObject(val)) {
if (Array.isArray(val))
return val.map((elem) => {
const res = convertUnknown(elem, seen, maxDepth - 1);
return res === void 0 ? UNDEFINED_MARKER : res;
});
if ("$$typeof" in val && "type" in val && "props" in val) {
const { type, props } = val, strType = typeof type == "function" ? type.name : type;
return typeof strType != "string" ? void 0 : {
__type: "jsx",
type: strType,
props: convertUnknown(props, seen, maxDepth - 1)
};
}
let hasType = !1;
const result = {};
for (const [key, field] of Object.entries(val))
key === "__type" && (hasType = !0), result[key] = convertUnknown(field, seen, maxDepth - 1);
return hasType ? { __type: "object", value: result } : result;
}
return UNKNOWN_MARKER;
}
function maybeStringOrJSX(val) {
if (typeof val == "string") return val;
if (isObject(val) && "$$typeof" in val && "type" in val && "props" in val) {
const { type, props } = val, strType = typeof type == "function" ? type.name : type;
return typeof strType != "string" ? void 0 : { __type: "jsx", type: strType, props: convertUnknown(props) };
}
}
function maybeValidations(obj) {
if (!isObject(obj) || !("type" in obj)) return;
const impliedRules = [];
switch ("options" in obj && isObject(obj.options) && "list" in obj.options && Array.isArray(obj.options.list) && impliedRules.push({
type: "enum",
values: obj.options.list.map((o) => convertUnknown(extractValueFromListOption(o, obj))).filter((v) => v !== void 0)
}), obj.type) {
case "url":
impliedRules.push({
type: "uri",
allowRelative: !1
});
break;
case "slug":
impliedRules.push({
type: "custom"
});
break;
case "reference":
impliedRules.push({
type: "reference"
});
break;
case "email":
impliedRules.push({
type: "email"
});
break;
}
if (!("validation" in obj) || !obj.validation)
return impliedRules.length > 0 ? [
{
level: "error",
rules: impliedRules
}
] : void 0;
const validations = [], rules = Array.isArray(obj.validation) ? obj.validation : [obj.validation];
for (const rule of rules) {
const validation = maybeValidation(rule);
if (validation === void 0)
continue;
const rulesToAdd = impliedRules.filter((ir) => !validation.rules.some((r) => isEqual(r, ir)));
rulesToAdd.length > 0 && validation.rules.unshift(...rulesToAdd), !validations.some((v) => isEqual(v, validation)) && validations.push(validation);
}
return validations.length > 0 ? validations : void 0;
}
function hasValueField(typeDef) {
return !typeDef || typeof typeDef != "object" ? !1 : "fields" in typeDef ? Array.isArray(typeDef.fields) ? typeDef.fields.some((field) => field.name === "value") : !1 : "type" in typeDef && typeDef.type ? hasValueField(typeDef.type) : !1;
}
function extractValueFromListOption(option, typeDef) {
return typeDef.jsonType === "object" && hasValueField(typeDef) ? option : isObject(option) && "value" in option && option.value ? option.value : option;
}
function maybeValidation(val) {
if (val) {
if (isIRuleFunction(val))
try {
const result = val(new Rule());
if (isIRule(result))
return maybeValidation(result);
throw new Error("failed to convert to plain rule");
} catch {
return {
level: "error",
rules: [{ type: "custom", name: "function" }]
};
}
if (isIRule(val)) {
const level = val._level || "error", message = maybeValidationMessage(val._message), rules = [];
for (const spec of val._rules || []) {
const optional = val._required === "optional" || void 0, convertedRule = convertRuleSpec(spec, optional);
convertedRule !== void 0 && (rules.some((r) => isEqual(r, convertedRule)) || rules.push(convertedRule));
}
return rules.length === 0 ? void 0 : {
level,
rules,
...message && { message }
};
}
}
}
function isIRule(val) {
return isObject(val) && "_rules" in val;
}
function maybeValidationMessage(val) {
if (typeof val == "string") return val;
if (!isObject(val) || Array.isArray(val)) return;
const objectMessage = {};
for (const [field, value] of Object.entries(val))
typeof field != "string" || typeof value != "string" || (objectMessage[field] = value);
return Object.keys(objectMessage).length > 0 ? objectMessage : void 0;
}
function isIRuleFunction(val) {
return typeof val == "function";
}
function convertRuleSpec(spec, optional) {
if (!isObject(spec) || !("flag" in spec))
return;
const constraint = "constraint" in spec ? spec.constraint : void 0;
switch (spec.flag) {
case "integer":
return { type: "integer" };
case "email":
return { type: "email" };
case "unique":
return { type: "uniqueItems" };
case "reference":
return { type: "reference" };
case "assetRequired":
return { type: "assetRequired" };
case "stringCasing":
return constraint === "uppercase" ? { type: "uppercase" } : constraint === "lowercase" ? { type: "lowercase" } : void 0;
case "all":
if (Array.isArray(constraint)) {
const children = constraint.map((childRule) => maybeValidation(childRule)).filter((c) => c !== void 0);
if (children.length > 0)
return { type: "allOf", children };
}
return;
case "either":
if (Array.isArray(constraint)) {
const children = constraint.map((childRule) => maybeValidation(childRule)).filter((c) => c !== void 0);
if (children.length > 0)
return { type: "anyOf", children };
}
return;
case "valid":
return Array.isArray(constraint) ? {
type: "enum",
values: constraint.map((c) => convertUnknown(c)).filter((v) => v !== void 0)
} : void 0;
case "min":
return { type: "minimum", value: convertConstraintValue(constraint) };
case "max":
return { type: "maximum", value: convertConstraintValue(constraint) };
case "length":
return { type: "length", value: convertConstraintValue(constraint) };
case "precision":
return { type: "precision", value: convertConstraintValue(constraint) };
case "lessThan":
return { type: "exclusiveMaximum", value: convertConstraintValue(constraint) };
case "greaterThan":
return { type: "exclusiveMinimum", value: convertConstraintValue(constraint) };
case "regex":
if (isObject(constraint) && "pattern" in constraint) {
const { pattern } = constraint, invert = "invert" in constraint ? maybeBoolean(constraint.invert) : void 0;
if (pattern instanceof RegExp)
return {
type: "regex",
pattern: pattern.source,
...invert && { invert: !0 }
};
}
return;
case "uri": {
const allowRelative = isObject(constraint) && "options" in constraint && isObject(constraint.options) && "allowRelative" in constraint.options ? maybeBoolean(constraint.options.allowRelative) : void 0;
return {
type: "uri",
...allowRelative !== void 0 && { allowRelative }
};
}
case "custom":
return { type: "custom", ...optional && { optional } };
case "media":
return { type: "custom", name: "media" };
case "type":
return;
case "presence":
return constraint === "required" ? { type: "required" } : void 0;
default:
return;
}
}
function convertConstraintValue(constraint) {
return isObject(constraint) && "type" in constraint && "path" in constraint && constraint.type && constraint.path ? {
type: "fieldReference",
path: Array.isArray(constraint.path) ? constraint.path : [constraint.path]
} : String(constraint);
}
function maybeBoolean(val) {
if (typeof val == "boolean")
return val;
}
function maybeI18n(val) {
if (!isObject(val) || Array.isArray(val)) return;
const localizedMessage = {};
for (const entry of Object.entries(val))
if (isI18nEntry(entry)) {
const [field, value] = entry;
localizedMessage[field] = {
ns: value.ns,
key: value.key
};
}
return Object.keys(localizedMessage).length > 0 ? localizedMessage : void 0;
}
function isI18nEntry(entry) {
const [key, value] = entry;
return typeof key == "string" && !!value && typeof value == "object" && "key" in value && "ns" in value && typeof value.key == "string" && typeof value.ns == "string";
}
function maybeOrdering(val) {
if (!isObject(val) || Array.isArray(val)) return;
const name = "name" in val && typeof val.name == "string" ? val.name : void 0;
if (name === void 0) return;
const title = "title" in val && typeof val.title == "string" ? val.title : name, by = "by" in val && Array.isArray(val.by) ? val.by : [], orderingBy = [];
for (const item of by) {
const orderingItem = maybeOrderingBy(item);
orderingItem && orderingBy.push(orderingItem);
}
if (orderingBy.length === 0) return;
const i18n = "i18n" in val ? maybeI18n(val.i18n) : void 0;
return {
name,
title,
by: orderingBy,
...i18n && { i18n }
};
}
function maybeOrderingBy(val) {
if (!isObject(val) || Array.isArray(val)) return;
const field = "field" in val && typeof val.field == "string" ? val.field : void 0, direction = "direction" in val && (val.direction === "asc" || val.direction === "desc") ? val.direction : void 0;
if (!(!field || !direction))
return { field, direction };
}
function processSchemaSynchronization(sync, response) {
return processSetSynchronization(sync, response);
}
const ACTIONS_FLAG = "__experimental_actions", DEFAULT_ACTIONS = ["create", "update", "delete", "publish"], VALID_ACTIONS = DEFAULT_ACTIONS, readActions = (schemaType) => ACTIONS_FLAG in schemaType ? schemaType[ACTIONS_FLAG] : DEFAULT_ACTIONS, validateActions = (typeName, actions) => {
if (!Array.isArray(actions))
throw new Error(
`The value of <type>.${ACTIONS_FLAG} should be an array with any of the actions ${VALID_ACTIONS.join(
", "
)}`
);
const invalid = difference(actions, VALID_ACTIONS);
if (invalid.length > 0)
throw new Error(
`Invalid action${invalid.length > 1 ? "s" : ""} configured for schema type "${typeName}": ${invalid.join(
", "
)}. Valid actions are: ${VALID_ACTIONS.join(", ")}`
);
return actions;
}, resolveEnabledActions = (schemaType) => validateActions(schemaType.name, readActions(schemaType)), isActionEnabled = (schemaType, action) => resolveEnabledActions(schemaType).includes(action);
var assetSourceData = {
name: "sanity.assetSourceData",
title: "Asset Source Data",
type: "object",
fields: [
{
name: "name",
title: "Source name",
description: "A canonical name for the source this asset is originating from",
type: "string"
},
{
name: "id",
title: "Asset Source ID",
description: "The unique ID for the asset within the originating source so you can programatically find back to it",
type: "string"
},
{
name: "url",
title: "Asset information URL",
description: "A URL to find more information about this asset in the originating source",
type: "string"
}
]
}, fileAsset = {
name: "sanity.fileAsset",
title: "File",
type: "document",
fieldsets: [
{
name: "system",
title: "System fields",
description: "These fields are managed by the system and not editable"
}
],
fields: [
{
name: "originalFilename",
type: "string",
title: "Original file name",
readOnly: !0
},
{
name: "label",
type: "string",
title: "Label"
},
{
name: "title",
type: "string",
title: "Title"
},
{
name: "description",
type: "string",
title: "Description"
},
{
name: "altText",
type: "string",
title: "Alternative text"
},
{
name: "sha1hash",
type: "string",
title: "SHA1 hash",
readOnly: !0,
fieldset: "system"
},
{
name: "extension",
type: "string",
title: "File extension",
readOnly: !0,
fieldset: "system"
},
{
name: "mimeType",
type: "string",
title: "Mime type",
readOnly: !0,
fieldset: "system"
},
{
name: "size",
type: "number",
title: "File size in bytes",
readOnly: !0,
fieldset: "system"
},
{
name: "assetId",
type: "string",
title: "Asset ID",
readOnly: !0,
fieldset: "system"
},
{
name: "uploadId",
type: "string",
readOnly: !0,
hidden: !0,
fieldset: "system"
},
{
name: "path",
type: "string",
title: "Path",
readOnly: !0,
fieldset: "system"
},
{
name: "url",
type: "string",
title: "Url",
readOnly: !0,
fieldset: "system"
},
{
name: "source",
type: "sanity.assetSourceData",
title: "Source",
readOnly: !0,
fieldset: "system"
}
],
preview: {
select: {
title: "originalFilename",
path: "path",
mimeType: "mimeType",
size: "size"
},
prepare(doc) {
return {
title: doc.title || doc.path.split("/").slice(-1)[0],
subtitle: `${doc.mimeType} (${(doc.size / 1024 / 1024).toFixed(2)} MB)`
};
}
},
orderings: [
{
title: "File size",
name: "fileSizeDesc",
by: [{ field: "size", direction: "desc" }]
}
]
}, geopoint = {
title: "Geographical Point",
name: "geopoint",
type: "object",
fields: [
{
name: "lat",
type: "number",
title: "Latitude"
},
{
name: "lng",
type: "number",
title: "Longitude"
},
{
name: "alt",
type: "number",
title: "Altitude"
}
]
}, imageAsset = {
name: "sanity.imageAsset",
title: "Image",
type: "document",
fieldsets: [
{
name: "system",
title: "System fields",
description: "These fields are managed by the system and not editable"
}
],
fields: [
{
name: "originalFilename",
type: "string",
title: "Original file name",
readOnly: !0
},
{
name: "label",
type: "string",
title: "Label"
},
{
name: "title",
type: "string",
title: "Title"
},
{
name: "description",
type: "string",
title: "Description"
},
{
name: "altText",
type: "string",
title: "Alternative text"
},
{
name: "sha1hash",
type: "string",
title: "SHA1 hash",
readOnly: !0,
fieldset: "system"
},
{
name: "extension",
type: "string",
readOnly: !0,
title: "File extension",
fieldset: "system"
},
{
name: "mimeType",
type: "string",
readOnly: !0,
title: "Mime type",
fieldset: "system"
},
{
name: "size",
type: "number",
title: "File size in bytes",
readOnly: !0,
fieldset: "system"
},
{
name: "assetId",
type: "string",
title: "Asset ID",
readOnly: !0,
fieldset: "system"
},
{
name: "uploadId",
type: "string",
readOnly: !0,
hidden: !0,
fieldset: "system"
},
{
name: "path",
type: "string",
title: "Path",
readOnly: !0,
fieldset: "system"
},
{
name: "url",
type: "string",
title: "Url",
readOnly: !0,
fieldset: "system"
},
{
name: "metadata",
type: "sanity.imageMetadata",
title: "Metadata"
},
{
name: "source",
type: "sanity.assetSourceData",
title: "Source",
readOnly: !0,
fieldset: "system"
}
],
preview: {
select: {
id: "_id",
title: "originalFilename",
mimeType: "mimeType",
size: "size"
},
prepare(doc) {
return {
title: doc.title || typeof doc.path == "string" && doc.path.split("/").slice(-1)[0],
media: { asset: { _ref: doc.id } },
subtitle: `${doc.mimeType} (${(Number(doc.size) / 1024 / 1024).toFixed(2)} MB)`
};
}
},
orderings: [
{
title: "File size",
name: "fileSizeDesc",
by: [{ field: "size", direction: "desc" }]
}
]
}, imageCrop = {
name: "sanity.imageCrop",
title: "Image crop",
type: "object",
fields: [
{
name: "top",
type: "number",
validation: (Rule2) => Rule2.required()
},
{
name: "bottom",
type: "number",
validation: (Rule2) => Rule2.required()
},
{
name: "left",
type: "number",
validation: (Rule2) => Rule2.required()
},
{
name: "right",
type: "number",
validation: (Rule2) => Rule2.required()
}
]
}, imageDimensions = {
name: "sanity.imageDimensions",
type: "object",
title: "Image dimensions",
fields: [
{
name: "height",
type: "number",
title: "Height",
readOnly: !0,
validation: (Rule2) => Rule2.required()
},
{
name: "width",
type: "number",
title: "Width",
readOnly: !0,
validation: (Rule2) => Rule2.required()
},
{
name: "aspectRatio",
type: "number",
title: "Aspect ratio",
readOnly: !0,
validation: (Rule2) => Rule2.required()
}
]
}, imageHotspot = {
name: "sanity.imageHotspot",
title: "Image hotspot",
type: "object",
fields: [
{
name: "x",
type: "number",
validation: (Rule2) => Rule2.required()
},
{
name: "y",
type: "number",
validation: (Rule2) => Rule2.required()
},
{
name: "height",
type: "number",
validation: (Rule2) => Rule2.required()
},
{
name: "width",
type: "number",
validation: (Rule2) => Rule2.required()
}
]
}, imageMetadata = {
name: "sanity.imageMetadata",
title: "Image metadata",
type: "object",
fieldsets: [
{
name: "extra",
title: "Extra metadata\u2026",
options: {
collapsable: !0
}
}
],
fields: [
{
name: "location",
type: "geopoint"
},
{
name: "dimensions",
title: "Dimensions",
type: "sanity.imageDimensions",
fieldset: "extra"
},
{
name: "palette",
type: "sanity.imagePalette",
title: "Palette",
fieldset: "extra"
},
{
name: "lqip",
title: "LQIP (Low-Quality Image Placeholder)",
type: "string",
readOnly: !0
},
{
name: "blurHash",
title: "BlurHash",
type: "string",
readOnly: !0
},
{
name: "hasAlpha",
title: "Has alpha channel",
type: "boolean",
readOnly: !0
},
{
name: "isOpaque",
title: "Is opaque",
type: "boolean",
readOnly: !0
}
]
}, imagePalette = {
name: "sanity.imagePalette",
title: "Image palette",
type: "object",
fields: [
{ name: "darkMuted", type: "sanity.imagePaletteSwatch", title: "Dark Muted" },
{ name: "lightVibrant", type: "sanity.imagePaletteSwatch", title: "Light Vibrant" },
{ name: "darkVibrant", type: "sanity.imagePaletteSwatch", title: "Dark Vibrant" },
{ name: "vibrant", type: "sanity.imagePaletteSwatch", title: "Vibrant" },
{ name: "dominant", type: "sanity.imagePaletteSwatch", title: "Dominant" },
{ name: "lightMuted", type: "sanity.imagePaletteSwatch", title: "Light Muted" },
{ name: "muted", type: "sanity.imagePaletteSwatch", title: "Muted" }
]
}, imagePaletteSwatch = {
name: "sanity.imagePaletteSwatch",
title: "Image palette swatch",
type: "object",
fields: [
{ name: "background", type: "string", title: "Background", readOnly: !0 },
{ name: "foreground", type: "string", title: "Foreground", readOnly: !0 },
{ name: "population", type: "number", title: "Population", readOnly: !0 },
{ name: "title", type: "string", title: "String", readOnly: !0 }
]
}, slug$1 = {
title: "Slug",
name: "slug",
type: "object",
fields: [
{
name: "current",
title: "Current slug",
type: "string",
validation: (Rule2) => Rule2.required()
},
{
// The source field is deprecated/unused, but leaving it included and hidden
// to prevent rendering "Unknown field" warnings on legacy data
name: "source",
title: "Source field",
type: "string",
hidden: !0
}
]
};
const builtinTypes = [
assetSourceData,
slug$1,
geopoint,
// legacyRichDate,
imageAsset,
fileAsset,
imageCrop,
imageHotspot,
imageMetadata,
imageDimensions,
imagePalette,
imagePaletteSwatch
], HELP_IDS = {
TYPE_INVALID: "schema-type-invalid",
TYPE_IS_ESM_MODULE: "schema-type-is-esm-module",
TYPE_NAME_RESERVED: "schema-type-name-reserved",
TYPE_MISSING_NAME: "schema-type-missing-name-or-type",
TYPE_MISSING_TYPE: "schema-type-missing-name-or-type",
TYPE_TITLE_RECOMMENDED: "schema-type-title-is-recommended",
TYPE_TITLE_INVALID: "schema-type-title-is-recommended",
OBJECT_FIELDS_INVALID: "schema-object-fields-invalid",
OBJECT_FIELD_NOT_UNIQUE: "schema-object-fields-invalid",
OBJECT_FIELD_NAME_INVALID: "schema-object-fields-invalid",
OBJECT_FIELD_DEFINITION_INVALID_TYPE: "schema-object-fields-invalid",
ARRAY_PREDEFINED_CHOICES_INVALID: "schema-predefined-choices-invalid",
ARRAY_OF_ARRAY: "schema-array-of-array",
ARRAY_OF_INVALID: "schema-array-of-invalid",
ARRAY_OF_NOT_UNIQUE: "schema-array-of-invalid",
ARRAY_OF_TYPE_GLOBAL_TYPE_CONFLICT: "schema-array-of-type-global-type-conflict",
ARRAY_OF_TYPE_BUILTIN_TYPE_CONFLICT: "schema-array-of-type-builtin-type-conflict",
REFERENCE_TO_INVALID: "schema-reference-to-invalid",
REFERENCE_TO_NOT_UNIQUE: "schema-reference-to-invalid",
REFERENCE_INVALID_OPTIONS: "schema-reference-invalid-options",
REFERENCE_INVALID_OPTIONS_LOCATION: "schema-reference-options-nesting",
REFERENCE_INVALID_FILTER_PARAMS_COMBINATION: "schema-reference-filter-params-combination",
SLUG_SLUGIFY_FN_RENAMED: "slug-slugifyfn-renamed",
ASSET_METADATA_FIELD_INVALID: "asset-metadata-field-invalid",
CROSS_DATASET_REFERENCE_INVALID: "cross-dataset-reference-invalid",
GLOBAL_DOCUMENT_REFERENCE_INVALID: "global-document-reference-invalid",
DEPRECATED_BLOCKEDITOR_KEY: "schema-deprecated-blockeditor-key",
STANDALONE_BLOCK_TYPE: "schema-standalone-block-type"
};
function createValidationResult(severity, message, helpId) {
if (helpId && !Object.keys(HELP_IDS).some((id) => HELP_IDS[id] === helpId))
throw new Error(
`Used the unknown helpId "${helpId}", please add it to the array in createValidationResult.js`
);
return {
severity,
message,
helpId
};
}
const error = (message, helpId) => createValidationResult("error", message, helpId), warning = (message, helpId) => createValidationResult("warning", message, helpId);
function groupProblems(types) {
return flatten(types.map((type) => getTypeProblems(type))).filter(
(type) => type.problems.length > 0
);
}
function createTypeWithMembersProblemsAccessor(memberPropertyName, getMembers = (type) => get(type, memberPropertyName)) {
return function(type, parentPath) {
const currentPath = [
...parentPath,
{ kind: "type", type: type.type, name: type.name }
], members = getMembers(type) || [], memberProblems = Array.isArray(members) ? members.map((memberType) => {
const propertySegment = {
kind: "property",
name: memberPropertyName
}, memberPath = [...currentPath, propertySegment];
return getTypeProblems(memberType, memberPath);
}) : [
[
{
path: currentPath,
problems: [error(`Member declaration (${memberPropertyName}) is not an array`)]
}
]
];
return [
{
path: currentPath,
problems: type._problems || []
},
...flatten(memberProblems)
];
};
}
const arrify = (val) => Array.isArray(val) ? val : typeof val > "u" && [] || [val], getObjectProblems = createTypeWithMembersProblemsAccessor("fields"), getImageProblems = createTypeWithMembersProblemsAccessor("fields"), getFileProblems = createTypeWithMembersProblemsAccessor("fields"), getArrayProblems = createTypeWithMembersProblemsAccessor("of"), getReferenceProblems = createTypeWithMembersProblemsAccessor(
"to",
(type) => "to" in type ? arrify(type.to) : []
), getBlockAnnotationProblems = createTypeWithMembersProblemsAccessor("marks.annotations"), getBlockMemberProblems = createTypeWithMembersProblemsAccessor("of"), getBlockProblems = (type, problems) => [
...getBlockAnnotationProblems(type, problems),
...getBlockMemberProblems(type, problems)
];
function getDefaultProblems(type, path = []) {
return [
{
path: [...path, { kind: "type", type: type.type, name: type.name }],
problems: type._problems || []
}
];
}
function getTypeProblems(type, path = []) {
switch (type.type) {
case "object":
return getObjectProblems(type, path);
case "document":
return getObjectProblems(type, path);
case "array":
return getArrayProblems(type, path);
case "reference":
return getReferenceProblems(type, path);
case "block":
return getBlockProblems(type, path);
case "image":
return getImageProblems(type, path);
case "file":
return getFileProblems(type, path);
default:
return getDefaultProblems(type, path);
}
}
function getDupes(array2, selector = (v) => v) {
const dupes = array2.reduce((acc, item) => {
const key = selector(item);
return acc[key] || (acc[key] = []), acc[key].push(item), acc;
}, {});
return Object.keys(dupes).map((key) => dupes[key].length > 1 ? dupes[key] : null).filter(Boolean);
}
const NOOP_VISITOR = (typeDef) => typeDef, TYPE_TYPE = { name: "type", type: null }, FUTURE_RESERVED = ["any", "time", "date"];
function traverseSchema(types = [], coreTypes2 = [], visitor = NOOP_VISITOR) {
const coreTypesRegistry = /* @__PURE__ */ Object.create(null), registry = /* @__PURE__ */ Object.create(null), coreTypeNames2 = coreTypes2.map((typeDef) => typeDef.name), reservedTypeNames = FUTURE_RESERVED.concat(coreTypeNames2), typeNames = types.map((typeDef) => typeDef && typeDef.name).filter(Boolean);
coreTypes2.forEach((coreType) => {
coreTypesRegistry[coreType.name] = coreType;
}), types.forEach((type, i) => {
registry[type && type.name || `__unnamed_${i}`] = {};
});
function getType(typeName) {
return typeName === "type" ? TYPE_TYPE : coreTypesRegistry[typeName] || registry[typeName] || null;
}
const duplicateNames = uniq(flatten(getDupes(typeNames)));
function isDuplicate(typeName) {
return duplicateNames.includes(typeName);
}
function getTypeNames() {
return typeNames.concat(coreTypeNames2);
}
function isReserved(typeName) {
return typeName === "type" || reservedTypeNames.includes(typeName);
}
const visitType = (isRoot) => (typeDef, index) => visitor(typeDef, {
visit: visitType(!1),
isRoot,
getType,
getTypeNames,
isReserved,
isDuplicate,
index
});
return coreTypes2.forEach((coreTypeDef) => {
Object.assign(coreTypesRegistry[coreTypeDef.name], visitType(coreTypeDef));
}), types.forEach((typeDef, i) => {
Object.assign(
registry[typeDef && typeDef.name || `__unnamed_${i}`],
visitType(!0)(typeDef, i)
);
}), {
get(typeName) {
const res = registry[typeName] || coreTypesRegistry[typeName];
if (res)
return res;
throw new Error(`No such type: ${typeName}`);
},
has(typeName) {
return typeName in registry || typeName in coreTypesRegistry;
},
getTypeNames() {
return Object.keys(registry);
},
getTypes() {
return this.getTypeNames().map(this.get);
},
toJSON() {
return this.getTypes();
}
};
}
const coreTypes = [
{ name: "array", jsonType: "array", type: "type" },
{ name: "block", jsonType: "object", type: "type" },
{ name: "boolean", jsonType: "boolean", type: "type" },
{ name: "datetime", jsonType: "string", type: "type" },
{ name: "date", jsonType: "string", type: "type" },
{ name: "document", jsonType: "object", type: "type" },
{ name: "email", jsonType: "string", type: "type" },
{ name: "file", jsonType: "object", type: "type" },
{ name: "geopoint", jsonType: "object", type: "type" },
{ name: "image", jsonType: "object", type: "type" },
{ name: "number", jsonType: "number", type: "type" },
{ name: "object", jsonType: "object", type: "type" },
{ name: "reference", jsonType: "object", type: "type" },
{ name: "crossDatasetReference", jsonType: "object", type: "type" },
{ name: "globalDocumentReference", jsonType: "object", type: "type" },
{ name: "slug", jsonType: "object", type: "type" },
{ name: "span", jsonType: "object", type: "type" },
{ name: "string", jsonType: "string", type: "type" },
{ name: "telephone", jsonType: "string", type: "type" },
{ name: "text", jsonType: "string", type: "type" },
{ name: "url", jsonType: "string", type: "type" }
], coreTypeNames = coreTypes.map((t) => t.name);
function traverseSanitySchema(schemaTypes, visitor) {
return traverseSchema(schemaTypes, coreTypes, visitor);
}
function isPrimitiveTypeName(typeName) {
return typeName === "string" || typeName === "number" || typeName === "boolean";
}
function isAssignable(typeName, type) {
return (typeof type.name == "string" ? type.name : type.type) === typeName;
}
function quote$2(n) {
return `"${n}"`;
}
function pluralize(arr, suf = "s") {
return arr.length === 1 ? "" : suf;
}
function format(value) {
return Array.isArray(value) ? `array with ${value.length} entries` : typeof value == "object" && value !== null ? `object with keys ${humanizeList(Object.keys(value).map(quote$2))}` : quote$2(value);
}
var array = (typeDef, visitorContext) => {
const ofIsArray = Array.isArray(typeDef.of);
if (ofIsArray) {
const invalid = typeDef.of.reduce((errs, def, idx) => {
if (typeof def.name == "string" && // specifying the same name as the type is redundant, but should not be a hard error at this point
// Consider showing a warning for this and deprecate this ability eventually
def.name !== def.type && coreTypeNames.includes(def.name))
return errs.concat(
error(
`Found array member declaration with the same type name as a built-in type ("${def.name}"). Array members can not be given the same name as a built-in type.`,
HELP_IDS.ARRAY_OF_TYPE_BUILTIN_TYPE_CONFLICT
)
);
if (def.type === "object" && def.name && visitorContext.getType(def.name))
return errs.concat(
warning(
`Found array member declaration with the same name as the global schema type "${def.name}". It's recommended to use a unique name to avoid possibly incompatible data types that shares the same name.`,
HELP_IDS.ARRAY_OF_TYPE_GLOBAL_TYPE_CONFLICT
)
);
if (def.type === "array")
return errs.concat(
error(
'Found array member declaration of type "array" - multidimensional arrays are not currently supported by Sanity',
HELP_IDS.ARRAY_OF_ARRAY
)
);
if (def)
return errs;
const err = `Found ${def === null ? "null" : typeof def}, expected member declaration`;
return errs.concat(
error(
`Found invalid type member declaration in array at index ${idx}: ${err}`,
HELP_IDS.ARRAY_OF_INVALID
)
);
}, []);
if (invalid.length > 0)
return {
...typeDef,
of: [],
_problems: invalid
};
}
const problems = flatten([
ofIsArray ? getDupes(typeDef.of, (t) => `${t.name};${t.type}`).map(
(dupes) => error(
`Found ${dupes.length} members with same type, but not unique names "${dupes[0].type}" in array. This makes it impossible to tell their values apart and you should consider naming them`,
HELP_IDS.ARRAY_OF_NOT_UNIQUE
)
) : error(
'The array type is missing or having an invalid value for the required "of" property',
HELP_IDS.ARRAY_OF_INVALID
)
]), of = ofIsArray ? typeDef.of : [], hasObjectTypesWithoutName = of.some(
(type) => type.type === "object" && typeof type.name > "u"
);
of.some((ofType) => ofType.type === "block") && hasObjectTypesWithoutName && problems.push(
error(
"The array type's 'of' property can't have an object type without a 'name' property as member, when the 'block' type is also a member of that array.",
HELP_IDS.ARRAY_OF_INVALID
)
);
const [primitiveTypes, objectTypes] = partition(
of,
(ofType) => isPrimitiveTypeName(ofType.type) || isPrimitiveTypeName(visitorContext.getType(ofType.type)?.jsonType)
), isMixedArray = primitiveTypes.length > 0 && objectTypes.length > 0;
if (isMixedArray) {
const primitiveTypeNames = primitiveTypes.map((t) => t.type), objectTypeNames = objectTypes.map((t) => t.type);
problems.push(
error(
`The array type's 'of' property can't have both object types and primitive types (found primitive type ${pluralize(
primitiveTypeNames
)} ${humanizeList(primitiveTypeNames.map(quote$2))} and object type${pluralize(
objectTypeNames
)} ${humanizeList(objectTypeNames.map(quote$2))})`,
HELP_IDS.ARRAY_OF_INVALID
)
);
}
const list = typeDef?.options?.list;
return !isMixedArray && Array.isArray(list) && (primitiveTypes.length > 0 ? list.forEach((option) => {
const value = option?.value ?? option;
if (!primitiveTypes.some((primitiveType) => typeof value === visitorContext.getType(primitiveType.type).jsonType)) {
const formattedTypeList = humanizeList(
primitiveTypes.map((t) => t.name || t.type),
{ conjunction: "or" }
);
problems.push(
error(
`An invalid entry found in options.list: ${format(
value
)}. Must be either a value of type ${formattedTypeList}, or an object with {title: string, value: ${formattedTypeList}}`,
HELP_IDS.ARRAY_PREDEFINED_CHOICES_INVALID
)
);
}
}) : list.forEach((option) => {
const optionTypeName = option._type || "object";
objectTypes.some(
(validObjectType) => isAssignable(optionTypeName, validObjectType)
) || problems.push(
error(
`An invalid entry found in options.list: ${format(
option
)}. Must be an object with "_type" set to ${humanizeList(
objectTypes.map((t) => t.name || t.type).map((t) => t === "object" ? "undefined" : quote$2(t)),
{ conjunction: "or" }
)}`,
HELP_IDS.ARRAY_PREDEFINED_CHOICES_INVALID
)
);
})), typeDef?.options?.list && typeDef?.options?.layout === "tags" && problems.push(
warning(
"Found array member declaration with both tags layout and a list of predefined values. If you want to display a list of predefined values, remove the tags layout from `options`."
)
), {
...typeDef,
of: of.map(visitorContext.visit),
_problems: problems
};
};
function isJSONTypeOf(type, jsonType, visitorContext) {
if ("jsonType" in type)
return type.jsonType === jsonType;
const parentType = visitorContext.getType(type.type);
if (!parentType)
throw new Error(`Could not resolve jsonType of ${type.name}. No parent type found`);
return isJSONTypeOf(parentType, jsonType, visitorContext);
}
const getTypeOf = (thing) => Array.isArray(thing) ? "array" : typeof thing, quote$1 = (str) => `"${str}"`, allowedKeys = [
"components",
"lists",
"marks",
"name",
"of",
"options",
"styles",
"title",
"type",
"validation"
], allowedMarkKeys = ["decorators", "annotations"], allowedStyleKeys = ["blockEditor", "title", "value", "icon", "component"], allowedDecoratorKeys = ["blockEditor", "title", "value", "icon", "component"], allowedListKeys = ["title", "value", "icon", "component"], supportedBuiltInObjectTypes = [
"file",
"image",
"object",
"reference",
"crossDatasetReference",
"globalDocumentReference"
];
function validateBlockType(typeDef, visitorContext) {
const problems = [];
let styles = typeDef.styles, lists = typeDef.lists, marks = typeDef.marks, members = typeDef.of;
const disallowedKeys = Object.keys(typeDef).filter(
(key) => !allowedKeys.includes(key) && !key.startsWith("_")
);
return disallowedKeys.length > 0 && problems.push(
error(
`Found unknown properties for block declaration: ${humanizeList(
disallowedKeys.map(quote$1)
)}`
)
), marks && (marks = validateMarks(typeDef.marks, visitorContext, problems)), styles && (styles = validateStyles(styles, visitorContext, problems)), lists && (lists = validateLists(lists, visitorContext, problems)), members && (members = validateMembers(members, visitorContext, problems)), {
...omit(typeDef, disallowedKeys),
marks,
styles,
name: typeDef.name || typeDef.type,
of: members,
_problems: problems
};
}
function validateMarks(marks, visitorContext, problems) {
let decorators = marks.decorators, annotations = marks.annotations;
if (!isPlainObject(marks))
return problems.push(error(`"marks" declaration should be an object, got ${getTypeOf(marks)}`)), problems;
const disallowedMarkKeys = Object.keys(marks).filter(
(key) => !allowedMarkKeys.includes(key) && !key.startsWith("_")
);
return disallowedMarkKeys.length > 0 && problems.push(
error(
`Found unknown properties for block declaration: ${humanizeList(
disallowedMarkKeys.map(quote$1)
)}`
)
), decorators && !Array.isArray(decorators) ? problems.push(
error(`"marks.decorators" declaration should be an array, got ${getTypeOf(decorators)}`)
) : decorators && (decorators.filter((dec) => !!dec.blockEditor).forEach((dec) => {
dec.icon = dec.blockEditor.icon, dec.component = dec.blockEditor.render;
}), decorators = validateDecorators(decorators, visitorContext, problems)), annotations && !Array.isArray(annotations) ? problems.push(
error(`"marks.annotations" declaration should be an array, got ${getTypeOf(annotations)}`)
) : annotations && (annotations = validateAnnotations(annotations, visitorContext, problems)), { ...marks, decorators, annotations };
}
function validateLists(lists, visitorContext, problems) {
return Array.isArray(lists) ? (lists.forEach((list, index) => {
if (!isPlainObject(list)) {
problems.push(error(`List must be an object, got ${getTypeOf(list)}`));
return;
}
const name = list.value || `#${index}`, disallowedKeys = Object.keys(list).filter(
(key) => !allowedListKeys.includes(key) && !key.startsWith("_")
);
disallowedKeys.length > 0 && problems.push(
error(
`Found unknown properties for list ${name}: ${humanizeList(disallowedKeys.map(quote$1))}`
)
), list.value ? typeof list.value != "string" ? problems.push(
error(
`List type #${index} has an invalid "value" property, expected string, got ${getTypeOf(
list.value
)}`
)
) : list.title || problems.push(warning(`List type ${name} is missing recommended "title" property`)) : problems.push(error(`List #${index} is missing required "value" property`));
}), lists) : (problems.push(error(`"lists" declaration should be an array, got ${getTypeOf(lists)}`)), problems);
}
function validateStyles(styles, visitorContext, problems) {
return Array.isArray(styles) ? (styles.forEach((style, index) => {
if (!isPlainObject(style)) {
problems.push(error(`Style must be an object, got ${getTypeOf(style)}`));
return;
}
const name = style.value || `#${index}`, disallowedKeys = Object.keys(style).filter(
(key) => !allowedStyleKeys.includes(key) && !key.startsWith("_")
);
disallowedKeys.length > 0 && problems.push(
error(
`Found unknown properties for style ${name}: ${humanizeList(disallowedKeys.map(quote$1))}`
)
), style.value ? typeof style.value != "string" ? problems.push(
error(
`Style #${index} has an invalid "value" property, expected string, got ${getTypeOf(
style.value
)}`
)
) : style.title || problems.push(warning(`Style ${name} is missing recommended "title" property`)) : problems.push(error(`Style #${index} is missing required "value" property`)), typeof style.blockEditor < "u" && (problems.push(
warning(
'Style has deprecated key "blockEditor", please refer to the documentation on how to configure the block type for version 3.',
HELP_IDS.DEPRECATED_BLOCKEDITOR_KEY
)
), style.component = style.component || style.blockEditor.render);
}), styles) : (problems.push(error(`"styles" declaration should be an array, got ${getTypeOf(styles)}`)), problems);
}
function validateDecorators(decorators, visitorContext, problems) {
return decorators.forEach((decorator, index) => {
if (!isPlainObject(decorator)) {
problems.push(error(`Annotation must be an object, got ${getTypeOf(decorator)}`));
return;
}
const name = decorator.value || `#${index}`, disallowedKeys = Object.keys(decorator).filter(
(key) => !allowedDecoratorKeys.includes(key) && !key.startsWith("_")
);
disallowedKeys.length > 0 && problems.push(
error(
`Found unknown properties for decorator ${name}: ${humanizeList(
disallowedKeys.map(quote$1)
)}`
)
), decorator.value ? typeof decorator.value != "string" ? problems.push(
error(
`Decorator #${index} has an invalid "value" property, expected string, got ${getTypeOf(
decorator.value
)}`
)
) : decorator.title || problems.push(warning(`Decorator ${name} is missing recommended "title" property`)) : problems.push(error(`Decorator #${index} is missing required "value" property`)), typeof decorator.blockEditor < "u" && (problems.push(
warning(
`Decorator "${name}" has deprecated key "blockEditor", please refer to the documentation on how to configure the block type for version 3.`,
HELP_IDS.DEPRECATED_BLOCKEDITOR_KEY
)
), decorator.icon = decorator.icon || decorator.blockEditor.icon, decorator.component = decorator.component || decorator.blockEditor.render);
}), decorators;
}
function validateAnnotations(annotations, visitorContext, problems) {
return annotations.map((annotation) => {
if (!isPlainObject(annotation))
return {
...annotation,
_problems: [error(`Annotation must be an object, got ${getTypeOf(annotation)}`)]
};
const { _problems } = visitorContext.visit(annotation, visitorContext), targetType = annotation.type && visitorContext.getType(annotation.type);
return targetType && !isJSONTypeOf(targetType, "object", visitorContext) && _problems.push(
error(
`Annotation cannot have type "${annotation.type}" - annotation types must inherit from object`
)
), typeof annotation.blockEditor < "u" && (problems.push(
warning(
'Annotation has deprecated key "blockEditor", please refer to the documentation on how to configure the block type for version 3.',
HELP_IDS.DEPRECATED_BLOCKEDITOR_KEY
)
), annotation.icon = annotation.icon || annotation.blockEditor.icon, annotation.blockEditor?.render && !annotation.components?.annotation && (annotation.components = annotation.components || {}, annotation.components.annotation = annotation.components.annotation || annotation.blockEditor.render)), { ...annotation, _problems };
});
}
function validateMembers(members, visitorContext, problems) {
if (!Array.isArray(members)) {
problems.push(error(`"of"