UNPKG

@sanity/schema

Version:
1,409 lines • 75.8 kB
import difference from "lodash/difference.js"; import { DEFAULT_MAX_FIELD_DEPTH, resolveSearchConfig, resolveSearchConfigForBaseFieldPaths } from "./_chunks-es/resolve.mjs"; 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 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" ? { name: schemaType.name, type: "type", value: { type: "object", attributes: { _type: { type: "objectAttribute", value: { type: "string", value: schemaType.name } }, ...value.attributes } } } : { 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 [.../* @__PURE__ */ 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, 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" declaration should be an array, got ${getTypeOf(members)}`)); return; } return members.map((member) => { const { _problems } = visitorContext.visit(member, visitorContext); if (member.type === "object" && member.name && visitorContext.getType(member.name)) return { ...member, _problems: [ warning( `Found array member declaration with the same name as the global schema type "${member.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 ) ] }; let type = member; for (; type && !type.jsonType; ) type = visitorContext.getType(type.type); const nonObjectCoreTypes = coreTypeNames.filter((n) => !supportedBuiltInObjectTypes.includes(n)); return ( // Must be object-like type (to validate hoisted types) type && type.jsonType !== "object" || // Can't be a core type, or core object type that isn't supported (like 'span') nonObjectCoreTypes.some((coreName) => coreName === member.type) ? { ...member, _problems: [ error( `Block member types must be a supported object-like type. The following built-in types are supported: '${supportedBuiltInObjectTypes.join( "', '" )}'. You can also use shorthands for previously defined object types like {type: 'myObjectType'}`, HELP_IDS.ARRAY_OF_TYPE_BUILTIN_TYPE_CONFLICT ) ] } : { ...member, _problems } ); }); } function validateNonObjectFieldsProp(typeDef, visitorContext) { if (!("fields" in typeDef)) return []; let type = typeDef; for (; type && !type.jsonType; ) type = visitorContext.getType(type.type); return type && type.jsonType !== "object" ? [error('Type has propery "fields", but is not an object/document type.')] : []; } const quote = (str) => `"${str}"`; function validateTypeName(typeName, visitorContext) { const possibleTypeNames = visitorContext.getTypeNames(); if (!typeName) return [error("Type is missing a type.", HELP_IDS.TYPE_MISSING_TYPE)]; if (typeof typeName != "string") return [ error( 'Type has an invalid "type"-property - should be a string.', HELP_IDS.TYPE_MISSING_TYPE ) ]; if (!possibleTypeNames.includes(typeName)) { const suggestions = possibleTypeNames.map((possibleTypeName) => [leven(typeName, possibleTypeName), possibleTypeName]).filter(([distance]) => distance < 3).map(([_, name]) => name), suggestion = suggestions.length > 0 ? ` Did you mean ${humanizeList(suggestions.map(quote), { conjunction: "or" })}?` : ""; return [error(`Unknown type: ${typeName}.${suggestion}`)]; } return []; } function validateDeprecatedProperties(type) { const warnings = []; return type?.inputComponent && warnings.push( warning('The "inputComponent" property is deprecated. Use "components.input" instead.') ), type?.preview?.component && warnings.push( warning('The "preview.component" property is deprecated. Use "components.preview" instead.') ), type?.diffComponent && warnings.push( warning('The "diffComponent" property is deprecated. Use "components.diff" instead.') ), type?.options?.editModal && warnings.push( warning('The "options.editModal" property is deprecated. Use "options.modal" instead.') ), type?.options?.isHighlighted && warnings.push( warning( 'The "options.isHighlighted" property is deprecated. You can put fields behind a collapsed fieldset if you want to hide them from plain sight.' ) ), warnings; } var common = (typeDef, visitorContext) => ({ ...typeDef, _problems: [ ...validateTypeName(typeDef.type, visitorContext), ...validateNonObjectFieldsProp(typeDef, visitorContext), ...validateDeprecatedProperties(typeDef) ].filter(Boolean) }); function normalizeToProp$2(typeDef) { return Array.isArray(typeDef.to) ? typeDef.to : typeDef.to ? [typeDef.to] : typeDef.to; } const VALID_DATASET = /^[a-z0-9~][-_a-z0-9]+$/; function isValidDatasetName(name) { return name.length >= 2 && name.toLowerCase() === name && VALID_DATASET.test(name) || `The provided dataset "${name}" doesn't look like a valid dataset. Dataset names must be more than 2 characters, can only contain lowercase characters, numbers, underscores and dashes and can not start with a dash or an underscore`; } var crossDatasetReference = (typeDef, visitorContext) => { const isValidTo = Array.isArray(typeDef.to) || isPlainObject(typeDef.to), normalizedTo = normalizeToProp$2(typeDef), problems = flatten([ isValidTo ? getDupes(normalizedTo, (t) => `${t.na