UNPKG

@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
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"