├── test ├── .eslintrc ├── fixtures │ ├── 01-simple.js │ ├── 19-auto-slug.js │ ├── 03-multi-type.js │ ├── 18-manual-slug.js │ ├── 04-reference.js │ ├── 05-reference-array.js │ ├── 08-inline-document.js │ ├── 06-reference-array-non-nullable.js │ ├── 09-inline-document-array.js │ ├── 07-directives.js │ ├── 16-error-inline-union.js │ ├── 14-reference-and-inline-array.js │ ├── 15-forced-inline-array.js │ ├── 17-error-non-ref-union-field.js │ ├── 02-basics.js │ ├── 11-union-array.js │ ├── 10-unions.js │ ├── 12-union-of-unions.js │ ├── 13-union-of-union-array.js │ └── 99-full.js ├── test.js └── __snapshots__ │ └── test.js.snap ├── .prettierrc ├── src ├── index.js ├── withoutUndefined.js ├── taggedNoop.js ├── stubExternalTypes.js ├── schemaError.js ├── fromGQL.js ├── coreSchema.js └── compileSchema.js ├── demo ├── .eslintrc ├── index.html ├── demo.css ├── GithubBadge.js ├── css │ ├── material.css │ └── codemirror.css └── demo.js ├── .babelrc ├── .eslintrc ├── .gitignore ├── package.json └── README.md /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const graphql = require('./taggedNoop') 2 | const fromGQL = require('./fromGQL') 3 | 4 | module.exports = { 5 | graphql, 6 | fromGQL 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/01-simple.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author { 5 | name: String 6 | } 7 | ` 8 | -------------------------------------------------------------------------------- /demo/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": true 5 | }, 6 | "parser": "babel-eslint", 7 | "extends": ["sanity", "sanity/react", "prettier", "prettier/react"] 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/19-auto-slug.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author { 5 | num: Float 6 | name: String! 7 | slug: Slug 8 | } 9 | ` 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "edge": "14" 8 | } 9 | } 10 | ], 11 | "@babel/preset-react" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["sanity", "prettier"], 3 | "env": {"node": true, "browser": true}, 4 | "parserOptions": { 5 | "ecmaVersion": 2018 6 | }, 7 | "rules": { 8 | "complexity": ["warn", 20] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/withoutUndefined.js: -------------------------------------------------------------------------------- 1 | module.exports = function withoutUndefined(obj) { 2 | return Object.keys(obj).reduce( 3 | (acc, key) => (typeof obj[key] === 'undefined' ? acc : {...(acc || {}), [key]: obj[key]}), 4 | undefined 5 | ) 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/03-multi-type.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author { 5 | name: String! 6 | } 7 | 8 | type Book implements Document { 9 | title: String 10 | } 11 | ` 12 | -------------------------------------------------------------------------------- /test/fixtures/18-manual-slug.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author { 5 | name: String! 6 | description: String 7 | slug: Slug @slug(source: "description") 8 | } 9 | ` 10 | -------------------------------------------------------------------------------- /src/taggedNoop.js: -------------------------------------------------------------------------------- 1 | module.exports = function taggedTemplateNoop(strings, ...keys) { 2 | const lastIndex = strings.length - 1 3 | return ( 4 | strings.slice(0, lastIndex).reduce((acc, slice, i) => acc + slice + keys[i], '') + 5 | strings[lastIndex] 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/04-reference.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author implements Document { 5 | name: String! 6 | } 7 | 8 | type Book implements Document { 9 | title: String 10 | author: Author! 11 | } 12 | ` 13 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sanity-graphql-schema demo 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/05-reference-array.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author implements Document { 5 | name: String! 6 | } 7 | 8 | type Book implements Document { 9 | title: String 10 | authors: [Author!] 11 | } 12 | ` 13 | -------------------------------------------------------------------------------- /test/fixtures/08-inline-document.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author implements Document { 5 | name: String! 6 | } 7 | 8 | type Book implements Document { 9 | title: String 10 | author: Author! @inline 11 | } 12 | ` 13 | -------------------------------------------------------------------------------- /test/fixtures/06-reference-array-non-nullable.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author implements Document { 5 | name: String! 6 | } 7 | 8 | type Book implements Document { 9 | title: String 10 | authors: [Author!]! 11 | } 12 | ` 13 | -------------------------------------------------------------------------------- /test/fixtures/09-inline-document-array.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author implements Document { 5 | name: String! 6 | } 7 | 8 | type Book implements Document { 9 | title: String 10 | authors: [Author!] @inline 11 | } 12 | ` 13 | -------------------------------------------------------------------------------- /src/stubExternalTypes.js: -------------------------------------------------------------------------------- 1 | const {upperFirst} = require('lodash') 2 | 3 | module.exports = function stubExternalTypes(types) { 4 | return types.reduce((sdl, type) => { 5 | return `${sdl} 6 | # Stub to make external types work 7 | scalar ${upperFirst(type.name)} # ${type.name}\n\n`.replace(/(\n+)\s*/g, '$1') 8 | }, '') 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/07-directives.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author implements Document { 5 | name: String! 6 | birthDate: Date @hidden 7 | } 8 | 9 | type Book implements Document { 10 | title: String 11 | author: Author 12 | tags: [String!]! @display(layout: "tags") 13 | } 14 | ` 15 | -------------------------------------------------------------------------------- /src/schemaError.js: -------------------------------------------------------------------------------- 1 | const withoutUndefined = require('./withoutUndefined') 2 | 3 | module.exports = function schemaError(message, name = '__root__', base = {}) { 4 | return withoutUndefined({ 5 | name, 6 | title: 'GQL schema error', 7 | type: 'string', 8 | ...base, 9 | _problems: [ 10 | { 11 | severity: 'error', 12 | message 13 | } 14 | ] 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/16-error-inline-union.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author { 5 | name: String! 6 | } 7 | 8 | type ExternalImage { 9 | caption: String 10 | url: Url 11 | } 12 | 13 | union Description = Author | ExternalImage 14 | 15 | type Book implements Document { 16 | title: String 17 | author: Author! 18 | description: Description 19 | } 20 | ` 21 | -------------------------------------------------------------------------------- /test/fixtures/14-reference-and-inline-array.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author implements Document { 5 | name: String! 6 | } 7 | 8 | type ExternalImage { 9 | caption: String 10 | url: Url 11 | } 12 | 13 | union Description = Author | ExternalImage 14 | 15 | type Book implements Document { 16 | title: String 17 | author: Author! 18 | description: [Description] 19 | } 20 | ` 21 | -------------------------------------------------------------------------------- /test/fixtures/15-forced-inline-array.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author implements Document { 5 | name: String! 6 | } 7 | 8 | type ExternalImage { 9 | caption: String 10 | url: Url 11 | } 12 | 13 | union Description = Author | ExternalImage 14 | 15 | type Book implements Document { 16 | title: String 17 | author: Author! 18 | description: [Description] @inline 19 | } 20 | ` 21 | -------------------------------------------------------------------------------- /test/fixtures/17-error-non-ref-union-field.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author implements Document { 5 | name: String! 6 | } 7 | 8 | type ExternalImage { 9 | caption: String 10 | url: Url 11 | } 12 | 13 | union Description = Author | ExternalImage 14 | 15 | type Book implements Document { 16 | title: String 17 | author: Author! 18 | description: Description 19 | } 20 | ` 21 | -------------------------------------------------------------------------------- /test/fixtures/02-basics.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Author { 5 | # GraphQL 6 | int: Int 7 | float: Float 8 | string: String 9 | bool: Boolean 10 | id: ID 11 | 12 | # Sanity 13 | number: Number 14 | email: Email 15 | text: Text 16 | date: Date 17 | dateTime: Datetime 18 | url: Url 19 | 20 | # Lists 21 | list: [String] 22 | textList: [Text] 23 | } 24 | ` 25 | -------------------------------------------------------------------------------- /test/fixtures/11-union-array.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Reviewer implements Document { 5 | name: String! 6 | joined: Date 7 | } 8 | 9 | type Author implements Document { 10 | name: String! 11 | firstPublished: Date 12 | } 13 | 14 | union User = Reviewer | Author 15 | 16 | type Book implements Document { 17 | title: String 18 | author: Author 19 | reviewers: [User!]! 20 | } 21 | ` 22 | -------------------------------------------------------------------------------- /test/fixtures/10-unions.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | type Reviewer implements Document { 5 | name: String! 6 | joined: Date 7 | } 8 | 9 | type Author implements Document { 10 | name: String! 11 | firstPublished: Date 12 | } 13 | 14 | union User = Reviewer | Author 15 | 16 | type Book implements Document { 17 | title: String 18 | author: Author 19 | reviewer: User 20 | descisionMaker: User! 21 | } 22 | ` 23 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | @import './css/codemirror.css'; 2 | @import './css/material.css'; 3 | 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | html, 9 | body { 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | #demo { 15 | background: #263238; 16 | } 17 | 18 | #demo { 19 | width: 100vw; 20 | height: 100vh; 21 | } 22 | 23 | .react-codemirror2 { 24 | width: 49.5vw; 25 | height: 100vh; 26 | display: inline-block; 27 | } 28 | 29 | .react-codemirror2:last-child { 30 | border-left: 1px solid #ccc; 31 | } 32 | 33 | .CodeMirror { 34 | margin: 0.5em 1em; 35 | font-size: 1.2em; 36 | height: 100vh; 37 | } 38 | -------------------------------------------------------------------------------- /test/fixtures/12-union-of-unions.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | union User = Reviewer | Author 5 | union Work = Book | Magazine 6 | union Entity = User | Work 7 | 8 | type Reviewer implements Document { 9 | name: String! 10 | joined: Date 11 | } 12 | 13 | type Author implements Document { 14 | name: String! 15 | firstPublished: Date 16 | } 17 | 18 | type Book implements Document { 19 | title: String 20 | author: Author 21 | reviewer: User 22 | } 23 | 24 | type Magazine implements Document { 25 | title: String 26 | attachedTo: Entity 27 | } 28 | ` 29 | -------------------------------------------------------------------------------- /test/fixtures/13-union-of-union-array.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | union User = Reviewer | Author 5 | union Work = Book | Magazine 6 | union Entity = User | Work 7 | 8 | type Reviewer implements Document { 9 | name: String! 10 | joined: Date 11 | } 12 | 13 | type Author implements Document { 14 | name: String! 15 | firstPublished: Date 16 | } 17 | 18 | type Book implements Document { 19 | title: String 20 | author: Author 21 | reviewer: User 22 | } 23 | 24 | type Magazine implements Document { 25 | title: String 26 | attachedTo: [Entity] 27 | } 28 | ` 29 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-sync */ 2 | const fs = require('fs') 3 | const path = require('path') 4 | const fromGql = require('../src/fromGQL') 5 | 6 | const defaultOptions = {externalTypes: [], throwOnError: true} 7 | const fixturesDir = path.join(__dirname, 'fixtures') 8 | const fixture = (sdl, opts = defaultOptions) => fromGql(sdl, opts) 9 | 10 | const files = fs.readdirSync(fixturesDir) 11 | files 12 | .filter(file => /^\d+/.test(file) && file.endsWith('.js')) 13 | .map(file => ({name: file.slice(0, -3), path: path.join(fixturesDir, file)})) 14 | .map(file => ({...file, content: require(file.path)})) 15 | .forEach(file => test(file.name, () => expect(fixture(file.content)).toMatchSnapshot())) 16 | -------------------------------------------------------------------------------- /src/fromGQL.js: -------------------------------------------------------------------------------- 1 | const compileSchema = require('./compileSchema') 2 | 3 | const placeholderSdl = 'type NoTypesDefined implements Document {pleaseFix: String}' 4 | const getPluginTypes = () => require('all:part:@sanity/base/schema-type') 5 | 6 | module.exports = function fromGQL(sdl, opts = {}) { 7 | const externalTypes = opts.externalTypes || getPluginTypes() 8 | const throwOnError = opts.throwOnError || false 9 | const schemaSdl = `${sdl}`.trim() || placeholderSdl 10 | 11 | try { 12 | return compileSchema(schemaSdl, {externalTypes}) 13 | } catch (err) { 14 | if (throwOnError) { 15 | throw err 16 | } 17 | 18 | // eslint-disable-next-line no-console 19 | console.error(err) 20 | return [ 21 | { 22 | name: 'gqlSchemaErrorCheckConsoleForDetails', 23 | title: 'GQL schema error - check console for details', 24 | type: 'object', 25 | fields: [] 26 | } 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # macOS finder cache file 40 | .DS_Store 41 | 42 | # VS Code settings 43 | .vscode 44 | 45 | # Lockfiles 46 | yarn.lock 47 | package-lock.json 48 | 49 | # Cache 50 | .cache 51 | 52 | # build ouput 53 | dist 54 | lib 55 | -------------------------------------------------------------------------------- /demo/GithubBadge.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default () => ( 4 | 8 | 15 | 16 | 21 | 25 | 26 | 27 | ) 28 | -------------------------------------------------------------------------------- /test/fixtures/99-full.js: -------------------------------------------------------------------------------- 1 | const graphql = require('../../src/taggedNoop') 2 | 3 | module.exports = graphql` 4 | """ 5 | A blog post 6 | 7 | It can contain newlines 8 | """ 9 | type Post implements Document @display(title: "Blog post", icon: "post") { 10 | title: String 11 | leadImage: CaptionedImage @hotspot 12 | author: Author! 13 | body: Text 14 | tags: [String] @display(layout: "tags") 15 | status: String @enum(values: [{name: "active"}, {name: "disabled"}], layout: "radio") 16 | views: Int @readOnly @hidden 17 | """ 18 | Comments are sent from the frontend 19 | """ 20 | comments: [Comment] @display(title: "User comments") 21 | } 22 | 23 | type Author implements Document 24 | @orderings( 25 | from: [ 26 | { 27 | title: "Release Date" 28 | name: "releaseDateDesc" 29 | by: [{field: "releaseDate.utc", direction: "desc"}] 30 | } 31 | ] 32 | ) { 33 | name: String 34 | profileImage: Image 35 | } 36 | 37 | type User implements Document { 38 | name: String 39 | } 40 | 41 | type CaptionedImage implements Image { 42 | caption: String 43 | uploadedBy: Author @inline 44 | } 45 | 46 | type Comment implements Document @fieldsets(from: [{title: "Statistics", name: "stats"}]) { 47 | """ 48 | Author of this comment 49 | """ 50 | author: Author! 51 | text: String 52 | likes: Int @fieldset(set: "stats") 53 | likedBy: [User] 54 | } 55 | 56 | union Person = Author | User 57 | ` 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-graphql-schema", 3 | "version": "0.0.6", 4 | "description": "Declare a Sanity schema using GraphQL SDL syntax", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "babel -d lib src", 9 | "demo": "parcel demo/index.html", 10 | "demo:build": "NODE_ENV=production parcel build --experimental-scope-hoisting demo/index.html --public-url='./'", 11 | "posttest": "eslint .", 12 | "prepublishOnly": "npm run build", 13 | "postpublish": "npm run demo:build && gh-pages -d dist" 14 | }, 15 | "devDependencies": { 16 | "@babel/cli": "^7.2.3", 17 | "@babel/core": "^7.2.2", 18 | "@babel/preset-env": "^7.2.3", 19 | "@babel/preset-react": "^7.0.0", 20 | "babel-core": "7.0.0-bridge.0", 21 | "babel-eslint": "^10.0.1", 22 | "babel-jest": "^23.6.0", 23 | "codemirror": "^5.42.2", 24 | "codemirror-graphql": "^0.8.3", 25 | "eslint": "^5.9.0", 26 | "eslint-config-prettier": "^3.3.0", 27 | "eslint-config-sanity": "^0.136.0", 28 | "gh-pages": "^2.0.1", 29 | "jest": "^23.6.0", 30 | "json5": "^2.1.0", 31 | "parcel-bundler": "^1.11.0", 32 | "parcel-plugin-bundle-visualiser": "^1.2.0", 33 | "prettier": "^1.15.2", 34 | "react": "^16.7.0", 35 | "react-codemirror2": "^5.1.0", 36 | "react-dom": "^16.7.0" 37 | }, 38 | "dependencies": { 39 | "graphql": "^14.0.2", 40 | "lodash": "^4.17.11", 41 | "oneline": "^1.0.0" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "git+ssh://git@github.com/rexxars/sanity-graphql-schema.git" 46 | }, 47 | "keywords": [ 48 | "sanity", 49 | "graphql", 50 | "sdl", 51 | "schema" 52 | ], 53 | "author": "Espen Hovlandsdal ", 54 | "license": "MIT", 55 | "bugs": { 56 | "url": "https://github.com/rexxars/sanity-graphql-schema/issues" 57 | }, 58 | "homepage": "https://github.com/rexxars/sanity-graphql-schema#readme" 59 | } 60 | -------------------------------------------------------------------------------- /demo/css/material.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Name: material 4 | Author: Michael Kaminsky (http://github.com/mkaminsky11) 5 | 6 | Original material color scheme by Mattia Astorino (https://github.com/equinusocio/material-theme) 7 | 8 | */ 9 | 10 | .cm-s-material.CodeMirror { 11 | background-color: #263238; 12 | color: rgba(233, 237, 237, 1); 13 | } 14 | .cm-s-material .CodeMirror-gutters { 15 | background: #263238; 16 | color: rgb(83,127,126); 17 | border: none; 18 | } 19 | .cm-s-material .CodeMirror-guttermarker, .cm-s-material .CodeMirror-guttermarker-subtle, .cm-s-material .CodeMirror-linenumber { color: rgb(83,127,126); } 20 | .cm-s-material .CodeMirror-cursor { border-left: 1px solid #f8f8f0; } 21 | .cm-s-material div.CodeMirror-selected { background: rgba(255, 255, 255, 0.15); } 22 | .cm-s-material.CodeMirror-focused div.CodeMirror-selected { background: rgba(255, 255, 255, 0.10); } 23 | .cm-s-material .CodeMirror-line::selection, .cm-s-material .CodeMirror-line > span::selection, .cm-s-material .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); } 24 | .cm-s-material .CodeMirror-line::-moz-selection, .cm-s-material .CodeMirror-line > span::-moz-selection, .cm-s-material .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); } 25 | 26 | .cm-s-material .CodeMirror-activeline-background { background: rgba(0, 0, 0, 0); } 27 | .cm-s-material .cm-keyword { color: rgba(199, 146, 234, 1); } 28 | .cm-s-material .cm-operator { color: rgba(233, 237, 237, 1); } 29 | .cm-s-material .cm-variable-2 { color: #80CBC4; } 30 | .cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: #82B1FF; } 31 | .cm-s-material .cm-builtin { color: #DECB6B; } 32 | .cm-s-material .cm-atom { color: #F77669; } 33 | .cm-s-material .cm-number { color: #F77669; } 34 | .cm-s-material .cm-def { color: rgba(233, 237, 237, 1); } 35 | .cm-s-material .cm-string { color: #C3E88D; } 36 | .cm-s-material .cm-string-2 { color: #80CBC4; } 37 | .cm-s-material .cm-comment { color: #546E7A; } 38 | .cm-s-material .cm-variable { color: #82B1FF; } 39 | .cm-s-material .cm-tag { color: #80CBC4; } 40 | .cm-s-material .cm-meta { color: #80CBC4; } 41 | .cm-s-material .cm-attribute { color: #FFCB6B; } 42 | .cm-s-material .cm-property { color: #80CBAE; } 43 | .cm-s-material .cm-qualifier { color: #DECB6B; } 44 | .cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: #DECB6B; } 45 | .cm-s-material .cm-tag { color: rgba(255, 83, 112, 1); } 46 | .cm-s-material .cm-error { 47 | color: rgba(255, 255, 255, 1.0); 48 | background-color: #EC5F67; 49 | } 50 | .cm-s-material .CodeMirror-matchingbracket { 51 | text-decoration: underline; 52 | color: white !important; 53 | } 54 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import {debounce} from 'lodash' 4 | import json5 from 'json5' 5 | import { 6 | UnControlled as UncontrolledCodeMirror, 7 | Controlled as ControlledCodeMirror 8 | } from 'react-codemirror2' 9 | import compileSchema from '../src/compileSchema' 10 | import GithubBadge from './GithubBadge' 11 | import 'codemirror-graphql/mode' 12 | import 'codemirror/mode/javascript/javascript' 13 | 14 | const defaultSdl = ` 15 | union BlockContent = Block | Image 16 | 17 | type Post { 18 | title: String! 19 | slug: Slug 20 | author: Author 21 | mainImage: Image @hotspot 22 | categories: [Category] 23 | publishedAt: Datetime 24 | body: [BlockContent] 25 | } 26 | 27 | type Category { 28 | title: String! 29 | description: Text 30 | } 31 | 32 | type Author { 33 | name: String! 34 | slug: Slug 35 | image: Image @hotspot 36 | bio: [Block] 37 | } 38 | ` 39 | 40 | const localStorageKey = 'gqlSchemaSdl' 41 | 42 | function getInitialValue() { 43 | try { 44 | const saved = window && window.localStorage && window.localStorage.getItem(localStorageKey) 45 | return saved || defaultSdl 46 | } catch (err) { 47 | return defaultSdl 48 | } 49 | } 50 | 51 | function getCompiled(value) { 52 | let schema 53 | try { 54 | const compiled = compileSchema(value, {externalTypes: []}) 55 | schema = json5.stringify(compiled, {space: 2, quote: "'"}) 56 | } catch (err) { 57 | schema = err.message 58 | } 59 | return schema 60 | } 61 | 62 | class SanityGraphQLDemo extends React.Component { 63 | constructor(props) { 64 | super(props) 65 | this.state = {schema: getCompiled(props.initialSdl)} 66 | this.handleChange = this.handleChange.bind(this) 67 | this.persist = debounce(this.persist.bind(this), 400) 68 | } 69 | 70 | persist() { 71 | const currentValue = this.state.currentValue || '' 72 | try { 73 | window.localStorage && window.localStorage.setItem(localStorageKey, currentValue) 74 | } catch (err) { 75 | // intentional noop 76 | } 77 | } 78 | 79 | handleChange(editor, data, value) { 80 | const schema = getCompiled(value) 81 | this.setState({schema, currentValue: value}) 82 | this.persist() 83 | } 84 | 85 | render() { 86 | return ( 87 | 88 | 89 | 97 | 104 | 105 | ) 106 | } 107 | } 108 | 109 | ReactDOM.render( 110 | , 111 | document.getElementById('demo') 112 | ) 113 | -------------------------------------------------------------------------------- /src/coreSchema.js: -------------------------------------------------------------------------------- 1 | const graphql = require('./taggedNoop') 2 | 3 | module.exports = graphql` 4 | directive @display(title: String, icon: String, layout: String) on FIELD_DEFINITION | OBJECT 5 | directive @fieldsets(from: [SanitySchemaFieldSet!]!) on OBJECT 6 | directive @fieldset(set: String!) on FIELD_DEFINITION 7 | directive @orderings(from: [SanitySchemaOrdering!]!) on OBJECT 8 | 9 | directive @hotspot on FIELD_DEFINITION 10 | directive @inline on FIELD_DEFINITION 11 | directive @hidden on FIELD_DEFINITION 12 | directive @readOnly on FIELD_DEFINITION 13 | 14 | # String directives 15 | directive @enum( 16 | values: [SanityNameTitlePair!]! 17 | layout: String 18 | direction: String 19 | ) on FIELD_DEFINITION 20 | 21 | # Image / file directives 22 | directive @extract(metadata: [String!]!, originalFilename: Boolean) on FIELD_DEFINITION 23 | 24 | # Block directives 25 | directive @block( 26 | styles: [SanityNameTitlePair!] 27 | decorators: [SanityNameTitlePair!] 28 | annotations: String 29 | ) on OBJECT 30 | 31 | # Slug directives 32 | directive @slug(source: String, maxLength: Int) on FIELD_DEFINITION 33 | 34 | scalar Number 35 | scalar Email 36 | scalar Text 37 | scalar Date 38 | scalar Datetime 39 | scalar Url 40 | 41 | type SanityNameTitlePair { 42 | name: String! 43 | title: String 44 | } 45 | 46 | type SanitySchemaFieldSet { 47 | name: String! 48 | title: String! 49 | collapsible: Boolean 50 | collapsed: Boolean 51 | } 52 | 53 | type SanitySchemaOrdering { 54 | name: String! 55 | title: String! 56 | by: [SanitySchemaSortOrder!]! 57 | } 58 | 59 | type SanitySchemaSortOrder { 60 | field: String! 61 | direction: String! 62 | } 63 | 64 | interface Document { 65 | _id: ID! 66 | _type: String! 67 | _createdAt: Datetime! 68 | _updatedAt: Datetime! 69 | _rev: String! 70 | } 71 | 72 | interface Image { 73 | _key: String 74 | _type: String 75 | asset: SanityImageAsset 76 | hotspot: SanityImageHotspot 77 | crop: SanityImageCrop 78 | } 79 | 80 | interface File { 81 | _key: String 82 | _type: String 83 | asset: SanityFileAsset 84 | } 85 | 86 | type Geopoint { 87 | _key: String 88 | _type: String 89 | lat: Float 90 | lng: Float 91 | alt: Float 92 | } 93 | 94 | type SanityFileAsset implements Document { 95 | _id: ID! 96 | _type: String! 97 | _createdAt: Datetime! 98 | _updatedAt: Datetime! 99 | _rev: String! 100 | originalFilename: String 101 | label: String 102 | sha1hash: String 103 | extension: String 104 | mimeType: String 105 | size: Float 106 | assetId: String 107 | path: String! 108 | url: String! 109 | } 110 | 111 | type SanityImageAsset implements Document { 112 | _id: ID! 113 | _type: String! 114 | _createdAt: Datetime! 115 | _updatedAt: Datetime! 116 | _rev: String! 117 | originalFilename: String 118 | label: String 119 | sha1hash: String 120 | extension: String 121 | mimeType: String 122 | size: Float 123 | assetId: String 124 | path: String! 125 | url: String! 126 | metadata: SanityImageMetadata 127 | } 128 | 129 | type SanityImageCrop { 130 | _key: String 131 | _type: String 132 | top: Float 133 | bottom: Float 134 | left: Float 135 | right: Float 136 | } 137 | 138 | type SanityImageDimensions { 139 | _key: String 140 | _type: String 141 | height: Float 142 | width: Float 143 | aspectRatio: Float 144 | } 145 | 146 | type SanityImageHotspot { 147 | _key: String 148 | _type: String 149 | x: Float 150 | y: Float 151 | height: Float 152 | width: Float 153 | } 154 | 155 | type SanityImageMetadata { 156 | _key: String 157 | _type: String 158 | location: Geopoint 159 | dimensions: SanityImageDimensions 160 | palette: SanityImagePalette 161 | lqip: String 162 | } 163 | 164 | type SanityImagePalette { 165 | _key: String 166 | _type: String 167 | darkMuted: SanityImagePaletteSwatch 168 | lightVibrant: SanityImagePaletteSwatch 169 | darkVibrant: SanityImagePaletteSwatch 170 | vibrant: SanityImagePaletteSwatch 171 | dominant: SanityImagePaletteSwatch 172 | lightMuted: SanityImagePaletteSwatch 173 | muted: SanityImagePaletteSwatch 174 | } 175 | 176 | type SanityImagePaletteSwatch { 177 | _key: String 178 | _type: String 179 | background: String 180 | foreground: String 181 | population: Float 182 | title: String 183 | } 184 | 185 | type Slug { 186 | _key: String 187 | _type: String 188 | current: String 189 | } 190 | 191 | type Block { 192 | _key: String 193 | _type: String 194 | spans: [Span!] 195 | markDefs: [SpanMark!] 196 | style: String 197 | list: String 198 | } 199 | 200 | interface SpanMark { 201 | _key: String! 202 | } 203 | 204 | type Span { 205 | _key: String 206 | _type: String 207 | text: String 208 | marks: [String!] 209 | } 210 | ` 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sanity-graphql-schema 2 | 3 | ## DEPRECATED/UNMAINTAINED 4 | 5 | **⚠️ This module is no longer maintained!** 6 | 7 | The type system in Sanity is very expressive. GraphQL directives are neat, but modeling every schema variation through them can be a bit of a hassle. Still - this module/repo serves as an example of the fact that Sanity schemas are "just" javascript - and can be generated using _other_ javascript. Pretty sweet! 8 | 9 | ## Description 10 | 11 | Declare a [Sanity](https://www.sanity.io/) schema using GraphQL SDL syntax. [Try the demo?](https://rexxars.github.io/sanity-graphql-schema/) 12 | 13 | - Auto-infers title from type/field names (`leadAsset` -> `Lead asset`) 14 | - Type names are automatically camelcased (`BlogPost` -> `blogPost`) 15 | - Types that implement "Document" is made into document types 16 | - Document types as field type assumed to be references, object types as inline 17 | - Use `@inline` directive to use document type embedded into parent document 18 | - Directives for most schema type options 19 | - "Non null" treated as "required" validation rule (only enforced in Studio) 20 | 21 | ## Installation 22 | 23 | ``` 24 | # In your Sanity studio folder 25 | yarn add sanity-graphql-schema 26 | ``` 27 | 28 | ## Basic usage 29 | 30 | In your schema entry point (usually `schemas/schema.js`), you normally have something along the lines of this: 31 | 32 | ```js 33 | import createSchema from 'part:@sanity/base/schema-creator' 34 | import schemaTypes from 'all:part:@sanity/base/schema-type' 35 | 36 | import author from './author' 37 | 38 | export default createSchema({ 39 | name: 'default', 40 | types: schemaTypes.concat([author]) 41 | }) 42 | ``` 43 | 44 | To use this module, import it, call the imported function with a GraphQL schema defined in the GraphQL schema definition language, and replace the value passed to `createSchema()` with the output: 45 | 46 | ```js 47 | import createSchema from 'part:@sanity/base/schema-creator' 48 | import {fromGQL, graphql} from 'sanity-graphql-schema' 49 | 50 | const schema = graphql` 51 | type Author implements Document { 52 | name: String! 53 | profileImage: Image 54 | } 55 | 56 | type BlogPost implements Document { 57 | title: String! 58 | slug: Slug 59 | body: Text 60 | leadImage: CaptionedImage 61 | tags: [String!]! @display(layout: "tags") 62 | author: Author! 63 | } 64 | 65 | type CaptionedImage implements Image { 66 | caption: String! 67 | } 68 | ` 69 | 70 | export default createSchema({ 71 | name: 'default', 72 | types: fromGQL(schema) 73 | }) 74 | ``` 75 | 76 | ## Directives 77 | 78 | - `@display(title: String, icon: String, layout: String)` 79 | Allows you to: 80 | - Set title of fields and types, overriding the auto-generated title 81 | - Set layout mode for a field (for instance, set `tags` as layout for an array field) 82 | - Set icon for document types by passing an icon ID (see example below) 83 | 84 | * `@fieldsets(from: [SanitySchemaFieldSet!]!)` 85 | Set the fieldsets available for an object/document type. Takes an array of `{name, title}` pairs 86 | 87 | * `@fieldset(set: String!)` 88 | Assign a field to a given fieldset 89 | 90 | * `@orderings(from: [SanitySchemaOrdering!]!)` 91 | Set the available ordering options for a document type. 92 | Takes an array of `{name, title, by}` pairs - see [Sanity documentation](https://www.sanity.io/docs/the-schema/sort-orders) for more information 93 | 94 | - `@enum(values: [SanityNameTitlePair!]!, layout: String, direction: String)` 95 | Set on string fields if you only want to allow certain values. 96 | - `values` takes an array of `{name, title}` pairs. 97 | - `layout` can be one of `dropdown` or `radio`, `dropdown` being the default. 98 | - `direction` determines which way radio buttons flow (`horizontal`, `vertical`) 99 | 100 | * `@extract(metadata: [String!]!, originalFilename: Boolean)` 101 | Set on fields of type `Image` or `File` to determine which metadata to extract, and whether or not to store the original filename 102 | 103 | * `@slug(source: String, maxLength: Int)` 104 | Set on fields of type `Slug`, determines which field to generate slugs from, or set maximum length of the slug. 105 | 106 | * `@hotspot` 107 | Set on image fields to opt-in to the hotspot/crop functionality 108 | 109 | * `@inline` 110 | Set on fields that use a document type as it's value if you want to embed the value instead of referencing an existing document 111 | 112 | * `@hidden` 113 | Set on fields to hide them from the user interface, yet still be recognized by the underlying schema machinery 114 | 115 | * `@readOnly` 116 | Set on fields to that should not be editable from the studio 117 | 118 | ## Todo 119 | 120 | - [x] Generate GQL types for custom types (color input, for instance) 121 | - [x] Custom scalars (`markdown`) 122 | - [x] Throw on non-reference union fields 123 | - [x] Better error handling 124 | - [x] Blocks/spans 125 | - [ ] Validation directives 126 | - [ ] Inject icons, input components as options argument 127 | - [ ] JSON scalar for arbitrary input for custom types? Or options as dotpath? Both? 128 | 129 | ## Developing 130 | 131 | ```bash 132 | git clone git@github.com:rexxars/sanity-graphql-schema.git 133 | cd sanity-graphql-schema 134 | npm install 135 | npm test 136 | ``` 137 | 138 | ## License 139 | 140 | MIT © [Espen Hovlandsdal](https://espen.codes/) 141 | -------------------------------------------------------------------------------- /demo/css/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: monospace; 6 | height: 300px; 7 | color: black; 8 | direction: ltr; 9 | } 10 | 11 | /* PADDING */ 12 | 13 | .CodeMirror-lines { 14 | padding: 4px 0; /* Vertical padding around content */ 15 | } 16 | .CodeMirror pre { 17 | padding: 0 4px; /* Horizontal padding of content */ 18 | } 19 | 20 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 21 | background-color: white; /* The little square between H and V scrollbars */ 22 | } 23 | 24 | /* GUTTER */ 25 | 26 | .CodeMirror-gutters { 27 | border-right: 1px solid #ddd; 28 | background-color: #f7f7f7; 29 | white-space: nowrap; 30 | } 31 | .CodeMirror-linenumbers {} 32 | .CodeMirror-linenumber { 33 | padding: 0 3px 0 5px; 34 | min-width: 20px; 35 | text-align: right; 36 | color: #999; 37 | white-space: nowrap; 38 | } 39 | 40 | .CodeMirror-guttermarker { color: black; } 41 | .CodeMirror-guttermarker-subtle { color: #999; } 42 | 43 | /* CURSOR */ 44 | 45 | .CodeMirror-cursor { 46 | border-left: 1px solid black; 47 | border-right: none; 48 | width: 0; 49 | } 50 | /* Shown when moving in bi-directional text */ 51 | .CodeMirror div.CodeMirror-secondarycursor { 52 | border-left: 1px solid silver; 53 | } 54 | .cm-fat-cursor .CodeMirror-cursor { 55 | width: auto; 56 | border: 0 !important; 57 | background: #7e7; 58 | } 59 | .cm-fat-cursor div.CodeMirror-cursors { 60 | z-index: 1; 61 | } 62 | .cm-fat-cursor-mark { 63 | background-color: rgba(20, 255, 20, 0.5); 64 | -webkit-animation: blink 1.06s steps(1) infinite; 65 | -moz-animation: blink 1.06s steps(1) infinite; 66 | animation: blink 1.06s steps(1) infinite; 67 | } 68 | .cm-animate-fat-cursor { 69 | width: auto; 70 | border: 0; 71 | -webkit-animation: blink 1.06s steps(1) infinite; 72 | -moz-animation: blink 1.06s steps(1) infinite; 73 | animation: blink 1.06s steps(1) infinite; 74 | background-color: #7e7; 75 | } 76 | @-moz-keyframes blink { 77 | 0% {} 78 | 50% { background-color: transparent; } 79 | 100% {} 80 | } 81 | @-webkit-keyframes blink { 82 | 0% {} 83 | 50% { background-color: transparent; } 84 | 100% {} 85 | } 86 | @keyframes blink { 87 | 0% {} 88 | 50% { background-color: transparent; } 89 | 100% {} 90 | } 91 | 92 | /* Can style cursor different in overwrite (non-insert) mode */ 93 | .CodeMirror-overwrite .CodeMirror-cursor {} 94 | 95 | .cm-tab { display: inline-block; text-decoration: inherit; } 96 | 97 | .CodeMirror-rulers { 98 | position: absolute; 99 | left: 0; right: 0; top: -50px; bottom: -20px; 100 | overflow: hidden; 101 | } 102 | .CodeMirror-ruler { 103 | border-left: 1px solid #ccc; 104 | top: 0; bottom: 0; 105 | position: absolute; 106 | } 107 | 108 | /* DEFAULT THEME */ 109 | 110 | .cm-s-default .cm-header {color: blue;} 111 | .cm-s-default .cm-quote {color: #090;} 112 | .cm-negative {color: #d44;} 113 | .cm-positive {color: #292;} 114 | .cm-header, .cm-strong {font-weight: bold;} 115 | .cm-em {font-style: italic;} 116 | .cm-link {text-decoration: underline;} 117 | .cm-strikethrough {text-decoration: line-through;} 118 | 119 | .cm-s-default .cm-keyword {color: #708;} 120 | .cm-s-default .cm-atom {color: #219;} 121 | .cm-s-default .cm-number {color: #164;} 122 | .cm-s-default .cm-def {color: #00f;} 123 | .cm-s-default .cm-variable, 124 | .cm-s-default .cm-punctuation, 125 | .cm-s-default .cm-property, 126 | .cm-s-default .cm-operator {} 127 | .cm-s-default .cm-variable-2 {color: #05a;} 128 | .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} 129 | .cm-s-default .cm-comment {color: #a50;} 130 | .cm-s-default .cm-string {color: #a11;} 131 | .cm-s-default .cm-string-2 {color: #f50;} 132 | .cm-s-default .cm-meta {color: #555;} 133 | .cm-s-default .cm-qualifier {color: #555;} 134 | .cm-s-default .cm-builtin {color: #30a;} 135 | .cm-s-default .cm-bracket {color: #997;} 136 | .cm-s-default .cm-tag {color: #170;} 137 | .cm-s-default .cm-attribute {color: #00c;} 138 | .cm-s-default .cm-hr {color: #999;} 139 | .cm-s-default .cm-link {color: #00c;} 140 | 141 | .cm-s-default .cm-error {color: #f00;} 142 | .cm-invalidchar {color: #f00;} 143 | 144 | .CodeMirror-composing { border-bottom: 2px solid; } 145 | 146 | /* Default styles for common addons */ 147 | 148 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} 149 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} 150 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 151 | .CodeMirror-activeline-background {background: #e8f2ff;} 152 | 153 | /* STOP */ 154 | 155 | /* The rest of this file contains styles related to the mechanics of 156 | the editor. You probably shouldn't touch them. */ 157 | 158 | .CodeMirror { 159 | position: relative; 160 | overflow: hidden; 161 | background: white; 162 | } 163 | 164 | .CodeMirror-scroll { 165 | overflow: scroll !important; /* Things will break if this is overridden */ 166 | /* 30px is the magic margin used to hide the element's real scrollbars */ 167 | /* See overflow: hidden in .CodeMirror */ 168 | margin-bottom: -30px; margin-right: -30px; 169 | padding-bottom: 30px; 170 | height: 100%; 171 | outline: none; /* Prevent dragging from highlighting the element */ 172 | position: relative; 173 | } 174 | .CodeMirror-sizer { 175 | position: relative; 176 | border-right: 30px solid transparent; 177 | } 178 | 179 | /* The fake, visible scrollbars. Used to force redraw during scrolling 180 | before actual scrolling happens, thus preventing shaking and 181 | flickering artifacts. */ 182 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 183 | position: absolute; 184 | z-index: 6; 185 | display: none; 186 | } 187 | .CodeMirror-vscrollbar { 188 | right: 0; top: 0; 189 | overflow-x: hidden; 190 | overflow-y: scroll; 191 | } 192 | .CodeMirror-hscrollbar { 193 | bottom: 0; left: 0; 194 | overflow-y: hidden; 195 | overflow-x: scroll; 196 | } 197 | .CodeMirror-scrollbar-filler { 198 | right: 0; bottom: 0; 199 | } 200 | .CodeMirror-gutter-filler { 201 | left: 0; bottom: 0; 202 | } 203 | 204 | .CodeMirror-gutters { 205 | position: absolute; left: 0; top: 0; 206 | min-height: 100%; 207 | z-index: 3; 208 | } 209 | .CodeMirror-gutter { 210 | white-space: normal; 211 | height: 100%; 212 | display: inline-block; 213 | vertical-align: top; 214 | margin-bottom: -30px; 215 | } 216 | .CodeMirror-gutter-wrapper { 217 | position: absolute; 218 | z-index: 4; 219 | background: none !important; 220 | border: none !important; 221 | } 222 | .CodeMirror-gutter-background { 223 | position: absolute; 224 | top: 0; bottom: 0; 225 | z-index: 4; 226 | } 227 | .CodeMirror-gutter-elt { 228 | position: absolute; 229 | cursor: default; 230 | z-index: 4; 231 | } 232 | .CodeMirror-gutter-wrapper ::selection { background-color: transparent } 233 | .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } 234 | 235 | .CodeMirror-lines { 236 | cursor: text; 237 | min-height: 1px; /* prevents collapsing before first draw */ 238 | } 239 | .CodeMirror pre { 240 | /* Reset some styles that the rest of the page might have set */ 241 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 242 | border-width: 0; 243 | background: transparent; 244 | font-family: inherit; 245 | font-size: inherit; 246 | margin: 0; 247 | white-space: pre; 248 | word-wrap: normal; 249 | line-height: inherit; 250 | color: inherit; 251 | z-index: 2; 252 | position: relative; 253 | overflow: visible; 254 | -webkit-tap-highlight-color: transparent; 255 | -webkit-font-variant-ligatures: contextual; 256 | font-variant-ligatures: contextual; 257 | } 258 | .CodeMirror-wrap pre { 259 | word-wrap: break-word; 260 | white-space: pre-wrap; 261 | word-break: normal; 262 | } 263 | 264 | .CodeMirror-linebackground { 265 | position: absolute; 266 | left: 0; right: 0; top: 0; bottom: 0; 267 | z-index: 0; 268 | } 269 | 270 | .CodeMirror-linewidget { 271 | position: relative; 272 | z-index: 2; 273 | padding: 0.1px; /* Force widget margins to stay inside of the container */ 274 | } 275 | 276 | .CodeMirror-widget {} 277 | 278 | .CodeMirror-rtl pre { direction: rtl; } 279 | 280 | .CodeMirror-code { 281 | outline: none; 282 | } 283 | 284 | /* Force content-box sizing for the elements where we expect it */ 285 | .CodeMirror-scroll, 286 | .CodeMirror-sizer, 287 | .CodeMirror-gutter, 288 | .CodeMirror-gutters, 289 | .CodeMirror-linenumber { 290 | -moz-box-sizing: content-box; 291 | box-sizing: content-box; 292 | } 293 | 294 | .CodeMirror-measure { 295 | position: absolute; 296 | width: 100%; 297 | height: 0; 298 | overflow: hidden; 299 | visibility: hidden; 300 | } 301 | 302 | .CodeMirror-cursor { 303 | position: absolute; 304 | pointer-events: none; 305 | } 306 | .CodeMirror-measure pre { position: static; } 307 | 308 | div.CodeMirror-cursors { 309 | visibility: hidden; 310 | position: relative; 311 | z-index: 3; 312 | } 313 | div.CodeMirror-dragcursors { 314 | visibility: visible; 315 | } 316 | 317 | .CodeMirror-focused div.CodeMirror-cursors { 318 | visibility: visible; 319 | } 320 | 321 | .CodeMirror-selected { background: #d9d9d9; } 322 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 323 | .CodeMirror-crosshair { cursor: crosshair; } 324 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 325 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 326 | 327 | .cm-searching { 328 | background-color: #ffa; 329 | background-color: rgba(255, 255, 0, .4); 330 | } 331 | 332 | /* Used to force a border model for a node */ 333 | .cm-force-border { padding-right: .1px; } 334 | 335 | @media print { 336 | /* Hide the cursor when printing */ 337 | .CodeMirror div.CodeMirror-cursors { 338 | visibility: hidden; 339 | } 340 | } 341 | 342 | /* See issue #2901 */ 343 | .cm-tab-wrap-hack:after { content: ''; } 344 | 345 | /* Help users use markselection to safely style text background */ 346 | span.CodeMirror-selectedtext { background: none; } 347 | -------------------------------------------------------------------------------- /src/compileSchema.js: -------------------------------------------------------------------------------- 1 | const oneline = require('oneline') 2 | const {parse, specifiedScalarTypes, valueFromASTUntyped, buildASTSchema} = require('graphql') 3 | const {words, snakeCase, camelCase, upperFirst, isPlainObject} = require('lodash') 4 | const coreSchema = require('./coreSchema') 5 | const schemaError = require('./schemaError') 6 | const stubExternalTypes = require('./stubExternalTypes') 7 | const withoutUndefined = require('./withoutUndefined') 8 | 9 | const core = parse(coreSchema) 10 | const coreTypeMap = core.definitions.reduce((map, def) => map.set(def.name.value, def), new Map()) 11 | 12 | const unsupported = ['ScalarTypeDefinition', 'InputObjectTypeDefinition'] 13 | 14 | module.exports = function compileSchema(userSchema, options) { 15 | const externalTypes = options.externalTypes || [] 16 | const externalTypesSchema = stubExternalTypes(externalTypes) 17 | const merged = parse([coreSchema, externalTypesSchema, userSchema].join('\n\n')) 18 | const external = externalTypesSchema ? parse(externalTypesSchema) : {definitions: []} 19 | const parsed = parse(userSchema) 20 | if (merged.kind !== 'Document') { 21 | return [schemaError('Could not parse GraphQL schema, expected kind `Document` at root')] 22 | } 23 | 24 | // Build schema to validate it 25 | const schema = buildASTSchema(merged) 26 | 27 | const externalTypeMap = external.definitions.reduce( 28 | (map, def) => map.set(def.name.value, def), 29 | new Map() 30 | ) 31 | 32 | const defs = parsed.definitions || [] 33 | const userTypeMap = new Map() 34 | for (let i = 0; i < defs.length; i++) { 35 | const def = defs[i] 36 | if (unsupported.includes(def.kind)) { 37 | return [schemaError(`Unsupported node of kind ${def.kind} found`)] 38 | } 39 | 40 | userTypeMap.set(def.name.value, def) 41 | } 42 | 43 | const opts = {schema, icons: {}} 44 | const typeMap = new Map([...userTypeMap, ...externalTypeMap, ...coreTypeMap]) 45 | const sanityTypes = [] 46 | for (const def of userTypeMap.values()) { 47 | const sanityType = convertType(def, typeMap, opts) 48 | if (sanityType) { 49 | sanityTypes.push(sanityType) 50 | } 51 | } 52 | 53 | return externalTypes.concat(sanityTypes) 54 | } 55 | 56 | function titleCase(str) { 57 | const [first, ...rest] = words(snakeCase(str)) 58 | return [upperFirst(first), ...rest].join(' ') 59 | } 60 | 61 | function isCoreScalar(typeName) { 62 | return specifiedScalarTypes.some(scalar => scalar.name === typeName) 63 | } 64 | 65 | function isScalar(typeName, map) { 66 | if (isCoreScalar(typeName)) { 67 | return true 68 | } 69 | 70 | const type = map.get(typeName) 71 | return type && isScalarTypeDef(type) 72 | } 73 | 74 | function isScalarTypeDef(def) { 75 | return def.kind === 'ScalarTypeDefinition' 76 | } 77 | 78 | function isInterface(def) { 79 | return def.kind === 'InterfaceTypeDefinition' 80 | } 81 | 82 | function isUnion(def) { 83 | return def && def.kind === 'UnionTypeDefinition' 84 | } 85 | 86 | function isDocument(def) { 87 | return def.interfaces && def.interfaces.some(iface => iface.name.value === 'Document') 88 | } 89 | 90 | function isArgument(node) { 91 | return node.kind === 'Argument' 92 | } 93 | 94 | function typeImplementsInterface(type, ifaceName) { 95 | if (!type) { 96 | return false 97 | } 98 | 99 | if (isInterface(type) && type.name.value === ifaceName) { 100 | return true 101 | } 102 | 103 | return type.interfaces && type.interfaces.some(iface => iface.name.value === ifaceName) 104 | } 105 | 106 | function getUnwrappedType(typeDef, map) { 107 | const unwrapFor = ['NonNullType', 'ListType'] 108 | const type = unwrapFor.includes(typeDef.kind) ? typeDef.type : typeDef 109 | if (unwrapFor.includes(type.kind)) { 110 | return getUnwrappedType(type, map) 111 | } 112 | 113 | if (type.kind !== 'NamedType') { 114 | throw new Error(oneline` 115 | Expected unwrapped type to be of kind "NamedType", got "${type.kind}" 116 | `) 117 | } 118 | 119 | if (isCoreScalar(type.name.value)) { 120 | return { 121 | kind: 'NamedType', 122 | name: {kind: 'Name', value: type.name.value} 123 | } 124 | } 125 | 126 | return map.get(type.name.value) 127 | } 128 | 129 | function getDirectives(def) { 130 | return (def.directives || []).reduce( 131 | (acc, dir) => ({ 132 | ...acc, 133 | [dir.name.value]: dir.arguments 134 | .filter(isArgument) 135 | .reduce((args, arg) => ({...args, [arg.name.value]: valueFromASTUntyped(arg.value)}), {}) 136 | }), 137 | {display: {}} 138 | ) 139 | } 140 | 141 | function convertType(def, ...args) { 142 | switch (def.kind) { 143 | case 'ObjectTypeDefinition': 144 | return convertObjectType(def, ...args) 145 | case 'UnionTypeDefinition': 146 | return null 147 | default: 148 | return schemaError(`Unhandled node kind "${def.kind}"`) 149 | } 150 | } 151 | 152 | function flattenUnionType(def, map) { 153 | return def.types.reduce((types, typeDef) => { 154 | const type = map.get(typeDef.name.value) || typeDef 155 | return isUnion(type) ? [...types, ...flattenUnionType(type, map)] : [...types, type] 156 | }, []) 157 | } 158 | 159 | function convertBlockType(def, map, options, directives) { 160 | const typeName = def.name.value 161 | const {display, block} = directives 162 | const {icons} = options 163 | 164 | const fields = def.fields.reduce((acc, field) => { 165 | acc[field.name.value] = field 166 | return acc 167 | }, {}) 168 | 169 | const {of: inlineTypes, ...rest} = fields 170 | const unknown = Object.keys(rest) 171 | if (unknown.length > 0) { 172 | return schemaError(oneline` 173 | Unknown keys for block type: 174 | ${unknown.map(str => `"${str}"`).join(', ')} 175 | `) 176 | } 177 | 178 | let ofMembers 179 | if (inlineTypes) { 180 | const nonNull = inlineTypes.type.kind === 'NonNullType' 181 | const type = nonNull ? inlineTypes.type.type : inlineTypes.type 182 | const ofDef = convertArrayField(inlineTypes, map, {...options, parent: def}, type, inlineTypes) 183 | ofMembers = ofDef.of 184 | } 185 | 186 | let {styles, decorators, annotations} = block 187 | if (styles) { 188 | const invalid = validateNameTitleArray(styles, 'styles') 189 | if (invalid) { 190 | return invalid 191 | } 192 | 193 | styles = enforceTitlesForNameTitlePairs(styles) 194 | } 195 | 196 | if (decorators) { 197 | const invalid = validateNameTitleArray(decorators, 'decorators') 198 | if (invalid) { 199 | return invalid 200 | } 201 | 202 | decorators = enforceTitlesForNameTitlePairs(decorators) 203 | } 204 | 205 | if (annotations && typeof annotations !== 'string') { 206 | return schemaError('`annotations` needs to be a string referencing a type', 'annotations') 207 | } else if (annotations && !map.has(annotations)) { 208 | return schemaError(`'annotations' references missing type: "${annotations}"`) 209 | } else if (annotations) { 210 | annotations = getAnnotationMembers(def, annotations, map, options) 211 | } 212 | 213 | return withoutUndefined({ 214 | name: camelCase(typeName), 215 | title: display.title || titleCase(def.name.value), 216 | type: 'block', 217 | description: def.description && def.description.value, 218 | icon: icons[display.icon], 219 | of: ofMembers 220 | }) 221 | } 222 | 223 | function getAnnotationMembers(def, annotations, map, options) { 224 | return convertArrayField( 225 | {}, 226 | map, 227 | {...options, parent: def}, 228 | { 229 | kind: 'NamedType', 230 | name: {kind: 'Name', value: annotations} 231 | }, 232 | {name: 'annotations'} 233 | ).of 234 | } 235 | 236 | function validateNameTitleArray(arr, name) { 237 | if (!Array.isArray(arr)) { 238 | return schemaError(`${name} must be an array of name/title pairs, got ${typeof arr}`, name) 239 | } 240 | 241 | for (let i = 0; i < arr.length; i++) { 242 | if (!isPlainObject(arr[i])) { 243 | return schemaError(`Expected name/title pair, unexpected value at index ${i}`, name) 244 | } 245 | 246 | const unknown = Object.keys(arr[i]).filter(key => !['name', 'title'].includes(key)) 247 | if (unknown.length > 0) { 248 | return schemaError( 249 | `Name/title pair has unknown keys: ${unknown.map(key => `"${key}"`).join(', ')}`, 250 | name 251 | ) 252 | } 253 | 254 | if (!arr[i].name) { 255 | return schemaError(`Name/title pair at index ${i} is missing "name" property`, name) 256 | } 257 | } 258 | 259 | return false 260 | } 261 | 262 | function enforceTitlesForNameTitlePairs(arr) { 263 | const getTitle = name => (/^h\d$/.test(name) ? `Heading ${name.slice(1)}` : titleCase(name)) 264 | return arr.map(item => ({ 265 | name: item.name, 266 | title: item.title || getTitle(item.name) 267 | })) 268 | } 269 | 270 | function convertObjectType(def, map, options) { 271 | const typeName = def.name.value 272 | 273 | if (coreTypeMap.has(typeName)) { 274 | return schemaError( 275 | oneline` 276 | "${typeName}" is a bundled type, please use a different name`, 277 | typeName 278 | ) 279 | } 280 | 281 | if (def.interfaces.length > 1) { 282 | return schemaError( 283 | oneline` 284 | Tried to implement multiple interfaces, only a single interface is allowed 285 | `, 286 | typeName 287 | ) 288 | } 289 | 290 | let type = 'object' 291 | if (def.interfaces[0]) { 292 | const iface = map.get(def.interfaces[0].name.value) 293 | if (!iface) { 294 | return schemaError( 295 | oneline` 296 | Tried to implement interface "${def.interfaces[0].name.value}", 297 | which is not defined`, 298 | typeName 299 | ) 300 | } 301 | 302 | if (!isInterface(iface)) { 303 | return schemaError( 304 | `Tried to implement "${iface.name.value}", which is not an interface`, 305 | typeName 306 | ) 307 | } 308 | 309 | type = camelCase(iface.name.value) 310 | } 311 | 312 | const directives = getDirectives(def) 313 | const {display, fieldsets, orderings, block} = directives 314 | const {icons} = options 315 | 316 | if (block) { 317 | return convertBlockType(def, map, options, directives) 318 | } 319 | 320 | return withoutUndefined({ 321 | name: camelCase(typeName), 322 | title: display.title || titleCase(def.name.value), 323 | type, 324 | description: def.description && def.description.value, 325 | icon: icons[display.icon], 326 | fields: def.fields.map(field => convertField(field, map, {...options, parent: def})), 327 | orderings: orderings && orderings.from, 328 | fieldsets: fieldsets && fieldsets.from 329 | }) 330 | } 331 | 332 | function optionsFromDirectives(type, directives, map, options) { 333 | const {parent} = options 334 | const typeName = type.name && type.name.value 335 | const typeDef = typeName && map.get(typeName) 336 | const {display, enum: enumDir, extract, hotspot, slug} = directives 337 | if (typeName === 'String') { 338 | return { 339 | list: enumDir && enumDir.values, 340 | layout: enumDir && enumDir.layout, 341 | direction: enumDir && enumDir.direction 342 | } 343 | } 344 | 345 | if (typeName === 'Slug') { 346 | return { 347 | source: (slug && slug.source) || getFirstStringField(parent.fields, map), 348 | maxLength: (slug && slug.maxLength) || 200 349 | } 350 | } 351 | 352 | if (typeImplementsInterface(typeDef, 'Image')) { 353 | return { 354 | metadata: extract && extract.metadata, 355 | storeOriginalFilename: extract && extract.originalFilename, 356 | hotspot: Boolean(hotspot) || undefined 357 | } 358 | } 359 | 360 | if (typeImplementsInterface(typeDef, 'File')) { 361 | return { 362 | storeOriginalFilename: extract && extract.originalFilename 363 | } 364 | } 365 | 366 | if (type.kind === 'ListType') { 367 | return { 368 | layout: display && display.layout 369 | } 370 | } 371 | 372 | return {} 373 | } 374 | 375 | function getFirstStringField(fields, map) { 376 | const first = fields 377 | .map(field => ({name: field.name.value, type: getUnwrappedType(field.type, map)})) 378 | .find(field => field.type && field.type.name.value === 'String') 379 | 380 | return first ? first.name : undefined 381 | } 382 | 383 | function convertArrayField(def, map, options, type, field) { 384 | const unwrapped = getUnwrappedType(type, map) 385 | const namedMembers = isUnion(unwrapped) ? flattenUnionType(unwrapped, map) : [unwrapped] 386 | const members = namedMembers.map(member => map.get(member.name.value) || member) 387 | const hasScalars = members.some(member => isScalar(member.name.value, map)) 388 | const hasNonScalars = members.some(member => !isScalar(member.name.value, map)) 389 | const hasDocuments = members.some(isDocument) 390 | const allDocuments = members.every(isDocument) 391 | 392 | if (hasScalars && hasNonScalars) { 393 | return schemaError( 394 | oneline` 395 | Field "${field.name}" on type "${options.parent.name.value}" 396 | cannot contain both scalar and object types`, 397 | field.name, 398 | field 399 | ) 400 | } 401 | 402 | let ofDef 403 | const forcedInline = Boolean(getDirectives(def).inline) 404 | 405 | if (hasScalars || forcedInline || !hasDocuments) { 406 | // [String | Number] 407 | // [Author | Book] @inline 408 | // [Image | ImageWithCaption] 409 | ofDef = members.map(getFieldDefForNamedType) 410 | } else if (allDocuments) { 411 | // [Author | Book] 412 | ofDef = [{type: 'reference', to: members.map(getFieldDefForNamedType)}] 413 | } else { 414 | // [Author | Image] 415 | const docs = members.filter(isDocument) 416 | const inline = members.filter(member => !isDocument(member)) 417 | ofDef = [ 418 | {type: 'reference', to: docs.map(getFieldDefForNamedType)}, 419 | ...inline.map(getFieldDefForNamedType) 420 | ] 421 | } 422 | 423 | return withoutUndefined({...field, type: 'array', of: ofDef}) 424 | } 425 | 426 | function convertField(def, map, options) { 427 | const nonNull = def.type.kind === 'NonNullType' 428 | const type = nonNull ? def.type.type : def.type 429 | const name = def.name && def.name.value 430 | 431 | const directives = getDirectives(def) 432 | const {display, readOnly, hidden, fieldset, enum: enumDir} = directives 433 | 434 | const field = { 435 | name, 436 | type: undefined, 437 | title: display.title || (name && titleCase(name)), 438 | description: def.description && def.description.value, 439 | readOnly: Boolean(readOnly) || undefined, 440 | hidden: Boolean(hidden) || undefined, 441 | fieldset: fieldset && fieldset.set, 442 | options: withoutUndefined(optionsFromDirectives(type, directives, map, options)) 443 | } 444 | 445 | if (def.arguments && def.arguments.length > 0) { 446 | return schemaError(`Field arguments are not supported`, name, field) 447 | } 448 | 449 | if (enumDir && type.name && type.name.value !== 'String') { 450 | return schemaError(`@enum directive only allowed for String fields`, name, field) 451 | } 452 | 453 | if (nonNull) { 454 | field.validation = field.validation 455 | ? [field.validation, Rule => Rule.required()] 456 | : Rule => Rule.required() 457 | } 458 | 459 | if (type.kind === 'ListType') { 460 | return convertArrayField(def, map, options, type, field) 461 | } 462 | 463 | if (type.kind === 'NamedType') { 464 | return convertNamedTypeField(def, map, options, type, field) 465 | } 466 | 467 | return field 468 | } 469 | 470 | function convertNamedTypeField(def, map, options, type, baseField) { 471 | const name = def.name && def.name.value 472 | const {parent} = options 473 | const typeName = type.name.value 474 | const referencedType = map.get(typeName) 475 | 476 | if (!referencedType && !isCoreScalar(typeName)) { 477 | return schemaError( 478 | oneline` 479 | Field "${name}" on type "${parent.name.value}" is of type "${typeName}", 480 | which is not a defined type`, 481 | name, 482 | baseField 483 | ) 484 | } else if (!referencedType || isScalar(typeName, map)) { 485 | return withoutUndefined({...baseField, ...getFieldDefForNamedType(type)}) 486 | } 487 | 488 | const members = isUnion(referencedType) ? flattenUnionType(referencedType, map) : [referencedType] 489 | const allMembersAreDocuments = members.every(isDocument) 490 | const shouldInline = Boolean(getDirectives(def).inline) || !allMembersAreDocuments 491 | const shouldReference = !shouldInline 492 | 493 | // Sanity doesn't support union fields, as such. 494 | // It does support references to one or more document type, but not "inline" unions. 495 | if (referencedType && !shouldReference && isUnion(referencedType)) { 496 | const targets = referencedType.types.map(member => getUnwrappedType(member, map)) 497 | const nonDocTypes = targets 498 | .filter(member => !isDocument(member)) 499 | .map(member => member.name.value) 500 | 501 | const plural = nonDocTypes.length > 1 ? 'are not document types' : 'is not a document type' 502 | const help = 503 | nonDocTypes.length > 0 && 504 | `(${nonDocTypes.join(', ')} ${plural} and therefore must be inlined)` 505 | 506 | return schemaError( 507 | oneline` 508 | Field "${name}" on type "${parent.name.value}" cannot be 509 | an inline union ${help || ''}`, 510 | name, 511 | baseField 512 | ) 513 | } 514 | 515 | const fieldDef = getFieldDefForNamedType(type) 516 | const refOverride = shouldReference 517 | ? {type: 'reference', to: members.map(getFieldDefForNamedType)} 518 | : {} 519 | 520 | return withoutUndefined({...baseField, ...fieldDef, ...refOverride}) 521 | } 522 | 523 | function getFieldDefForNamedType(type) { 524 | switch (type.name.value) { 525 | case 'Int': 526 | return {type: 'number', validation: Rule => Rule.integer()} 527 | case 'Float': 528 | return {type: 'number'} 529 | case 'ID': 530 | return {type: 'string'} 531 | default: 532 | return {type: camelCase(type.name.value)} 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /test/__snapshots__/test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`01-simple 1`] = ` 4 | Array [ 5 | Object { 6 | "fields": Array [ 7 | Object { 8 | "name": "name", 9 | "title": "Name", 10 | "type": "string", 11 | }, 12 | ], 13 | "name": "author", 14 | "title": "Author", 15 | "type": "object", 16 | }, 17 | ] 18 | `; 19 | 20 | exports[`02-basics 1`] = ` 21 | Array [ 22 | Object { 23 | "fields": Array [ 24 | Object { 25 | "name": "int", 26 | "title": "Int", 27 | "type": "number", 28 | "validation": [Function], 29 | }, 30 | Object { 31 | "name": "float", 32 | "title": "Float", 33 | "type": "number", 34 | }, 35 | Object { 36 | "name": "string", 37 | "title": "String", 38 | "type": "string", 39 | }, 40 | Object { 41 | "name": "bool", 42 | "title": "Bool", 43 | "type": "boolean", 44 | }, 45 | Object { 46 | "name": "id", 47 | "title": "Id", 48 | "type": "string", 49 | }, 50 | Object { 51 | "name": "number", 52 | "title": "Number", 53 | "type": "number", 54 | }, 55 | Object { 56 | "name": "email", 57 | "title": "Email", 58 | "type": "email", 59 | }, 60 | Object { 61 | "name": "text", 62 | "title": "Text", 63 | "type": "text", 64 | }, 65 | Object { 66 | "name": "date", 67 | "title": "Date", 68 | "type": "date", 69 | }, 70 | Object { 71 | "name": "dateTime", 72 | "title": "Date time", 73 | "type": "datetime", 74 | }, 75 | Object { 76 | "name": "url", 77 | "title": "Url", 78 | "type": "url", 79 | }, 80 | Object { 81 | "name": "list", 82 | "of": Array [ 83 | Object { 84 | "type": "string", 85 | }, 86 | ], 87 | "title": "List", 88 | "type": "array", 89 | }, 90 | Object { 91 | "name": "textList", 92 | "of": Array [ 93 | Object { 94 | "type": "text", 95 | }, 96 | ], 97 | "title": "Text list", 98 | "type": "array", 99 | }, 100 | ], 101 | "name": "author", 102 | "title": "Author", 103 | "type": "object", 104 | }, 105 | ] 106 | `; 107 | 108 | exports[`03-multi-type 1`] = ` 109 | Array [ 110 | Object { 111 | "fields": Array [ 112 | Object { 113 | "name": "name", 114 | "title": "Name", 115 | "type": "string", 116 | "validation": [Function], 117 | }, 118 | ], 119 | "name": "author", 120 | "title": "Author", 121 | "type": "object", 122 | }, 123 | Object { 124 | "fields": Array [ 125 | Object { 126 | "name": "title", 127 | "title": "Title", 128 | "type": "string", 129 | }, 130 | ], 131 | "name": "book", 132 | "title": "Book", 133 | "type": "document", 134 | }, 135 | ] 136 | `; 137 | 138 | exports[`04-reference 1`] = ` 139 | Array [ 140 | Object { 141 | "fields": Array [ 142 | Object { 143 | "name": "name", 144 | "title": "Name", 145 | "type": "string", 146 | "validation": [Function], 147 | }, 148 | ], 149 | "name": "author", 150 | "title": "Author", 151 | "type": "document", 152 | }, 153 | Object { 154 | "fields": Array [ 155 | Object { 156 | "name": "title", 157 | "title": "Title", 158 | "type": "string", 159 | }, 160 | Object { 161 | "name": "author", 162 | "title": "Author", 163 | "to": Array [ 164 | Object { 165 | "type": "author", 166 | }, 167 | ], 168 | "type": "reference", 169 | "validation": [Function], 170 | }, 171 | ], 172 | "name": "book", 173 | "title": "Book", 174 | "type": "document", 175 | }, 176 | ] 177 | `; 178 | 179 | exports[`05-reference-array 1`] = ` 180 | Array [ 181 | Object { 182 | "fields": Array [ 183 | Object { 184 | "name": "name", 185 | "title": "Name", 186 | "type": "string", 187 | "validation": [Function], 188 | }, 189 | ], 190 | "name": "author", 191 | "title": "Author", 192 | "type": "document", 193 | }, 194 | Object { 195 | "fields": Array [ 196 | Object { 197 | "name": "title", 198 | "title": "Title", 199 | "type": "string", 200 | }, 201 | Object { 202 | "name": "authors", 203 | "of": Array [ 204 | Object { 205 | "to": Array [ 206 | Object { 207 | "type": "author", 208 | }, 209 | ], 210 | "type": "reference", 211 | }, 212 | ], 213 | "title": "Authors", 214 | "type": "array", 215 | }, 216 | ], 217 | "name": "book", 218 | "title": "Book", 219 | "type": "document", 220 | }, 221 | ] 222 | `; 223 | 224 | exports[`06-reference-array-non-nullable 1`] = ` 225 | Array [ 226 | Object { 227 | "fields": Array [ 228 | Object { 229 | "name": "name", 230 | "title": "Name", 231 | "type": "string", 232 | "validation": [Function], 233 | }, 234 | ], 235 | "name": "author", 236 | "title": "Author", 237 | "type": "document", 238 | }, 239 | Object { 240 | "fields": Array [ 241 | Object { 242 | "name": "title", 243 | "title": "Title", 244 | "type": "string", 245 | }, 246 | Object { 247 | "name": "authors", 248 | "of": Array [ 249 | Object { 250 | "to": Array [ 251 | Object { 252 | "type": "author", 253 | }, 254 | ], 255 | "type": "reference", 256 | }, 257 | ], 258 | "title": "Authors", 259 | "type": "array", 260 | "validation": [Function], 261 | }, 262 | ], 263 | "name": "book", 264 | "title": "Book", 265 | "type": "document", 266 | }, 267 | ] 268 | `; 269 | 270 | exports[`07-directives 1`] = ` 271 | Array [ 272 | Object { 273 | "fields": Array [ 274 | Object { 275 | "name": "name", 276 | "title": "Name", 277 | "type": "string", 278 | "validation": [Function], 279 | }, 280 | Object { 281 | "hidden": true, 282 | "name": "birthDate", 283 | "title": "Birth date", 284 | "type": "date", 285 | }, 286 | ], 287 | "name": "author", 288 | "title": "Author", 289 | "type": "document", 290 | }, 291 | Object { 292 | "fields": Array [ 293 | Object { 294 | "name": "title", 295 | "title": "Title", 296 | "type": "string", 297 | }, 298 | Object { 299 | "name": "author", 300 | "title": "Author", 301 | "to": Array [ 302 | Object { 303 | "type": "author", 304 | }, 305 | ], 306 | "type": "reference", 307 | }, 308 | Object { 309 | "name": "tags", 310 | "of": Array [ 311 | Object { 312 | "type": "string", 313 | }, 314 | ], 315 | "options": Object { 316 | "layout": "tags", 317 | }, 318 | "title": "Tags", 319 | "type": "array", 320 | "validation": [Function], 321 | }, 322 | ], 323 | "name": "book", 324 | "title": "Book", 325 | "type": "document", 326 | }, 327 | ] 328 | `; 329 | 330 | exports[`08-inline-document 1`] = ` 331 | Array [ 332 | Object { 333 | "fields": Array [ 334 | Object { 335 | "name": "name", 336 | "title": "Name", 337 | "type": "string", 338 | "validation": [Function], 339 | }, 340 | ], 341 | "name": "author", 342 | "title": "Author", 343 | "type": "document", 344 | }, 345 | Object { 346 | "fields": Array [ 347 | Object { 348 | "name": "title", 349 | "title": "Title", 350 | "type": "string", 351 | }, 352 | Object { 353 | "name": "author", 354 | "title": "Author", 355 | "type": "author", 356 | "validation": [Function], 357 | }, 358 | ], 359 | "name": "book", 360 | "title": "Book", 361 | "type": "document", 362 | }, 363 | ] 364 | `; 365 | 366 | exports[`09-inline-document-array 1`] = ` 367 | Array [ 368 | Object { 369 | "fields": Array [ 370 | Object { 371 | "name": "name", 372 | "title": "Name", 373 | "type": "string", 374 | "validation": [Function], 375 | }, 376 | ], 377 | "name": "author", 378 | "title": "Author", 379 | "type": "document", 380 | }, 381 | Object { 382 | "fields": Array [ 383 | Object { 384 | "name": "title", 385 | "title": "Title", 386 | "type": "string", 387 | }, 388 | Object { 389 | "name": "authors", 390 | "of": Array [ 391 | Object { 392 | "type": "author", 393 | }, 394 | ], 395 | "title": "Authors", 396 | "type": "array", 397 | }, 398 | ], 399 | "name": "book", 400 | "title": "Book", 401 | "type": "document", 402 | }, 403 | ] 404 | `; 405 | 406 | exports[`10-unions 1`] = ` 407 | Array [ 408 | Object { 409 | "fields": Array [ 410 | Object { 411 | "name": "name", 412 | "title": "Name", 413 | "type": "string", 414 | "validation": [Function], 415 | }, 416 | Object { 417 | "name": "joined", 418 | "title": "Joined", 419 | "type": "date", 420 | }, 421 | ], 422 | "name": "reviewer", 423 | "title": "Reviewer", 424 | "type": "document", 425 | }, 426 | Object { 427 | "fields": Array [ 428 | Object { 429 | "name": "name", 430 | "title": "Name", 431 | "type": "string", 432 | "validation": [Function], 433 | }, 434 | Object { 435 | "name": "firstPublished", 436 | "title": "First published", 437 | "type": "date", 438 | }, 439 | ], 440 | "name": "author", 441 | "title": "Author", 442 | "type": "document", 443 | }, 444 | Object { 445 | "fields": Array [ 446 | Object { 447 | "name": "title", 448 | "title": "Title", 449 | "type": "string", 450 | }, 451 | Object { 452 | "name": "author", 453 | "title": "Author", 454 | "to": Array [ 455 | Object { 456 | "type": "author", 457 | }, 458 | ], 459 | "type": "reference", 460 | }, 461 | Object { 462 | "name": "reviewer", 463 | "title": "Reviewer", 464 | "to": Array [ 465 | Object { 466 | "type": "reviewer", 467 | }, 468 | Object { 469 | "type": "author", 470 | }, 471 | ], 472 | "type": "reference", 473 | }, 474 | Object { 475 | "name": "descisionMaker", 476 | "title": "Descision maker", 477 | "to": Array [ 478 | Object { 479 | "type": "reviewer", 480 | }, 481 | Object { 482 | "type": "author", 483 | }, 484 | ], 485 | "type": "reference", 486 | "validation": [Function], 487 | }, 488 | ], 489 | "name": "book", 490 | "title": "Book", 491 | "type": "document", 492 | }, 493 | ] 494 | `; 495 | 496 | exports[`11-union-array 1`] = ` 497 | Array [ 498 | Object { 499 | "fields": Array [ 500 | Object { 501 | "name": "name", 502 | "title": "Name", 503 | "type": "string", 504 | "validation": [Function], 505 | }, 506 | Object { 507 | "name": "joined", 508 | "title": "Joined", 509 | "type": "date", 510 | }, 511 | ], 512 | "name": "reviewer", 513 | "title": "Reviewer", 514 | "type": "document", 515 | }, 516 | Object { 517 | "fields": Array [ 518 | Object { 519 | "name": "name", 520 | "title": "Name", 521 | "type": "string", 522 | "validation": [Function], 523 | }, 524 | Object { 525 | "name": "firstPublished", 526 | "title": "First published", 527 | "type": "date", 528 | }, 529 | ], 530 | "name": "author", 531 | "title": "Author", 532 | "type": "document", 533 | }, 534 | Object { 535 | "fields": Array [ 536 | Object { 537 | "name": "title", 538 | "title": "Title", 539 | "type": "string", 540 | }, 541 | Object { 542 | "name": "author", 543 | "title": "Author", 544 | "to": Array [ 545 | Object { 546 | "type": "author", 547 | }, 548 | ], 549 | "type": "reference", 550 | }, 551 | Object { 552 | "name": "reviewers", 553 | "of": Array [ 554 | Object { 555 | "to": Array [ 556 | Object { 557 | "type": "reviewer", 558 | }, 559 | Object { 560 | "type": "author", 561 | }, 562 | ], 563 | "type": "reference", 564 | }, 565 | ], 566 | "title": "Reviewers", 567 | "type": "array", 568 | "validation": [Function], 569 | }, 570 | ], 571 | "name": "book", 572 | "title": "Book", 573 | "type": "document", 574 | }, 575 | ] 576 | `; 577 | 578 | exports[`12-union-of-unions 1`] = ` 579 | Array [ 580 | Object { 581 | "fields": Array [ 582 | Object { 583 | "name": "name", 584 | "title": "Name", 585 | "type": "string", 586 | "validation": [Function], 587 | }, 588 | Object { 589 | "name": "joined", 590 | "title": "Joined", 591 | "type": "date", 592 | }, 593 | ], 594 | "name": "reviewer", 595 | "title": "Reviewer", 596 | "type": "document", 597 | }, 598 | Object { 599 | "fields": Array [ 600 | Object { 601 | "name": "name", 602 | "title": "Name", 603 | "type": "string", 604 | "validation": [Function], 605 | }, 606 | Object { 607 | "name": "firstPublished", 608 | "title": "First published", 609 | "type": "date", 610 | }, 611 | ], 612 | "name": "author", 613 | "title": "Author", 614 | "type": "document", 615 | }, 616 | Object { 617 | "fields": Array [ 618 | Object { 619 | "name": "title", 620 | "title": "Title", 621 | "type": "string", 622 | }, 623 | Object { 624 | "name": "author", 625 | "title": "Author", 626 | "to": Array [ 627 | Object { 628 | "type": "author", 629 | }, 630 | ], 631 | "type": "reference", 632 | }, 633 | Object { 634 | "name": "reviewer", 635 | "title": "Reviewer", 636 | "to": Array [ 637 | Object { 638 | "type": "reviewer", 639 | }, 640 | Object { 641 | "type": "author", 642 | }, 643 | ], 644 | "type": "reference", 645 | }, 646 | ], 647 | "name": "book", 648 | "title": "Book", 649 | "type": "document", 650 | }, 651 | Object { 652 | "fields": Array [ 653 | Object { 654 | "name": "title", 655 | "title": "Title", 656 | "type": "string", 657 | }, 658 | Object { 659 | "name": "attachedTo", 660 | "title": "Attached to", 661 | "to": Array [ 662 | Object { 663 | "type": "reviewer", 664 | }, 665 | Object { 666 | "type": "author", 667 | }, 668 | Object { 669 | "type": "book", 670 | }, 671 | Object { 672 | "type": "magazine", 673 | }, 674 | ], 675 | "type": "reference", 676 | }, 677 | ], 678 | "name": "magazine", 679 | "title": "Magazine", 680 | "type": "document", 681 | }, 682 | ] 683 | `; 684 | 685 | exports[`13-union-of-union-array 1`] = ` 686 | Array [ 687 | Object { 688 | "fields": Array [ 689 | Object { 690 | "name": "name", 691 | "title": "Name", 692 | "type": "string", 693 | "validation": [Function], 694 | }, 695 | Object { 696 | "name": "joined", 697 | "title": "Joined", 698 | "type": "date", 699 | }, 700 | ], 701 | "name": "reviewer", 702 | "title": "Reviewer", 703 | "type": "document", 704 | }, 705 | Object { 706 | "fields": Array [ 707 | Object { 708 | "name": "name", 709 | "title": "Name", 710 | "type": "string", 711 | "validation": [Function], 712 | }, 713 | Object { 714 | "name": "firstPublished", 715 | "title": "First published", 716 | "type": "date", 717 | }, 718 | ], 719 | "name": "author", 720 | "title": "Author", 721 | "type": "document", 722 | }, 723 | Object { 724 | "fields": Array [ 725 | Object { 726 | "name": "title", 727 | "title": "Title", 728 | "type": "string", 729 | }, 730 | Object { 731 | "name": "author", 732 | "title": "Author", 733 | "to": Array [ 734 | Object { 735 | "type": "author", 736 | }, 737 | ], 738 | "type": "reference", 739 | }, 740 | Object { 741 | "name": "reviewer", 742 | "title": "Reviewer", 743 | "to": Array [ 744 | Object { 745 | "type": "reviewer", 746 | }, 747 | Object { 748 | "type": "author", 749 | }, 750 | ], 751 | "type": "reference", 752 | }, 753 | ], 754 | "name": "book", 755 | "title": "Book", 756 | "type": "document", 757 | }, 758 | Object { 759 | "fields": Array [ 760 | Object { 761 | "name": "title", 762 | "title": "Title", 763 | "type": "string", 764 | }, 765 | Object { 766 | "name": "attachedTo", 767 | "of": Array [ 768 | Object { 769 | "to": Array [ 770 | Object { 771 | "type": "reviewer", 772 | }, 773 | Object { 774 | "type": "author", 775 | }, 776 | Object { 777 | "type": "book", 778 | }, 779 | Object { 780 | "type": "magazine", 781 | }, 782 | ], 783 | "type": "reference", 784 | }, 785 | ], 786 | "title": "Attached to", 787 | "type": "array", 788 | }, 789 | ], 790 | "name": "magazine", 791 | "title": "Magazine", 792 | "type": "document", 793 | }, 794 | ] 795 | `; 796 | 797 | exports[`14-reference-and-inline-array 1`] = ` 798 | Array [ 799 | Object { 800 | "fields": Array [ 801 | Object { 802 | "name": "name", 803 | "title": "Name", 804 | "type": "string", 805 | "validation": [Function], 806 | }, 807 | ], 808 | "name": "author", 809 | "title": "Author", 810 | "type": "document", 811 | }, 812 | Object { 813 | "fields": Array [ 814 | Object { 815 | "name": "caption", 816 | "title": "Caption", 817 | "type": "string", 818 | }, 819 | Object { 820 | "name": "url", 821 | "title": "Url", 822 | "type": "url", 823 | }, 824 | ], 825 | "name": "externalImage", 826 | "title": "External image", 827 | "type": "object", 828 | }, 829 | Object { 830 | "fields": Array [ 831 | Object { 832 | "name": "title", 833 | "title": "Title", 834 | "type": "string", 835 | }, 836 | Object { 837 | "name": "author", 838 | "title": "Author", 839 | "to": Array [ 840 | Object { 841 | "type": "author", 842 | }, 843 | ], 844 | "type": "reference", 845 | "validation": [Function], 846 | }, 847 | Object { 848 | "name": "description", 849 | "of": Array [ 850 | Object { 851 | "to": Array [ 852 | Object { 853 | "type": "author", 854 | }, 855 | ], 856 | "type": "reference", 857 | }, 858 | Object { 859 | "type": "externalImage", 860 | }, 861 | ], 862 | "title": "Description", 863 | "type": "array", 864 | }, 865 | ], 866 | "name": "book", 867 | "title": "Book", 868 | "type": "document", 869 | }, 870 | ] 871 | `; 872 | 873 | exports[`15-forced-inline-array 1`] = ` 874 | Array [ 875 | Object { 876 | "fields": Array [ 877 | Object { 878 | "name": "name", 879 | "title": "Name", 880 | "type": "string", 881 | "validation": [Function], 882 | }, 883 | ], 884 | "name": "author", 885 | "title": "Author", 886 | "type": "document", 887 | }, 888 | Object { 889 | "fields": Array [ 890 | Object { 891 | "name": "caption", 892 | "title": "Caption", 893 | "type": "string", 894 | }, 895 | Object { 896 | "name": "url", 897 | "title": "Url", 898 | "type": "url", 899 | }, 900 | ], 901 | "name": "externalImage", 902 | "title": "External image", 903 | "type": "object", 904 | }, 905 | Object { 906 | "fields": Array [ 907 | Object { 908 | "name": "title", 909 | "title": "Title", 910 | "type": "string", 911 | }, 912 | Object { 913 | "name": "author", 914 | "title": "Author", 915 | "to": Array [ 916 | Object { 917 | "type": "author", 918 | }, 919 | ], 920 | "type": "reference", 921 | "validation": [Function], 922 | }, 923 | Object { 924 | "name": "description", 925 | "of": Array [ 926 | Object { 927 | "type": "author", 928 | }, 929 | Object { 930 | "type": "externalImage", 931 | }, 932 | ], 933 | "title": "Description", 934 | "type": "array", 935 | }, 936 | ], 937 | "name": "book", 938 | "title": "Book", 939 | "type": "document", 940 | }, 941 | ] 942 | `; 943 | 944 | exports[`16-error-inline-union 1`] = ` 945 | Array [ 946 | Object { 947 | "fields": Array [ 948 | Object { 949 | "name": "name", 950 | "title": "Name", 951 | "type": "string", 952 | "validation": [Function], 953 | }, 954 | ], 955 | "name": "author", 956 | "title": "Author", 957 | "type": "object", 958 | }, 959 | Object { 960 | "fields": Array [ 961 | Object { 962 | "name": "caption", 963 | "title": "Caption", 964 | "type": "string", 965 | }, 966 | Object { 967 | "name": "url", 968 | "title": "Url", 969 | "type": "url", 970 | }, 971 | ], 972 | "name": "externalImage", 973 | "title": "External image", 974 | "type": "object", 975 | }, 976 | Object { 977 | "fields": Array [ 978 | Object { 979 | "name": "title", 980 | "title": "Title", 981 | "type": "string", 982 | }, 983 | Object { 984 | "name": "author", 985 | "title": "Author", 986 | "type": "author", 987 | "validation": [Function], 988 | }, 989 | Object { 990 | "_problems": Array [ 991 | Object { 992 | "message": "Field \\"description\\" on type \\"Book\\" cannot be an inline union (Author, ExternalImage are not document types and therefore must be inlined)", 993 | "severity": "error", 994 | }, 995 | ], 996 | "name": "description", 997 | "title": "Description", 998 | }, 999 | ], 1000 | "name": "book", 1001 | "title": "Book", 1002 | "type": "document", 1003 | }, 1004 | ] 1005 | `; 1006 | 1007 | exports[`17-error-non-ref-union-field 1`] = ` 1008 | Array [ 1009 | Object { 1010 | "fields": Array [ 1011 | Object { 1012 | "name": "name", 1013 | "title": "Name", 1014 | "type": "string", 1015 | "validation": [Function], 1016 | }, 1017 | ], 1018 | "name": "author", 1019 | "title": "Author", 1020 | "type": "document", 1021 | }, 1022 | Object { 1023 | "fields": Array [ 1024 | Object { 1025 | "name": "caption", 1026 | "title": "Caption", 1027 | "type": "string", 1028 | }, 1029 | Object { 1030 | "name": "url", 1031 | "title": "Url", 1032 | "type": "url", 1033 | }, 1034 | ], 1035 | "name": "externalImage", 1036 | "title": "External image", 1037 | "type": "object", 1038 | }, 1039 | Object { 1040 | "fields": Array [ 1041 | Object { 1042 | "name": "title", 1043 | "title": "Title", 1044 | "type": "string", 1045 | }, 1046 | Object { 1047 | "name": "author", 1048 | "title": "Author", 1049 | "to": Array [ 1050 | Object { 1051 | "type": "author", 1052 | }, 1053 | ], 1054 | "type": "reference", 1055 | "validation": [Function], 1056 | }, 1057 | Object { 1058 | "_problems": Array [ 1059 | Object { 1060 | "message": "Field \\"description\\" on type \\"Book\\" cannot be an inline union (ExternalImage is not a document type and therefore must be inlined)", 1061 | "severity": "error", 1062 | }, 1063 | ], 1064 | "name": "description", 1065 | "title": "Description", 1066 | }, 1067 | ], 1068 | "name": "book", 1069 | "title": "Book", 1070 | "type": "document", 1071 | }, 1072 | ] 1073 | `; 1074 | 1075 | exports[`18-manual-slug 1`] = ` 1076 | Array [ 1077 | Object { 1078 | "fields": Array [ 1079 | Object { 1080 | "name": "name", 1081 | "title": "Name", 1082 | "type": "string", 1083 | "validation": [Function], 1084 | }, 1085 | Object { 1086 | "name": "description", 1087 | "title": "Description", 1088 | "type": "string", 1089 | }, 1090 | Object { 1091 | "name": "slug", 1092 | "options": Object { 1093 | "maxLength": 200, 1094 | "source": "description", 1095 | }, 1096 | "title": "Slug", 1097 | "type": "slug", 1098 | }, 1099 | ], 1100 | "name": "author", 1101 | "title": "Author", 1102 | "type": "object", 1103 | }, 1104 | ] 1105 | `; 1106 | 1107 | exports[`19-auto-slug 1`] = ` 1108 | Array [ 1109 | Object { 1110 | "fields": Array [ 1111 | Object { 1112 | "name": "num", 1113 | "title": "Num", 1114 | "type": "number", 1115 | }, 1116 | Object { 1117 | "name": "name", 1118 | "title": "Name", 1119 | "type": "string", 1120 | "validation": [Function], 1121 | }, 1122 | Object { 1123 | "name": "slug", 1124 | "options": Object { 1125 | "maxLength": 200, 1126 | "source": "name", 1127 | }, 1128 | "title": "Slug", 1129 | "type": "slug", 1130 | }, 1131 | ], 1132 | "name": "author", 1133 | "title": "Author", 1134 | "type": "object", 1135 | }, 1136 | ] 1137 | `; 1138 | 1139 | exports[`99-full 1`] = ` 1140 | Array [ 1141 | Object { 1142 | "description": "A blog post 1143 | 1144 | It can contain newlines", 1145 | "fields": Array [ 1146 | Object { 1147 | "name": "title", 1148 | "title": "Title", 1149 | "type": "string", 1150 | }, 1151 | Object { 1152 | "name": "leadImage", 1153 | "options": Object { 1154 | "hotspot": true, 1155 | }, 1156 | "title": "Lead image", 1157 | "type": "captionedImage", 1158 | }, 1159 | Object { 1160 | "name": "author", 1161 | "title": "Author", 1162 | "to": Array [ 1163 | Object { 1164 | "type": "author", 1165 | }, 1166 | ], 1167 | "type": "reference", 1168 | "validation": [Function], 1169 | }, 1170 | Object { 1171 | "name": "body", 1172 | "title": "Body", 1173 | "type": "text", 1174 | }, 1175 | Object { 1176 | "name": "tags", 1177 | "of": Array [ 1178 | Object { 1179 | "type": "string", 1180 | }, 1181 | ], 1182 | "options": Object { 1183 | "layout": "tags", 1184 | }, 1185 | "title": "Tags", 1186 | "type": "array", 1187 | }, 1188 | Object { 1189 | "name": "status", 1190 | "options": Object { 1191 | "layout": "radio", 1192 | "list": Array [ 1193 | Object { 1194 | "name": "active", 1195 | }, 1196 | Object { 1197 | "name": "disabled", 1198 | }, 1199 | ], 1200 | }, 1201 | "title": "Status", 1202 | "type": "string", 1203 | }, 1204 | Object { 1205 | "hidden": true, 1206 | "name": "views", 1207 | "readOnly": true, 1208 | "title": "Views", 1209 | "type": "number", 1210 | "validation": [Function], 1211 | }, 1212 | Object { 1213 | "description": "Comments are sent from the frontend", 1214 | "name": "comments", 1215 | "of": Array [ 1216 | Object { 1217 | "to": Array [ 1218 | Object { 1219 | "type": "comment", 1220 | }, 1221 | ], 1222 | "type": "reference", 1223 | }, 1224 | ], 1225 | "title": "User comments", 1226 | "type": "array", 1227 | }, 1228 | ], 1229 | "name": "post", 1230 | "title": "Blog post", 1231 | "type": "document", 1232 | }, 1233 | Object { 1234 | "fields": Array [ 1235 | Object { 1236 | "name": "name", 1237 | "title": "Name", 1238 | "type": "string", 1239 | }, 1240 | Object { 1241 | "name": "profileImage", 1242 | "title": "Profile image", 1243 | "type": "image", 1244 | }, 1245 | ], 1246 | "name": "author", 1247 | "orderings": Array [ 1248 | Object { 1249 | "by": Array [ 1250 | Object { 1251 | "direction": "desc", 1252 | "field": "releaseDate.utc", 1253 | }, 1254 | ], 1255 | "name": "releaseDateDesc", 1256 | "title": "Release Date", 1257 | }, 1258 | ], 1259 | "title": "Author", 1260 | "type": "document", 1261 | }, 1262 | Object { 1263 | "fields": Array [ 1264 | Object { 1265 | "name": "name", 1266 | "title": "Name", 1267 | "type": "string", 1268 | }, 1269 | ], 1270 | "name": "user", 1271 | "title": "User", 1272 | "type": "document", 1273 | }, 1274 | Object { 1275 | "fields": Array [ 1276 | Object { 1277 | "name": "caption", 1278 | "title": "Caption", 1279 | "type": "string", 1280 | }, 1281 | Object { 1282 | "name": "uploadedBy", 1283 | "title": "Uploaded by", 1284 | "type": "author", 1285 | }, 1286 | ], 1287 | "name": "captionedImage", 1288 | "title": "Captioned image", 1289 | "type": "image", 1290 | }, 1291 | Object { 1292 | "fields": Array [ 1293 | Object { 1294 | "description": "Author of this comment", 1295 | "name": "author", 1296 | "title": "Author", 1297 | "to": Array [ 1298 | Object { 1299 | "type": "author", 1300 | }, 1301 | ], 1302 | "type": "reference", 1303 | "validation": [Function], 1304 | }, 1305 | Object { 1306 | "name": "text", 1307 | "title": "Text", 1308 | "type": "string", 1309 | }, 1310 | Object { 1311 | "fieldset": "stats", 1312 | "name": "likes", 1313 | "title": "Likes", 1314 | "type": "number", 1315 | "validation": [Function], 1316 | }, 1317 | Object { 1318 | "name": "likedBy", 1319 | "of": Array [ 1320 | Object { 1321 | "to": Array [ 1322 | Object { 1323 | "type": "user", 1324 | }, 1325 | ], 1326 | "type": "reference", 1327 | }, 1328 | ], 1329 | "title": "Liked by", 1330 | "type": "array", 1331 | }, 1332 | ], 1333 | "fieldsets": Array [ 1334 | Object { 1335 | "name": "stats", 1336 | "title": "Statistics", 1337 | }, 1338 | ], 1339 | "name": "comment", 1340 | "title": "Comment", 1341 | "type": "document", 1342 | }, 1343 | ] 1344 | `; 1345 | --------------------------------------------------------------------------------