From 0d38a52f119bd5239484a99c78064522728968c9 Mon Sep 17 00:00:00 2001 From: Sindre Gulseth Date: Wed, 6 Mar 2024 14:51:53 +0100 Subject: [PATCH] feat: add cmd to generate a JSON representation of schema --- packages/@sanity/schema/package.json | 2 + .../@sanity/schema/src/_exports/_internal.ts | 1 + .../schema/src/sanity/extractSchema.ts | 712 +++++ .../__snapshots__/extractSchema.test.ts.snap | 2767 +++++++++++++++++ .../test/extractSchema/extractSchema.test.ts | 138 + .../test/extractSchema/fixtures/block.ts | 514 +++ .../cli/actions/schema/extractAction.ts | 71 + .../src/_internal/cli/commands/index.ts | 2 + .../commands/schema/extractSchemaCommand.ts | 32 + .../_internal/cli/threads/extractSchema.ts | 73 + pnpm-lock.yaml | 15 + 11 files changed, 4327 insertions(+) create mode 100644 packages/@sanity/schema/src/sanity/extractSchema.ts create mode 100644 packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap create mode 100644 packages/@sanity/schema/test/extractSchema/extractSchema.test.ts create mode 100644 packages/@sanity/schema/test/extractSchema/fixtures/block.ts create mode 100644 packages/sanity/src/_internal/cli/actions/schema/extractAction.ts create mode 100644 packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts create mode 100644 packages/sanity/src/_internal/cli/threads/extractSchema.ts diff --git a/packages/@sanity/schema/package.json b/packages/@sanity/schema/package.json index 5606fd81cd50..4dbec33b9a90 100644 --- a/packages/@sanity/schema/package.json +++ b/packages/@sanity/schema/package.json @@ -77,6 +77,7 @@ "@sanity/generate-help-url": "^3.0.0", "@sanity/types": "3.32.0", "arrify": "^1.0.1", + "groq-js": "1.5.0-canary.1", "humanize-list": "^1.0.1", "leven": "^3.1.0", "lodash": "^4.17.21", @@ -84,6 +85,7 @@ }, "devDependencies": { "@jest/globals": "^29.7.0", + "@sanity/icons": "^2.8.0", "rimraf": "^3.0.2" } } diff --git a/packages/@sanity/schema/src/_exports/_internal.ts b/packages/@sanity/schema/src/_exports/_internal.ts index 2e073fe4d76d..1da4b6c341b5 100644 --- a/packages/@sanity/schema/src/_exports/_internal.ts +++ b/packages/@sanity/schema/src/_exports/_internal.ts @@ -4,6 +4,7 @@ export { resolveSearchConfig, resolveSearchConfigForBaseFieldPaths, } from '../legacy/searchConfig/resolve' +export {extractSchema} from '../sanity/extractSchema' export {groupProblems} from '../sanity/groupProblems' export { type _FIXME_ as FIXME, diff --git a/packages/@sanity/schema/src/sanity/extractSchema.ts b/packages/@sanity/schema/src/sanity/extractSchema.ts new file mode 100644 index 000000000000..b4539043f31a --- /dev/null +++ b/packages/@sanity/schema/src/sanity/extractSchema.ts @@ -0,0 +1,712 @@ +import { + type ArrayDefinition, + type ArrayOfType, + type BlockDefinition, + type CrossDatasetReferenceDefinition, + type DocumentDefinition, + type FieldDefinition, + type FileDefinition, + type ImageDefinition, + type NumberDefinition, + type ObjectDefinition, + type ReferenceDefinition, + type Rule, + type SchemaTypeDefinition, + type StringDefinition, +} from '@sanity/types' +import { + type ArrayTypeNode, + createReferenceTypeNode, + type InlineTypeNode, + type NumberTypeNode, + type ObjectAttribute, + type ObjectTypeNode, + type PrimitiveTypeNode, + type SchemaType, + type StringTypeNode, + type TypeNode, + type UnionTypeNode, + type UnknownTypeNode, +} from 'groq-js' + +const documentDefaultFields = (typeName: string): Record => ({ + _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'}, + }, +}) +const typesMap = new Map([ + ['text', {type: 'string'}], + ['url', {type: 'string'}], + ['datetime', {type: 'string'}], + ['date', {type: 'string'}], + ['boolean', {type: 'boolean'}], + ['email', {type: 'string'}], +]) + +export interface ExtractSchemaOptions { + enforceRequiredFields?: boolean +} + +export function extractSchema( + schemaTypeDefinitions: SchemaTypeDefinition[], + extractOptions: ExtractSchemaOptions = {}, +): SchemaType { + const schema: SchemaType = [] + schemaTypeDefinitions.forEach((type) => { + if (isDocumentType(type)) { + const attributes = documentDefaultFields(type.name) satisfies Record + + for (const field of type.fields || []) { + const fieldIsRequired = isFieldRequired(field) + attributes[field.name] = { + type: 'objectAttribute', + optional: extractOptions.enforceRequiredFields ? fieldIsRequired : true, + value: parseField(field, extractOptions), + } satisfies ObjectAttribute + } + + schema.push({ + name: type.name, + type: 'document', + attributes, + }) + return + } + + if (isObjectType(type)) { + const attributes = type.fields.reduce>((acc, field) => { + const fieldIsRequired = isFieldRequired(field) + acc[field.name] = { + type: 'objectAttribute', + optional: extractOptions.enforceRequiredFields ? !fieldIsRequired : true, + value: parseField(field, extractOptions), + } satisfies ObjectAttribute + + return acc + }, {}) satisfies Record + + attributes._type = { + type: 'objectAttribute', + value: { + type: 'string', + value: type.name, + }, + } satisfies ObjectAttribute + + schema.push({ + name: type.name, + type: 'type', + value: { + type: 'object', + attributes, + }, + }) + return + } + if (isArrayType(type)) { + schema.push({ + type: 'type', + name: type.name, + value: createArray(type, extractOptions), + }) + return + } + + if (isBlockType(type)) { + schema.push({ + type: 'type', + name: type.name, + value: createBlock(type, extractOptions), + }) + return + } + if (isImageType(type)) { + schema.push({ + type: 'type', + name: type.name, + value: createImage(type, extractOptions), + }) + return + } + if (isFileType(type)) { + schema.push({ + type: 'type', + name: type.name, + value: createFile(type, extractOptions), + }) + return + } + + if (isReferenceType(type)) { + schema.push({ + type: 'type', + name: type.name, + value: createReferenceTypeNodeDefintion(type), + }) + return + } + + if (isCrossDatasetReferenceType(type)) { + schema.push({ + type: 'type', + name: type.name, + value: createCrossDatasetReferenceTypeNodeDefintion(type), + }) + return + } + + if (isStringType(type)) { + schema.push({ + type: 'type', + name: type.name, + value: createStringTypeNodeDefintion(type), + }) + } + + if (isNumberType(type)) { + schema.push({ + type: 'type', + name: type.name, + value: createNumberTypeNodeDefintion(type), + }) + } + + if (typesMap.has(type.type)) { + schema.push({ + type: 'type', + name: type.name, + value: typesMap.get(type.type), + }) + return + } + + schema.push({ + type: 'type', + name: type.name, + value: { + type: 'inline', + name: type.type, + } satisfies InlineTypeNode, + }) + }) + + return schema +} + +function createKeyField(): ObjectAttribute { + return { + type: 'objectAttribute', + value: { + type: 'string', + }, + } +} + +function isFieldRequired(field: FieldDefinition): boolean { + if (!field.validation) { + return false + } + const rules = Array.isArray(field.validation) ? field.validation : [field.validation] + for (const rule of rules) { + let required = false + const proxy = new Proxy( + {}, + { + get: (target, methodName) => () => { + if (methodName === 'required') { + required = true + } + return proxy + }, + }, + ) as Rule + if (typeof rule === 'function') { + rule(proxy) + if (required) { + return true + } + } + } + + return false +} + +function parseField(field: FieldDefinition, extractOptions: ExtractSchemaOptions): TypeNode { + if (isObjectType(field)) { + const attributes: Record = {} + field.fields.forEach((f) => { + const fieldIsRequired = isFieldRequired(f) + attributes[f.name] = { + type: 'objectAttribute', + value: parseField(f, extractOptions), + optional: extractOptions.enforceRequiredFields ? fieldIsRequired : true, + } + }) + attributes._type = { + type: 'objectAttribute', + value: { + type: 'string', + value: field.name, + }, + } satisfies ObjectAttribute + + return { + type: field.type, + attributes, + } + } + + if (isArrayType(field)) { + return createArray(field, extractOptions) + } + + if (isBlockType(field)) { + return createBlock(field, extractOptions) + } + if (isImageType(field)) { + return createImage(field, extractOptions) + } + if (isFileType(field)) { + return createFile(field, extractOptions) + } + + if (isReferenceType(field)) { + return createReferenceTypeNodeDefintion(field) + } + + if (isCrossDatasetReferenceType(field)) { + return createCrossDatasetReferenceTypeNodeDefintion(field) + } + + if (isStringType(field)) { + return createStringTypeNodeDefintion(field) + } + + if (isNumberType(field)) { + return createNumberTypeNodeDefintion(field) + } + + if (typesMap.has(field.type)) { + return typesMap.get(field.type) + } + + return { + type: 'inline', + name: field.type, + } +} + +function isDocumentType(n: SchemaTypeDefinition): n is DocumentDefinition { + return n.type === 'document' +} +function isFieldDefinition(n: unknown): n is FieldDefinition { + return ( + n !== null && + typeof n === 'object' && + 'type' in n && + (('fieldset' in n && typeof n.fieldset === 'string') || + !('fieldset' in n) || + typeof n.fieldset === 'undefined') && + (('group' in n && (typeof n.group === 'string' || Array.isArray(n.group))) || + !('group' in n) || + typeof n.group === 'undefined') + ) +} + +function isObjectType(n: {type: string}): n is ObjectDefinition { + return n.type === 'object' +} +function isArrayType(n: {type: string}): n is ArrayDefinition { + return n.type === 'array' +} +function isBlockType(n: {type: string}): n is BlockDefinition { + return n.type === 'block' +} +function isReferenceType(n: {type: string}): n is ReferenceDefinition { + return n.type === 'reference' +} +function isCrossDatasetReferenceType(n: {type: string}): n is CrossDatasetReferenceDefinition { + return n.type === 'crossDatasetReference' +} +function isImageType(n: {type: string}): n is ImageDefinition { + return n.type === 'image' +} +function isFileType(n: {type: string}): n is FileDefinition { + return n.type === 'file' +} +function isStringType(n: {type: string}): n is StringDefinition { + return n.type === 'string' +} +function isNumberType(n: {type: string}): n is NumberDefinition { + return n.type === 'number' +} + +function createPrimitiveAttribute( + key: string, + type: PrimitiveTypeNode['type'], + optional = false, +): Record> { + return { + [key]: { + type: 'objectAttribute', + value: {type}, + optional, + }, + } +} + +function createStringTypeNodeDefintion( + stringDefinition: StringDefinition, +): StringTypeNode | UnionTypeNode { + if (stringDefinition.options?.list) { + return { + type: 'union', + of: stringDefinition.options.list.map((v) => ({ + type: 'string', + value: typeof v === 'string' ? v : v.value, + })), + } + } + return { + type: 'string', + } +} + +function createNumberTypeNodeDefintion( + numberDefinition: NumberDefinition, +): NumberTypeNode | UnionTypeNode { + if (numberDefinition.options?.list) { + return { + type: 'union', + of: numberDefinition.options.list.map((v) => ({ + type: 'number', + value: typeof v === 'number' ? v : v.value, + })), + } + } + return { + type: 'number', + } +} + +function createImage( + imageDefinition: ImageDefinition, + extractOptions: ExtractSchemaOptions, +): ObjectTypeNode { + const attributes: Record = {} + for (const field of imageDefinition.fields || []) { + const fieldIsRequired = isFieldRequired(field) + attributes[field.name] = { + type: 'objectAttribute', + value: parseField(field, extractOptions), + optional: extractOptions.enforceRequiredFields ? fieldIsRequired : true, + } + } + + if (imageDefinition.options?.hotspot) { + attributes.hotspot = { + type: 'objectAttribute', + value: { + type: 'object', + attributes: { + _type: { + type: 'objectAttribute', + value: { + type: 'string', + value: 'sanity.imageHotspot', + }, + }, + ...createPrimitiveAttribute('x', 'number'), + ...createPrimitiveAttribute('y', 'number'), + ...createPrimitiveAttribute('height', 'number'), + ...createPrimitiveAttribute('width', 'number'), + }, + }, + optional: true, + } + attributes.crop = { + type: 'objectAttribute', + value: { + type: 'object', + attributes: { + _type: { + type: 'objectAttribute', + value: { + type: 'string', + value: 'sanity.imageCrop', + }, + }, + ...createPrimitiveAttribute('top', 'number'), + ...createPrimitiveAttribute('bottom', 'number'), + ...createPrimitiveAttribute('left', 'number'), + ...createPrimitiveAttribute('right', 'number'), + }, + }, + optional: true, + } + } + return { + type: 'object', + attributes: { + _type: { + type: 'objectAttribute', + value: { + type: 'string', + value: 'image', + }, + }, + asset: { + type: 'objectAttribute', + value: createReferenceTypeNode('sanity.imageAsset'), + }, + ...attributes, + }, + } +} +function createFile( + fileDefinition: FileDefinition, + extractOptions: ExtractSchemaOptions, +): ObjectTypeNode { + const attributes: Record = {} + for (const field of fileDefinition.fields || []) { + const fieldIsRequired = isFieldRequired(field) + attributes[field.name] = { + type: 'objectAttribute', + value: parseField(field, extractOptions), + optional: extractOptions.enforceRequiredFields ? fieldIsRequired : true, + } + } + + return { + type: 'object', + attributes: { + _type: { + type: 'objectAttribute', + value: { + type: 'string', + value: 'file', + }, + }, + ...attributes, + }, + } +} + +function createArray( + arrayDefinition: ArrayDefinition, + extractOptions: ExtractSchemaOptions, +): ArrayTypeNode { + const of = [ + ...arrayDefinition.of.map((f) => { + if (isFieldDefinition(f)) { + const field = parseField(f, extractOptions) + if (field.type === 'inline') { + return { + type: 'object', + attributes: { + _key: createKeyField(), + }, + rest: field, + } satisfies ObjectTypeNode + } + + if (field.type === 'object') { + field.rest = { + type: 'object', + attributes: { + _key: createKeyField(), + }, + } + return field + } + + return field + } + + if (typesMap.has(f.type)) { + return typesMap.get(f.type) + } + + return createReferenceTypeNode(f.type, true) + }), + ] satisfies TypeNode[] + + return { + type: 'array', + of: + of.length > 1 + ? { + type: 'union', + of, + } + : of[0], + } +} + +function createBlock( + blockDefinition: BlockDefinition, + extractOptions: ExtractSchemaOptions, +): ObjectTypeNode { + const styleField = { + type: 'objectAttribute', + optional: true, + value: { + type: 'union', + of: + blockDefinition.styles?.map((style) => ({ + type: 'string', + value: style.value, + })) || [], + }, + } satisfies ObjectAttribute> + const listItemField = { + type: 'objectAttribute', + optional: true, + value: { + type: 'union', + of: + blockDefinition.lists?.map((list) => ({ + type: 'string', + value: list.value, + })) || [], + }, + } satisfies ObjectAttribute> + const levelField = { + type: 'objectAttribute', + optional: true, + value: { + type: 'number', + }, + } satisfies ObjectAttribute + const marks: TypeNode[] = [ + {type: 'string'}, + ...(blockDefinition.marks?.decorators?.map( + (mark): TypeNode => ({ + type: 'string', + value: mark.value, + }), + ) || []), + ] + const childrenField = { + type: 'objectAttribute', + value: { + type: 'array', + of: { + type: 'union', + of: [ + { + type: 'object', + attributes: { + _key: createKeyField(), + text: { + type: 'objectAttribute', + value: { + type: 'string', + }, + } satisfies ObjectAttribute, + marks: { + type: 'objectAttribute', + value: { + type: 'array', + of: { + type: 'union', + of: marks, + }, + }, + } satisfies ObjectAttribute>, + }, + } satisfies ObjectTypeNode, + ], + } satisfies UnionTypeNode, + }, + } satisfies ObjectAttribute>> + + const markDefsField: ObjectAttribute = { + type: 'objectAttribute', + value: { + type: 'array', + of: { + type: 'union', + of: + blockDefinition.marks?.annotations?.map((annotation) => + createMarkDefField(annotation, extractOptions), + ) || [], + }, + }, + } + return { + type: 'object', + attributes: { + _key: createKeyField(), + level: levelField, + style: styleField, + listItem: listItemField, + children: childrenField, + markDefs: markDefsField, + }, + } +} + +function createMarkDefField( + annotation: ArrayOfType<'object' | 'reference'>, + extractOptions: ExtractSchemaOptions, +): TypeNode { + if (annotation.type === 'object' && 'fields' in annotation) { + const attributes: Record = {} + for (const field of annotation.fields) { + attributes[field.name] = { + type: 'objectAttribute', + value: parseField(field, extractOptions), + optional: true, + } satisfies ObjectAttribute + } + + return { + type: 'object', + attributes, + } + } + + if (annotation.type === 'reference' && 'to' in annotation) { + return createReferenceTypeNodeDefintion(annotation) + } + + return { + type: 'object', + attributes: {}, + } +} + +function createReferenceTypeNodeDefintion( + reference: Pick, +): ObjectTypeNode | UnionTypeNode { + if (Array.isArray(reference.to)) { + return { + type: 'union', + of: reference.to.map((t) => createReferenceTypeNode(t.type)), + } + } + return createReferenceTypeNode(reference.to.type) +} +function createCrossDatasetReferenceTypeNodeDefintion( + _: CrossDatasetReferenceDefinition, +): TypeNode { + return {type: 'unknown'} satisfies UnknownTypeNode +} diff --git a/packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap b/packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap new file mode 100644 index 000000000000..90ae6b155533 --- /dev/null +++ b/packages/@sanity/schema/test/extractSchema/__snapshots__/extractSchema.test.ts.snap @@ -0,0 +1,2767 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Extract schema test Extracts schema general 1`] = ` +Array [ + Object { + "attributes": Object { + "_createdAt": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_id": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_rev": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "validDocument", + }, + }, + "_updatedAt": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "blocks": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + "type": "array", + }, + }, + "list": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [ + Object { + "type": "string", + "value": "a", + }, + Object { + "type": "string", + "value": "b", + }, + Object { + "type": "string", + "value": "c", + }, + ], + "type": "union", + }, + }, + "number": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "other": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "otherValidDocument", + "type": "object", + }, + }, + "others": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [ + Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "otherValidDocument", + "type": "object", + }, + ], + "type": "union", + }, + }, + "someInlinedObject": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "name": "obj", + "type": "inline", + }, + }, + "title": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "name": "validDocument", + "type": "document", + }, + Object { + "name": "blocks", + "type": "type", + "value": Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "blocks", + }, + }, + "arrayOfArticles": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "blocksTest", + "type": "inline", + }, + "type": "object", + }, + "type": "array", + }, + }, + "blockInBlock": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + "type": "array", + }, + }, + "blockList": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "blockListEntry", + }, + }, + "blocks": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + "type": "array", + }, + }, + "title": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + "type": "array", + }, + }, + "customized": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "author", + "type": "inline", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + Object { + "type": "string", + "value": "strong", + }, + Object { + "type": "string", + "value": "em", + }, + Object { + "type": "string", + "value": "color", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [ + Object { + "type": "string", + "value": "bullet", + }, + Object { + "type": "string", + "value": "number", + }, + ], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "author", + "type": "object", + }, + Object { + "attributes": Object { + "testString": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [ + Object { + "type": "string", + "value": "normal", + }, + Object { + "type": "string", + "value": "h1", + }, + Object { + "type": "string", + "value": "h2", + }, + Object { + "type": "string", + "value": "blockquote", + }, + ], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "deep": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "deep", + }, + }, + "blocks": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "image", + }, + }, + "asset": Object { + "type": "objectAttribute", + "value": Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "sanity.imageAsset", + "type": "object", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "author", + "type": "inline", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + Object { + "type": "string", + "value": "strong", + }, + Object { + "type": "string", + "value": "em", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [ + Object { + "type": "string", + "value": "bullet", + }, + Object { + "type": "string", + "value": "number", + }, + ], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "author", + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [ + Object { + "type": "string", + "value": "normal", + }, + Object { + "type": "string", + "value": "h1", + }, + Object { + "type": "string", + "value": "h2", + }, + Object { + "type": "string", + "value": "blockquote", + }, + ], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "something": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + }, + "defaults": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "image", + }, + }, + "asset": Object { + "type": "objectAttribute", + "value": Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "sanity.imageAsset", + "type": "object", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "author", + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "book", + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "objectWithNestedArray", + }, + }, + "array": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": undefined, + }, + }, + "author": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [ + Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "author", + "type": "object", + }, + ], + "type": "union", + }, + }, + "title": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + "type": "array", + }, + }, + "title": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "author", + "type": "inline", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "code", + "type": "inline", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "testObject", + }, + }, + "field1": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "otherTestObject", + }, + }, + "field1": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "field3": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": undefined, + }, + }, + "aNumber": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "aString": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + "type": "array", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "spotifyEmbed", + "type": "inline", + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "first": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "href": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + "type": "array", + }, + }, + "minimal": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + "type": "array", + }, + }, + "nestedWithDualColumnCTA": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "localeRichtext", + }, + }, + "en": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "image", + }, + }, + "asset": Object { + "type": "objectAttribute", + "value": Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "sanity.imageAsset", + "type": "object", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "twoColCTA", + }, + }, + "columnone": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "richTextObject", + "type": "inline", + }, + "type": "object", + }, + "type": "array", + }, + }, + "columntwo": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "richTextObject", + "type": "inline", + }, + "type": "object", + }, + "type": "array", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + "type": "array", + }, + }, + "readOnlyWithDefaults": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "image", + }, + }, + "asset": Object { + "type": "objectAttribute", + "value": Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "sanity.imageAsset", + "type": "object", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "author", + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "book", + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "author", + "type": "inline", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "code", + "type": "inline", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "testObject", + }, + }, + "field1": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "otherTestObject", + }, + }, + "field1": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "field3": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": undefined, + }, + }, + "aNumber": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "aString": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + "type": "array", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "recursive": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "recursive", + }, + }, + "blocks": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "blocksTest", + "type": "inline", + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + }, + "type": "object", + }, + }, + "reproCH9436": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "image", + }, + }, + "asset": Object { + "type": "objectAttribute", + "value": Object { + "attributes": Object { + "_ref": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "reference", + }, + }, + "_weak": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "boolean", + }, + }, + }, + "dereferencesTo": "sanity.imageAsset", + "type": "object", + }, + }, + "caption": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + Object { + "type": "string", + "value": "strong", + }, + Object { + "type": "string", + "value": "em", + }, + Object { + "type": "string", + "value": "underline", + }, + Object { + "type": "string", + "value": "strikethrough", + }, + Object { + "type": "string", + "value": "superscript", + }, + Object { + "type": "string", + "value": "subscript", + }, + Object { + "type": "string", + "value": "alignleft", + }, + Object { + "type": "string", + "value": "aligncenter", + }, + Object { + "type": "string", + "value": "alignright", + }, + Object { + "type": "string", + "value": "alignjustify", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + "type": "array", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "title": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "withGeopoint": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "children": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "marks": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [ + Object { + "type": "string", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "text": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + "level": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, + "listItem": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + "markDefs": Object { + "type": "objectAttribute", + "value": Object { + "of": Object { + "of": Array [], + "type": "union", + }, + "type": "array", + }, + }, + "style": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "of": Array [], + "type": "union", + }, + }, + }, + "rest": Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "type": "object", + }, + "type": "object", + }, + Object { + "attributes": Object { + "_key": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "rest": Object { + "name": "geopoint", + "type": "inline", + }, + "type": "object", + }, + ], + "type": "union", + }, + "type": "array", + }, + }, + }, + "type": "object", + }, + }, + Object { + "attributes": Object { + "_createdAt": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_id": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_rev": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "_type": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + "value": "otherValidDocument", + }, + }, + "_updatedAt": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + "title": Object { + "optional": true, + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, + }, + "name": "otherValidDocument", + "type": "document", + }, + Object { + "name": "object", + "type": "type", + "value": Object { + "name": "obj", + "type": "inline", + }, + }, +] +`; diff --git a/packages/@sanity/schema/test/extractSchema/extractSchema.test.ts b/packages/@sanity/schema/test/extractSchema/extractSchema.test.ts new file mode 100644 index 000000000000..83a11fb4624c --- /dev/null +++ b/packages/@sanity/schema/test/extractSchema/extractSchema.test.ts @@ -0,0 +1,138 @@ +import assert from 'node:assert' + +import {describe, expect, test} from '@jest/globals' + +import {extractSchema} from '../../src/sanity/extractSchema' +import Block from './fixtures/block' + +describe('Extract schema test', () => { + test('Extracts schema general', () => { + const schemaDef = [ + { + title: 'Valid document', + name: 'validDocument', + type: 'document', + fields: [ + { + title: 'Title', + name: 'title', + type: 'string', + }, + { + title: 'List', + name: 'list', + type: 'string', + options: { + list: ['a', 'b', 'c'], + }, + }, + { + title: 'Number', + name: 'number', + type: 'number', + }, + { + title: 'some other object', + name: 'someInlinedObject', + type: 'obj', + }, + { + title: 'Blocks', + name: 'blocks', + type: 'array', + of: [{type: 'block'}], + }, + { + type: 'reference', + name: 'other', + to: { + type: 'otherValidDocument', + }, + }, + { + type: 'reference', + name: 'others', + to: [ + { + type: 'otherValidDocument', + }, + ], + }, + ], + }, + Block, + { + title: 'Other valid document', + name: 'otherValidDocument', + type: 'document', + fields: [ + { + title: 'Title', + name: 'title', + type: 'string', + }, + ], + }, + { + title: 'Obj', + type: 'obj', + name: 'object', + fields: [ + { + title: 'Field #1', + name: 'field-1', + type: 'string', + }, + { + title: 'Field #2', + name: 'field-2', + type: 'number', + }, + ], + }, + ] + + const extracted = extractSchema(schemaDef) + expect(extracted).toHaveLength(4) + expect(extracted[0].name).toEqual('validDocument') + expect(extracted[0].type).toEqual('document') + assert(extracted[0].type === 'document') // this is a workaround for TS https://github.com/DefinitelyTyped/DefinitelyTyped/issues/41179 + expect(Object.keys(extracted[0].attributes)).toStrictEqual([ + '_id', + '_type', + '_createdAt', + '_updatedAt', + '_rev', + 'title', + 'list', + 'number', + 'someInlinedObject', + 'blocks', + 'other', + 'others', + ]) + + // Check that the block type is extracted correctly, as an array + expect(extracted[0].attributes.blocks.type).toEqual('objectAttribute') + expect(extracted[0].attributes.blocks.value.type).toEqual('array') + assert(extracted[0].attributes.blocks.value.type === 'array') // this is a workaround for TS + expect(extracted[0].attributes.blocks.value.of.type).toEqual('object') + assert(extracted[0].attributes.blocks.value.of.type === 'object') // this is a workaround for TS + expect(Object.keys(extracted[0].attributes.blocks.value.of.attributes)).toStrictEqual([ + '_key', + 'level', + 'style', + 'listItem', + 'children', + 'markDefs', + ]) + + expect(extracted).toMatchSnapshot() + }) + + test('Can extract example studio', async () => { + const schemaDef = await import('../../../schema/example/schema-def') + const extracted = extractSchema(schemaDef.default.types) + expect(extracted.length).toBeGreaterThan(0) // we don't really care about the exact number, just that it passes :+1: + }) +}) diff --git a/packages/@sanity/schema/test/extractSchema/fixtures/block.ts b/packages/@sanity/schema/test/extractSchema/fixtures/block.ts new file mode 100644 index 000000000000..3b9a097cdc7d --- /dev/null +++ b/packages/@sanity/schema/test/extractSchema/fixtures/block.ts @@ -0,0 +1,514 @@ +import {ComposeIcon, DropIcon, ImageIcon} from '@sanity/icons' + +const linkType = { + type: 'object', + name: 'link', + fields: [ + { + type: 'string', + name: 'href', + validation: (Rule) => Rule.uri({scheme: ['http', 'https']}).required(), + }, + ], + options: { + modal: { + type: 'popover', + width: 2, + }, + }, +} + +export default { + name: 'blocks', + title: 'Blocks test', + type: 'object', + icon: ComposeIcon, + fields: [ + { + name: 'title', + title: 'Title', + type: 'string', + }, + { + name: 'first', + title: 'Block array as first field', + type: 'array', + of: [ + { + type: 'block', + marks: { + annotations: [linkType], + }, + }, + ], + }, + { + name: 'defaults', + title: 'Content', + description: 'Profound description of what belongs here', + type: 'array', + of: [ + {type: 'image', title: 'Image', icon: ImageIcon}, + { + type: 'reference', + name: 'authorReference', + to: {type: 'author'}, + title: 'Reference to author', + }, + { + type: 'reference', + name: 'bookReference', + to: {type: 'book'}, + title: 'Reference to book', + }, + { + type: 'object', + name: 'objectWithNestedArray', + title: 'An object with nested array', + fields: [ + { + name: 'title', + type: 'string', + }, + { + name: 'array', + type: 'array', + of: [ + { + type: 'object', + fields: [ + {type: 'string', name: 'title'}, + {type: 'reference', name: 'author', to: [{type: 'author'}]}, + ], + }, + ], + }, + ], + }, + {type: 'author', title: 'Embedded author'}, + {type: 'code', title: 'Code'}, + // { + // type: 'color', + // name: 'colorBlock', + // title: 'Color (block)', + // icon: DropIcon, + // }, + { + type: 'object', + title: 'Test object', + name: 'testObject', + fields: [{name: 'field1', type: 'string'}], + }, + { + type: 'object', + title: 'Other test object', + name: 'otherTestObject', + fields: [ + {name: 'field1', type: 'string'}, + { + name: 'field3', + type: 'array', + of: [ + { + type: 'object', + fields: [ + {name: 'aString', type: 'string'}, + {name: 'aNumber', type: 'number'}, + ], + }, + ], + }, + ], + }, + // { + // type: 'block', + // of: [ + // { + // type: 'color', + // title: 'Color', + // }, + // ], + // }, + { + type: 'spotifyEmbed', + name: 'spotifyEmbed', + title: 'Spotify embed', + }, + ], + }, + { + name: 'nestedWithDualColumnCTA', + title: 'Nested, with dual column CTA', + type: 'array', + of: [ + { + name: 'localeRichtext', + type: 'object', + fields: [ + { + title: 'English', + name: 'en', + type: 'array', + of: [ + {type: 'block'}, + {type: 'image'}, + { + name: 'twoColCTA', + type: 'object', + title: 'Two Column CTA', + description: 'Inserts two content blocks.', + fields: [ + { + name: 'columnone', + title: 'Column One', + type: 'array', + of: [{type: 'richTextObject'}], + }, + { + name: 'columntwo', + title: 'Column Two', + type: 'array', + of: [{type: 'richTextObject'}], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + name: 'readOnlyWithDefaults', + title: 'Read only with defaults', + description: 'This is read only', + type: 'array', + readOnly: true, + of: [ + {type: 'image', title: 'Image'}, + { + type: 'reference', + name: 'authorReference', + to: {type: 'author'}, + title: 'Reference to author', + }, + { + type: 'reference', + name: 'bookReference', + to: {type: 'book'}, + title: 'Reference to book', + }, + {type: 'author', title: 'Embedded author'}, + {type: 'code', title: 'Code'}, + // { + // type: 'color', + // name: 'colorBlock', + // title: 'Color (block)', + // }, + { + type: 'object', + title: 'Test object', + name: 'testObject', + fields: [{name: 'field1', type: 'string'}], + }, + { + type: 'object', + title: 'Other test object', + name: 'otherTestObject', + fields: [ + {name: 'field1', type: 'string'}, + { + name: 'field3', + type: 'array', + of: [ + { + type: 'object', + fields: [ + {name: 'aString', type: 'string'}, + {name: 'aNumber', type: 'number'}, + ], + }, + ], + }, + ], + }, + // { + // type: 'block', + // of: [ + // { + // type: 'color', + // title: 'Color', + // }, + // ], + // }, + ], + }, + { + name: 'minimal', + title: 'Reset all options', + type: 'array', + of: [ + { + type: 'block', + styles: [], + lists: [], + marks: { + decorators: [], + annotations: [], + }, + }, + ], + }, + { + name: 'reproCH9436', + title: 'Images', + type: 'array', + description: 'Repro case for https://app.clubhouse.io/sanity-io/story/9436/', + of: [ + {type: 'block'}, + { + name: 'imageWithPortableTextCaption', + type: 'image', + fields: [ + { + name: 'caption', + title: 'Caption', + type: 'array', + description: + 'The amount of toolbar buttons here should not affect the width of the PTE input or the width of the dialog which contains it', + options: {isHighlighted: true}, + of: [ + { + title: 'Block', + type: 'block', + marks: { + decorators: [ + // the number of decorators here will currently force the width of the PTE input + // to be wider than the dialog, which again makes the dialog content overflow + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'}, + {title: 'Underline', value: 'underline'}, + {title: 'Strikethrough', value: 'strikethrough'}, + {title: 'Superscript', value: 'superscript'}, + {title: 'Subscript', value: 'subscript'}, + {title: 'Left', value: 'alignleft'}, + {title: 'Center', value: 'aligncenter'}, + {title: 'Right', value: 'alignright'}, + {title: 'Justify', value: 'alignjustify'}, + ], + }, + }, + ], + }, + ], + }, + ], + }, + { + name: 'customized', + title: 'Customized with block types', + type: 'array', + of: [ + {type: 'author', title: 'Author'}, + { + type: 'block', + styles: [ + {title: 'Normal', value: 'normal'}, + {title: 'H1', value: 'h1'}, + {title: 'H2', value: 'h2'}, + {title: 'Quote', value: 'blockquote'}, + ], + lists: [ + {title: 'Bullet', value: 'bullet'}, + {title: 'Numbered', value: 'number'}, + ], + marks: { + decorators: [ + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'}, + {title: 'Decorator with custom icon', value: 'color', icon: DropIcon}, + ], + annotations: [ + { + name: 'Author', + title: 'Author', + type: 'reference', + to: {type: 'author'}, + }, + { + title: 'Annotation with custom icon', + name: 'test', + type: 'object', + icon: DropIcon, + fields: [ + { + name: 'testString', + type: 'string', + }, + ], + }, + ], + }, + of: [ + { + type: 'image', + title: 'Image', + fields: [ + {title: 'Caption', name: 'caption', type: 'string', options: {isHighlighted: true}}, + { + title: 'Authors', + name: 'authors', + type: 'array', + options: {isHighlighted: true}, + of: [{type: 'author', title: 'Author'}], + }, + ], + }, + ], + }, + ], + }, + { + name: 'withGeopoint', + title: 'With geopoints', + type: 'array', + of: [{type: 'block'}, {type: 'geopoint'}], + }, + { + name: 'deep', + title: 'Blocks deep down', + type: 'object', + fields: [ + {name: 'something', title: 'Something', type: 'string'}, + { + name: 'blocks', + type: 'array', + title: 'Blocks', + of: [ + {type: 'image', title: 'Image'}, + {type: 'author', title: 'Author'}, + { + type: 'block', + styles: [ + {title: 'Normal', value: 'normal'}, + {title: 'H1', value: 'h1'}, + {title: 'H2', value: 'h2'}, + {title: 'Quote', value: 'blockquote'}, + ], + lists: [ + {title: 'Bullet', value: 'bullet'}, + {title: 'Numbered', value: 'number'}, + ], + marks: { + decorators: [ + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'}, + ], + annotations: [ + {name: 'Author', title: 'Author', type: 'reference', to: {type: 'author'}}, + ], + }, + }, + ], + }, + ], + }, + { + title: 'Array of articles', + name: 'arrayOfArticles', + type: 'array', + of: [ + { + type: 'blocksTest', + }, + ], + }, + { + title: 'Block in block', + name: 'blockInBlock', + type: 'array', + of: [ + { + type: 'block', + of: [ + { + name: 'footnote', + title: 'Footnote', + type: 'object', + fields: [ + { + title: 'Footnote', + name: 'footnote', + type: 'array', + of: [ + { + type: 'block', + lists: [], + styles: [], + marks: { + decorators: [ + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'}, + ], + annotations: [], + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + name: 'recursive', + type: 'object', + fields: [ + { + name: 'blocks', + type: 'array', + title: 'Blocks', + of: [ + { + type: 'block', + styles: [], + lists: [], + marks: { + decorators: [], + annotations: [], + }, + }, + { + type: 'blocksTest', + title: 'Blocks test!', + }, + ], + }, + ], + }, + { + name: 'blockList', + title: 'Array of blocks', + type: 'array', + of: [ + { + name: 'blockListEntry', + type: 'object', + fields: [ + { + name: 'title', + title: 'Title', + type: 'string', + }, + { + name: 'blocks', + type: 'array', + of: [{type: 'block'}], + }, + ], + }, + ], + }, + ], +} diff --git a/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts new file mode 100644 index 000000000000..0f97012f7b02 --- /dev/null +++ b/packages/sanity/src/_internal/cli/actions/schema/extractAction.ts @@ -0,0 +1,71 @@ +import {writeFile} from 'node:fs/promises' +import {dirname, join} from 'node:path' +import {Worker} from 'node:worker_threads' + +import {type CliCommandArguments, type CliCommandContext} from '@sanity/cli' +import readPkgUp from 'read-pkg-up' + +import { + type ExtractSchemaWorkerData, + type ExtractSchemaWorkerResult, +} from '../../threads/extractSchema' + +interface ExtractFlags { + workspace?: string + path?: string + 'enforce-required-fields'?: boolean +} + +export type SchemaValidationFormatter = (result: ExtractSchemaWorkerResult) => string + +export default async function extractAction( + args: CliCommandArguments, + {workDir, output}: CliCommandContext, +): Promise { + const flags = args.extOptions + + const rootPkgPath = readPkgUp.sync({cwd: __dirname})?.path + if (!rootPkgPath) { + throw new Error('Could not find root directory for `sanity` package') + } + + const workerPath = join( + dirname(rootPkgPath), + 'lib', + '_internal', + 'cli', + 'threads', + 'extractSchema.js', + ) + + const spinner = output + .spinner({}) + .start( + flags['enforce-required-fields'] + ? 'Extracting schema, with enforced required fields' + : 'Extracting schema', + ) + + const worker = new Worker(workerPath, { + workerData: { + workDir, + workspaceName: flags.workspace, + enforceRequiredFields: flags['enforce-required-fields'], + } satisfies ExtractSchemaWorkerData, + // eslint-disable-next-line no-process-env + env: process.env, + }) + + const {schema} = await new Promise((resolve, reject) => { + worker.addListener('message', resolve) + worker.addListener('error', reject) + }) + + const path = flags.path || join(process.cwd(), 'schema.json') + + spinner.text = `Writing schema to ${path}` + + await writeFile(path, JSON.stringify(schema, null, 2)) + + spinner.succeed('Extracted schema') +} diff --git a/packages/sanity/src/_internal/cli/commands/index.ts b/packages/sanity/src/_internal/cli/commands/index.ts index 37463949ef93..e27daee4cce2 100644 --- a/packages/sanity/src/_internal/cli/commands/index.ts +++ b/packages/sanity/src/_internal/cli/commands/index.ts @@ -46,6 +46,7 @@ import listMigrationsCommand from './migration/listMigrationsCommand' import migrationGroup from './migration/migrationGroup' import runMigrationCommand from './migration/runMigrationCommand' import previewCommand from './preview/previewCommand' +import extractSchemaCommand from './schema/extractSchemaCommand' import schemaGroup from './schema/schemaGroup' import validateSchemaCommand from './schema/validateSchemaCommand' import startCommand from './start/startCommand' @@ -105,6 +106,7 @@ const commands: (CliCommandDefinition | CliCommandGroupDefinition)[] = [ startCommand, schemaGroup, validateSchemaCommand, + extractSchemaCommand, previewCommand, uninstallCommand, execCommand, diff --git a/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts new file mode 100644 index 000000000000..46694e7d74b4 --- /dev/null +++ b/packages/sanity/src/_internal/cli/commands/schema/extractSchemaCommand.ts @@ -0,0 +1,32 @@ +import {type CliCommandDefinition} from '@sanity/cli' + +const description = 'Extracts a JSON representation of a Sanity schema within a Studio context.' + +const helpText = ` +**Note**: This command is experimental and subject to change. + +Options + --workspace The name of the workspace to generate a schema for + --path Optional path to specify destination of the schema file + --enforce-required-fields Makes the schema generated treat fields marked as required as non-optional. Defaults to false. + +Examples + # Extracts schema types in a Sanity project with more than one workspace + sanity schema extract --workspace default +` + +const extractSchemaCommand: CliCommandDefinition = { + name: 'extract', + group: 'schema', + signature: '', + description, + helpText, + hideFromHelp: true, + action: async (args, context) => { + const mod = await import('../../actions/schema/extractAction') + + return mod.default(args, context) + }, +} satisfies CliCommandDefinition + +export default extractSchemaCommand diff --git a/packages/sanity/src/_internal/cli/threads/extractSchema.ts b/packages/sanity/src/_internal/cli/threads/extractSchema.ts new file mode 100644 index 000000000000..2bc5b2fcc1dc --- /dev/null +++ b/packages/sanity/src/_internal/cli/threads/extractSchema.ts @@ -0,0 +1,73 @@ +import {isMainThread, parentPort, workerData as _workerData} from 'node:worker_threads' + +import {extractSchema} from '@sanity/schema/_internal' +import {type Workspace} from 'sanity' + +import {getStudioWorkspaces} from '../util/getStudioWorkspaces' +import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment' + +export interface ExtractSchemaWorkerData { + workDir: string + workspaceName?: string + enforceRequiredFields?: boolean +} + +export interface ExtractSchemaWorkerResult { + schema: ReturnType +} + +if (isMainThread || !parentPort) { + throw new Error('This module must be run as a worker thread') +} + +const opts = _workerData as ExtractSchemaWorkerData +const cleanup = mockBrowserEnvironment(opts.workDir) + +async function main() { + try { + const workspaces = await getStudioWorkspaces({basePath: opts.workDir}) + + const workspace = getWorkspace({workspaces, workspaceName: opts.workspaceName}) + + const {types} = workspace.schema._original || {types: []} + + const schema = extractSchema(types, { + enforceRequiredFields: opts.enforceRequiredFields, + }) + + parentPort?.postMessage({ + schema, + } satisfies ExtractSchemaWorkerResult) + } finally { + cleanup() + } +} + +main() + +function getWorkspace({ + workspaces, + workspaceName, +}: { + workspaces: Workspace[] + workspaceName?: string +}): Workspace { + if (workspaces.length === 0) { + throw new Error('No studio configuration found') + } + + if (workspaces.length === 1) { + return workspaces[0] + } + + if (workspaceName === undefined) { + throw new Error( + `Multiple workspaces found. Please specify which workspace to use with '--workspace'.`, + ) + } + const workspace = workspaces.find((w) => w.name === workspaceName) + if (!workspace) { + throw new Error(`Could not find workspace "${workspaceName}"`) + } + return workspace +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 633ed17282d6..f7fbe96006db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1172,6 +1172,9 @@ importers: arrify: specifier: ^1.0.1 version: 1.0.1 + groq-js: + specifier: 1.5.0-canary.1 + version: 1.5.0-canary.1 humanize-list: specifier: ^1.0.1 version: 1.0.1 @@ -1188,6 +1191,9 @@ importers: '@jest/globals': specifier: ^29.7.0 version: 29.7.0 + '@sanity/icons': + specifier: ^2.8.0 + version: 2.10.3(react@18.2.0) rimraf: specifier: ^3.0.2 version: 3.0.2 @@ -11489,6 +11495,15 @@ packages: resolution: {integrity: sha512-h2vFXJ/U5VX9bzlqqZLgx/XS0ibNJza4eMxUjZTqkpe3gKafFIJSkMP0RS/XEQR18gJMjXAJNziWdY7JYYfl7w==} engines: {node: '>= 14'} + /groq-js@1.5.0-canary.1: + resolution: {integrity: sha512-p3eqvL0mYS9bzCgpQT4IGs32MCDyyWOU7ilpr7UR4k7AedXYNtd/ha9UpszP6i2VrAXCfmJ63zvvTut6JCKgSQ==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: false + /groq@3.31.0: resolution: {integrity: sha512-GKJtR3ICQJCt2V135g70HLtoizADsuP7ikb1s1CF8RS1O8uPXbSYxtMcqqWpMDSAxyxD6YA86TfV7TLf1t1cPQ==} engines: {node: '>=18'}