UNPKG

@sanity/schema

Version:
1,465 lines • 81.9 kB
import { SetBuilder, processSetSynchronization } from "@sanity/descriptors"; import { OWN_PROPS_NAME } from "./_chunks-es/resolve.mjs"; import { DEFAULT_MAX_FIELD_DEPTH, resolveSearchConfig, resolveSearchConfigForBaseFieldPaths } from "./_chunks-es/resolve.mjs"; import difference from "lodash/difference.js"; import { createReferenceTypeNode } from "groq-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"; 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 }) => ({ name, title: maybeString(title), hidden: conditionalTrue(hidden), default: maybeTrue(def) }) ) )); const reason = ownProps.deprecated?.reason; return { 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 }; } 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), typeof val == "object") { 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 (val && typeof val == "object" && "$$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 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" }, { name: "bottom", type: "number" }, { name: "left", type: "number" }, { name: "right", type: "number" } ] }, imageDimensions = { name: "sanity.imageDimensions", type: "object", title: "Image dimensions", fields: [ { name: "height", type: "number", title: "Height", readOnly: !0 }, { name: "width", type: "number", title: "Width", readOnly: !0 }, { name: "aspectRatio", type: "number", title: "Aspect ratio", readOnly: !0 } ] }, imageHotspot = { name: "sanity.imageHotspot", title: "Image hotspot", type: "object", fields: [ { name: "x", type: "number" }, { name: "y", type: "number" }, { name: "height", type: "number" }, { name: "width", type: "number" } ] }, 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: (Rule) => Rule.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 ], documentDefaultFields = (typeName) => ({ _id: { type: "objectAttribute", value: { type: "string" } }, _type: { type: "objectAttribute", value: { type: "string", value: typeName } }, _createdAt: { type: "objectAttribute", value: { type: "string" } }, _updatedAt: { type: "objectAttribute", value: { type: "string" } }, _rev: { type: "objectAttribute", value: { type: "string" } } }), typesMap = /* @__PURE__ */ new Map([ ["text", { type: "string" }], ["url", { type: "string" }], ["datetime", { type: "string" }], ["date", { type: "string" }], ["boolean", { type: "boolean" }], ["email", { type: "string" }] ]); function extractSchema(schemaDef, extractOptions = {}) { const inlineFields = /* @__PURE__ */ new Set(), documentTypes = /* @__PURE__ */ new Map(), schema = []; sortByDependencies(schemaDef).forEach((typeName) => { const schemaType = schemaDef.get(typeName); if (schemaType === void 0) return; const base = convertBaseType(schemaType); base !== null && (base.type === "type" && inlineFields.add(schemaType), base.type === "document" && documentTypes.set(typeName, base), schema.push(base)); }); function convertBaseType(schemaType) { let typeName; if (schemaType.type ? typeName = schemaType.type.name : "jsonType" in schemaType && (typeName = schemaType.jsonType), typeName === "document" && isObjectType(schemaType)) { const defaultAttributes = documentDefaultFields(schemaType.name), object2 = createObject(schemaType); return object2.type === "unknown" ? null : { name: schemaType.name, type: "document", attributes: { ...defaultAttributes, ...object2.attributes } }; } const value = convertSchemaType(schemaType); return value.type === "unknown" ? null : value.type === "object" ? (value.attributes = { _type: { type: "objectAttribute", value: { type: "string", value: schemaType.name } }, ...value.attributes }, { name: schemaType.name, type: "type", value }) : { name: schemaType.name, type: "type", value }; } function convertSchemaType(schemaType) { if (inlineFields.has(schemaType.type)) return { type: "inline", name: schemaType.type.name }; if (schemaType.type?.type?.name === "object") return { type: "inline", name: schemaType.type.name }; if (isStringType(schemaType)) return createStringTypeNodeDefintion(schemaType); if (isNumberType(schemaType)) return createNumberTypeNodeDefintion(schemaType); if (schemaType.type && typesMap.has(schemaType.type.name)) return typesMap.get(schemaType.type.name); if (isCrossDatasetReferenceType(schemaType)) return { type: "unknown" }; if (isGlobalDocumentReferenceType(schemaType)) return { type: "unknown" }; if (isReferenceType(schemaType)) return createReferenceTypeNodeDefintion(schemaType); if (isArrayType(schemaType)) return createArray(schemaType); if (isObjectType(schemaType)) return createObject(schemaType); if (lastType(schemaType)?.name === "document") { const doc = documentTypes.get(schemaType.name); return doc === void 0 ? { type: "unknown" } : { type: "object", attributes: doc?.attributes }; } throw new Error(`Type "${schemaType.name}" not found`); } function createObject(schemaType) { const attributes = {}, fields = gatherFields(schemaType); for (const field of fields) { const fieldIsRequired = isFieldRequired(field), value = convertSchemaType(field.type); if (value === null) continue; hasAssetRequired(field) && value.type === "object" && (value.attributes.asset.optional = !1); const optional = extractOptions.enforceRequiredFields ? fieldIsRequired === !1 : !0; attributes[field.name] = { type: "objectAttribute", value, optional }; } return Object.keys(attributes).length === 0 ? { type: "unknown" } : (schemaType.type?.name !== "document" && schemaType.name !== "object" && (attributes._type = { type: "objectAttribute", value: { type: "string", value: schemaType.name } }), { type: "object", attributes }); } function createArray(arraySchemaType) { const of = []; for (const item of arraySchemaType.of) { const field = convertSchemaType(item); field.type === "inline" ? of.push({ type: "object", attributes: { _key: createKeyField() }, rest: field }) : (field.type === "object" && (field.rest = { type: "object", attributes: { _key: createKeyField() } }), of.push(field)); } return of.length === 0 ? { type: "null" } : { type: "array", of: of.length > 1 ? { type: "union", of } : of[0] }; } return schema; } function createKeyField() { return { type: "objectAttribute", value: { type: "string" } }; } function isFieldRequired(field) { const { validation } = field.type; if (!validation) return !1; const rules = Array.isArray(validation) ? validation : [validation]; for (const rule of rules) { let required = !1; const proxy = new Proxy( {}, { get: (target, methodName) => () => (methodName === "required" && (required = !0), proxy) } ); if (typeof rule == "function" && (rule(proxy), required) || typeof rule == "object" && rule !== null && "_required" in rule && rule._required === "required") return !0; } return !1; } function hasAssetRequired(field) { const { validation } = field.type; if (!validation) return !1; const rules = Array.isArray(validation) ? validation : [validation]; for (const rule of rules) { let assetRequired = !1; const proxy = new Proxy( {}, { get: (target, methodName) => () => (methodName === "assetRequired" && (assetRequired = !0), proxy) } ); if (typeof rule == "function" && (rule(proxy), assetRequired) || typeof rule == "object" && rule !== null && "_rules" in rule && Array.isArray(rule._rules) && rule._rules.some((r) => r.flag === "assetRequired")) return !0; } return !1; } function isObjectType(typeDef) { return isType(typeDef, "object") || typeDef.jsonType === "object" || "fields" in typeDef; } function isArrayType(typeDef) { return isType(typeDef, "array"); } function isReferenceType(typeDef) { return isType(typeDef, "reference"); } function isCrossDatasetReferenceType(typeDef) { return isType(typeDef, "crossDatasetReference"); } function isGlobalDocumentReferenceType(typeDef) { return isType(typeDef, "globalDocumentReference"); } function isStringType(typeDef) { return isType(typeDef, "string"); } function isNumberType(typeDef) { return isType(typeDef, "number"); } function createStringTypeNodeDefintion(stringSchemaType) { const listOptions = stringSchemaType.options?.list; return listOptions && Array.isArray(listOptions) ? { type: "union", of: listOptions.map((v) => ({ type: "string", value: typeof v == "string" ? v : v.value })) } : { type: "string" }; } function createNumberTypeNodeDefintion(numberSchemaType) { const listOptions = numberSchemaType.options?.list; return listOptions && Array.isArray(listOptions) ? { type: "union", of: listOptions.map((v) => ({ type: "number", value: typeof v == "number" ? v : v.value })) } : { type: "number" }; } function createReferenceTypeNodeDefintion(reference2) { const references = gatherReferenceNames(reference2); return references.length === 1 ? createReferenceTypeNode(references[0]) : { type: "union", of: references.map((name) => createReferenceTypeNode(name)) }; } function gatherReferenceNames(type) { const allReferences = gatherReferenceTypes(type); return [...new Set(allReferences.map((ref) => ref.name))]; } function gatherReferenceTypes(type) { const refTo = "to" in type ? type.to : []; return "type" in type && isReferenceType(type.type) ? [...gatherReferenceTypes(type.type), ...refTo] : refTo; } function gatherFields(type) { return "fields" in type ? type.type ? gatherFields(type.type).concat(type.fields) : type.fields : []; } function isType(typeDef, typeName) { let type = typeDef; for (; type; ) { if (type.name === typeName || type.type && type.type.name === typeName) return !0; type = type.type; } return !1; } function lastType(typeDef) { let type = typeDef; for (; type; ) { if (!type.type) return type; type = type.type; } } function sortByDependencies(compiledSchema) { const seen = /* @__PURE__ */ new Set(); function walkDependencies(schemaType, dependencies) { if (!seen.has(schemaType)) { if (seen.add(schemaType), "fields" in schemaType) for (const field of gatherFields(schemaType)) { const last = lastType(field.type); if (last.name === "document") { dependencies.add(last); continue; } let schemaTypeName; schemaType.type.type ? schemaTypeName = field.type.type.name : "jsonType" in schemaType.type && (schemaTypeName = field.type.jsonType), (schemaTypeName === "object" || schemaTypeName === "block") && (isReferenceType(field.type) ? field.type.to.forEach((ref) => dependencies.add(ref.type)) : dependencies.add(field.type)), walkDependencies(field.type, dependencies); } else if ("of" in schemaType) for (const item of schemaType.of) walkDependencies(item, dependencies); } } const dependencyMap = /* @__PURE__ */ new Map(); compiledSchema.getTypeNames().forEach((typeName) => { const schemaType = compiledSchema.get(typeName); if (schemaType === void 0 || schemaType.type === null) return; const dependencies = /* @__PURE__ */ new Set(); walkDependencies(schemaType, dependencies), dependencyMap.set(schemaType, dependencies), seen.clear(); }); const typeNames = [], currentlyVisiting = /* @__PURE__ */ new Set(), visited = /* @__PURE__ */ new Set(); function visit(type) { if (visited.has(type) || currentlyVisiting.has(type)) return; currentlyVisiting.add(type); const deps = dependencyMap.get(type); deps !== void 0 && deps.forEach((dep) => visit(dep)), currentlyVisiting.delete(type), visited.add(type), typeNames.includes(type.name) || typeNames.unshift(type.name); } for (const [type] of dependencyMap) visit(type); return typeNames; } const 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, prob