├── README.md ├── examples ├── blog │ ├── .npmrc │ ├── gatsby-browser.js │ ├── src │ │ ├── styles │ │ │ └── global.css │ │ └── pages │ │ │ └── index.js │ ├── postcss.config.js │ ├── tailwind.config.js │ ├── gatsby-config.js │ └── package.json ├── cdrs │ ├── .prettierrc │ ├── .prettierignore │ ├── README.md │ ├── gatsby-config.js │ ├── .gitignore │ ├── package.json │ └── test │ │ └── graphqlReferences.test.js └── shared-content-studio │ ├── .eslintrc │ ├── README.md │ ├── static │ └── .gitkeep │ ├── shared.tar.gz │ ├── production.tar.gz │ ├── sanity.cli.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── sanity.config.ts │ └── production.graphql ├── packages └── gatsby-source-sanity │ ├── .eslintignore │ ├── index.js │ ├── src │ ├── debug.ts │ ├── types │ │ ├── pumpify.ts │ │ ├── sanityMutator.d.ts │ │ ├── gatsby.ts │ │ └── sanity.ts │ ├── util │ │ ├── getPluginStatus.ts │ │ ├── removeSystemDocuments.ts │ │ ├── cache.ts │ │ ├── handleDrafts.ts │ │ ├── removeGatsbyInternalProps.ts │ │ ├── documentIds.ts │ │ ├── rejectOnApiError.ts │ │ ├── getAllDocuments.ts │ │ ├── downloadDocuments.ts │ │ ├── getDocumentStream.ts │ │ ├── validateConfig.ts │ │ ├── handleDeltaChanges.ts │ │ ├── createNodeManifest.ts │ │ ├── errors.ts │ │ ├── handleWebhookEvent.ts │ │ ├── resolveReferences.ts │ │ ├── getGraphQLResolverMap.ts │ │ ├── getSyncWithGatsby.ts │ │ ├── mapCrossDatasetReferences.ts │ │ ├── remoteGraphQLSchema.ts │ │ ├── normalize.ts │ │ └── rewriteGraphQLSchema.ts │ ├── index.ts │ ├── gatsby-ssr.ts │ ├── images │ │ ├── extendImageNode.ts │ │ └── getGatsbyImageProps.ts │ └── gatsby-node.ts │ ├── jest.config.js │ ├── test │ ├── fixtures │ │ └── circularTypes.graphql │ ├── handleWebhookEvent.test.ts │ ├── getGatsbyImageProps.test.ts │ ├── resolveReferences.test.ts │ └── __snapshots__ │ │ └── getGatsbyImageProps.test.ts.snap │ ├── .eslintrc │ ├── gatsby-node.js │ ├── gatsby-ssr.js │ ├── tsconfig.json │ ├── LICENSE │ ├── package.json │ ├── MIGRATION.md │ └── README.md ├── .prettierrc ├── .gitignore ├── .editorconfig ├── package.json └── .github ├── renovate.json └── workflows ├── release.yml └── main.yml /README.md: -------------------------------------------------------------------------------- 1 | packages/gatsby-source-sanity/README.md -------------------------------------------------------------------------------- /examples/blog/.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /examples/blog/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import './src/styles/global.css' 2 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/.eslintignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /lib-es5 3 | -------------------------------------------------------------------------------- /examples/cdrs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /examples/cdrs/.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /examples/shared-content-studio/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/eslint-config-studio" 3 | } 4 | -------------------------------------------------------------------------------- /examples/blog/src/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/index.js: -------------------------------------------------------------------------------- 1 | // Browser/ES5 targets 2 | module.exports = require('./lib-es5') 3 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/debug.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug' 2 | 3 | export default debug('sanity') 4 | -------------------------------------------------------------------------------- /examples/shared-content-studio/README.md: -------------------------------------------------------------------------------- 1 | This Sanity Studio is for content models and content for the shared-content example app 2 | -------------------------------------------------------------------------------- /examples/shared-content-studio/static/.gitkeep: -------------------------------------------------------------------------------- 1 | Files placed here will be served by the Sanity server under the `/static`-prefix 2 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | } 5 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/test/fixtures/circularTypes.graphql: -------------------------------------------------------------------------------- 1 | type Author { 2 | title: String 3 | friends: [Author] 4 | } 5 | -------------------------------------------------------------------------------- /examples/blog/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": ["sanity", "prettier"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/gatsby-node.js: -------------------------------------------------------------------------------- 1 | // Proxy to TypeScript-compiled output 2 | module.exports = require('./lib/gatsby-node') 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "trailingComma": "all", 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /examples/shared-content-studio/shared.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/gatsby-source-sanity/HEAD/examples/shared-content-studio/shared.tar.gz -------------------------------------------------------------------------------- /examples/shared-content-studio/production.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/gatsby-source-sanity/HEAD/examples/shared-content-studio/production.tar.gz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .tarballs 3 | node_modules 4 | lib/ 5 | lib-es5/ 6 | yarn.lock 7 | coverage 8 | .cache 9 | public 10 | .vercel 11 | .vscode 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/types/pumpify.ts: -------------------------------------------------------------------------------- 1 | declare module 'pumpify' { 2 | import {Stream, Duplex} from 'stream' 3 | export const obj: (...streams: Stream[]) => Duplex 4 | } 5 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/getPluginStatus.ts: -------------------------------------------------------------------------------- 1 | import {SourceNodesArgs} from 'gatsby' 2 | 3 | export default function getPluginStatus(args: SourceNodesArgs) { 4 | return args.store.getState().status.plugins?.[`gatsby-source-sanity`] 5 | } 6 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/index.ts: -------------------------------------------------------------------------------- 1 | export const pkgName = 'gatsby-source-sanity' 2 | 3 | export {ImageFormat, getGatsbyImageData, GatsbyImageDataArgs} from './images/getGatsbyImageProps' 4 | export {resolveReferences} from './util/resolveReferences' 5 | -------------------------------------------------------------------------------- /examples/blog/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/pages/**/*.{js,jsx,ts,tsx}', './src/components/**/*.{js,jsx,ts,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } 9 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/types/sanityMutator.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@sanity/mutator' { 2 | interface Match { 3 | path: string[] 4 | value: any 5 | } 6 | 7 | export function extractWithPath(path: string, doc: {[key: string]: any}): Match[] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | // Proxy to TypeScript-compiled output. 2 | // Note that unlike gatsby-node.js, we need to explicitly define the exported hooks 3 | // as they seem to be statically analyzed at build time. 4 | const ssr = require('./lib/gatsby-ssr') 5 | 6 | exports.onRenderBody = ssr.onRenderBody 7 | -------------------------------------------------------------------------------- /examples/cdrs/README.md: -------------------------------------------------------------------------------- 1 | This is a test Gatsby app for end-to-end testing the gatsby-source-sanity plugin with Sanity Content Lake data and schema 2 | 3 | # Testing 4 | 5 | Manually run the application 6 | 7 | ```bash 8 | rm -rf .cache public && npm run develop 9 | ``` 10 | 11 | Run the test suite 12 | 13 | ```bash 14 | npm test 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/shared-content-studio/sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import {defineCliConfig} from 'sanity/cli' 2 | 3 | export default defineCliConfig({ 4 | api: { 5 | projectId: 'rz9j51w2', 6 | dataset: 'production', 7 | }, 8 | graphql: [ 9 | { 10 | id: 'books', 11 | workspace: 'books', 12 | }, 13 | { 14 | id: 'authors', 15 | workspace: 'authors', 16 | }, 17 | ], 18 | }) 19 | -------------------------------------------------------------------------------- /examples/blog/gatsby-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('gatsby').GatsbyConfig} 3 | */ 4 | module.exports = { 5 | plugins: [ 6 | 'gatsby-plugin-postcss', 7 | { 8 | resolve: 'gatsby-source-sanity', 9 | options: { 10 | // https://pv8y60vp.api.sanity.io/v1/graphql/production/default 11 | projectId: 'pv8y60vp', 12 | dataset: 'production', 13 | }, 14 | }, 15 | `gatsby-plugin-image`, 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/removeSystemDocuments.ts: -------------------------------------------------------------------------------- 1 | import * as through from 'through2' 2 | import {SanityDocument} from '../types/sanity' 3 | 4 | function filter(doc: SanityDocument, enc: string, callback: through.TransformCallback) { 5 | if (doc && doc._id && doc._id.startsWith('_.')) { 6 | return callback() 7 | } 8 | 9 | return callback(null, doc) 10 | } 11 | 12 | export const removeSystemDocuments = () => through.obj(filter) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*", 5 | "examples/*" 6 | ], 7 | "scripts": { 8 | "build": "npm run build -w gatsby-source-sanity", 9 | "format": "npm run format -w gatsby-source-sanity", 10 | "prepublishOnly": "npm run prepublishOnly -w gatsby-source-sanity", 11 | "release": "npm run release -w gatsby-source-sanity", 12 | "test": "npm run test -w gatsby-source-sanity" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/gatsby-ssr.ts: -------------------------------------------------------------------------------- 1 | import {createElement} from 'react' 2 | import {GatsbySSR, RenderBodyArgs} from 'gatsby' 3 | 4 | export const onRenderBody: GatsbySSR['onRenderBody'] = ({ 5 | setHeadComponents, 6 | }: RenderBodyArgs): any => { 7 | setHeadComponents([ 8 | createElement('link', { 9 | rel: 'preconnect', 10 | key: 'sanity-cdn-preconnect', 11 | href: 'https://cdn.sanity.io', 12 | }), 13 | ]) 14 | } 15 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/cache.ts: -------------------------------------------------------------------------------- 1 | import {PluginConfig} from './validateConfig' 2 | 3 | export type StateCache = { 4 | [key: string]: any 5 | } 6 | 7 | export enum CACHE_KEYS { 8 | TYPE_MAP = 'typeMap', 9 | GRAPHQL_SDL = 'graphqlSdl', 10 | IMAGE_EXTENSIONS = 'imageExt', 11 | LAST_BUILD = 'lastBuildTime', 12 | } 13 | 14 | export function getCacheKey(config: PluginConfig, suffix: CACHE_KEYS) { 15 | return `${config.projectId}-${config.dataset}-${config.typePrefix ?? ''}-${suffix}` 16 | } 17 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/handleDrafts.ts: -------------------------------------------------------------------------------- 1 | import * as through from 'through2' 2 | import {SanityDocument} from '../types/sanity' 3 | import {isDraftId} from './documentIds' 4 | 5 | function filter(doc: SanityDocument, enc: string, callback: through.TransformCallback) { 6 | return isDraft(doc) ? callback() : callback(null, doc) 7 | } 8 | 9 | function isDraft(doc: SanityDocument) { 10 | return doc && doc._id && isDraftId(doc._id) 11 | } 12 | 13 | export const removeDrafts = () => through.obj(filter) 14 | -------------------------------------------------------------------------------- /examples/shared-content-studio/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # Compiled Sanity Studio 9 | /dist 10 | 11 | # Temporary Sanity runtime, generated by the CLI on every dev server start 12 | /.sanity 13 | 14 | # Logs 15 | /logs 16 | *.log 17 | 18 | # Coverage directory used by testing tools 19 | /coverage 20 | 21 | # Misc 22 | .DS_Store 23 | *.pem 24 | 25 | # Typescript 26 | *.tsbuildinfo 27 | 28 | # Dotenv and similar local-only files 29 | *.local 30 | -------------------------------------------------------------------------------- /examples/shared-content-studio/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "exclude": ["node_modules", "test/**/*.ts"], 4 | "compilerOptions": { 5 | "strict": true, 6 | "noUnusedLocals": true, 7 | "noImplicitReturns": true, 8 | "module": "commonjs", 9 | "downlevelIteration": true, 10 | "removeComments": false, 11 | "preserveConstEnums": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "sourceMap": true, 15 | "outDir": "./lib", 16 | "resolveJsonModule": true, 17 | "target": "es2017", 18 | "declaration": true, 19 | "lib": ["es2017", "dom", "esnext.asynciterable"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>sanity-io/renovate-config", ":reviewer(team:ecosystem)"], 4 | "packageRules": [ 5 | { 6 | "matchPackageNames": ["gatsby-source-sanity"], 7 | "rangeStrategy": "pin", 8 | "schedule": ["at any time"] 9 | }, 10 | { 11 | "description": "Dependency updates to other package jsons than the root should always use the chore scope as they aren't published to npm", 12 | "matchFileNames": ["examples/blog/package.json"], 13 | "extends": [":semanticCommitTypeAll(chore)"], 14 | "groupSlug": "examples" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/removeGatsbyInternalProps.ts: -------------------------------------------------------------------------------- 1 | import {SanityInputNode, SanityNode} from '../types/gatsby' 2 | 3 | // Gatsby mutates (...tm) the `internal` object, adding `owner`. 4 | // This function helps "clean" the internal representation if we are readding/reusing the node 5 | export const removeGatsbyInternalProps = (node: SanityNode | SanityInputNode): SanityInputNode => { 6 | if (!node || typeof node.internal === 'undefined') { 7 | return node 8 | } 9 | 10 | const {mediaType, type, contentDigest} = node.internal 11 | return { 12 | ...node, 13 | internal: { 14 | mediaType, 15 | type, 16 | contentDigest, 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/documentIds.ts: -------------------------------------------------------------------------------- 1 | export function isDraftId(id: string) { 2 | return id.startsWith('drafts.') 3 | } 4 | 5 | export const prefixId = (id: string) => (id.startsWith('drafts.') ? id : `drafts.${id}`) 6 | 7 | export const unprefixId = (id: string) => id.replace(/^drafts\./, '') 8 | 9 | export const safeId = (id: string, makeSafe: (id: string) => string) => { 10 | return /^(image|file)-[a-z0-9]{32,}-/.test(id) 11 | ? // Use raw IDs for assets as we might use these with asset tooling 12 | id 13 | : // Prefix Gatsbyfied IDs with a dash as it's not allowed in Sanity, 14 | // thus enabling easy checks for Gatsby vs Sanity IDs 15 | `-${makeSafe(id)}` 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | id-token: write # Required for OIDC 10 | contents: read 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: actions/setup-node@v4 18 | with: 19 | cache: npm 20 | node-version: 'lts/*' 21 | registry-url: 'https://registry.npmjs.org' 22 | # Ensure npm 11.5.1 or later is installed 23 | - name: Update npm 24 | run: npm install -g npm@latest 25 | - run: npm ci 26 | - run: npm publish 27 | working-directory: packages/gatsby-source-sanity 28 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/rejectOnApiError.ts: -------------------------------------------------------------------------------- 1 | import * as through from 'through2' 2 | import {SanityDocument, ApiError} from '../types/sanity' 3 | 4 | type DocumentOrApiError = SanityDocument | ApiError 5 | 6 | function filter(sanityDoc: DocumentOrApiError, enc: string, callback: through.TransformCallback) { 7 | const doc = sanityDoc as SanityDocument 8 | if (doc._id && doc._type) { 9 | callback(null, doc) 10 | return 11 | } 12 | 13 | const error = sanityDoc as ApiError 14 | if (error.statusCode && error.error) { 15 | callback(new Error(`${error.statusCode}: ${error.error}`)) 16 | return 17 | } 18 | 19 | callback() 20 | } 21 | 22 | export const rejectOnApiError = () => through.obj(filter) 23 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/types/gatsby.ts: -------------------------------------------------------------------------------- 1 | import {Node, NodeInput} from 'gatsby' 2 | import {GraphQLFieldResolver} from 'gatsby/graphql' 3 | 4 | export interface SanityNode extends Node { 5 | _id: string // Sanity document ID 6 | } 7 | 8 | export interface SanityInputNode extends NodeInput { 9 | _id: string // Sanity document ID 10 | } 11 | 12 | export type GatsbyNodeModel = { 13 | getNodeById: (args: {id: string}) => SanityNode 14 | } 15 | 16 | export type GatsbyGraphQLContext = { 17 | nodeModel: GatsbyNodeModel 18 | } 19 | 20 | export type GatsbyResolverMap = { 21 | [typeName: string]: { 22 | [fieldName: string]: { 23 | type?: string 24 | resolve: GraphQLFieldResolver<{[key: string]: any}, GatsbyGraphQLContext> 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-blog", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "gatsby build", 7 | "clean": "gatsby clean", 8 | "develop": "gatsby develop", 9 | "serve": "gatsby serve", 10 | "start": "gatsby develop" 11 | }, 12 | "dependencies": { 13 | "gatsby": "^5.5.0", 14 | "gatsby-plugin-image": "^3.5.0", 15 | "gatsby-plugin-sharp": "^5.5.0", 16 | "gatsby-source-sanity": "*", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | }, 20 | "devDependencies": { 21 | "autoprefixer": "^10.4.13", 22 | "gatsby-plugin-postcss": "^6.5.0", 23 | "postcss": "^8.4.21", 24 | "tailwindcss": "^3.2.4" 25 | }, 26 | "engines": { 27 | "node": ">= 18" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/types/sanity.ts: -------------------------------------------------------------------------------- 1 | export type SanityDocument = Record> = { 2 | [P in keyof T]: T[P] 3 | } & { 4 | _id: string 5 | _rev: string 6 | _type: string 7 | _createdAt: string 8 | _updatedAt: string 9 | } 10 | 11 | export interface SanityRef { 12 | _ref: string 13 | } 14 | 15 | export interface ApiError { 16 | statusCode: number 17 | error: string 18 | message: string 19 | } 20 | 21 | /** 22 | * Body received only in delete operations. 23 | * All others are handled by handleDeltaWebhook. 24 | */ 25 | export interface SanityWebhookDeleteBody { 26 | operation: 'delete' 27 | documentId: string 28 | projectId?: string 29 | dataset?: string 30 | } 31 | 32 | export type SanityWebhookBody = SanityWebhookDeleteBody | {} 33 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/getAllDocuments.ts: -------------------------------------------------------------------------------- 1 | import split from 'split2' 2 | import {rejectOnApiError} from './rejectOnApiError' 3 | import {getDocumentStream} from './getDocumentStream' 4 | import {removeDrafts} from './handleDrafts' 5 | import * as through from 'through2' 6 | import {removeSystemDocuments} from './removeSystemDocuments' 7 | import pumpify from 'pumpify' 8 | import {Stream} from 'stream' 9 | 10 | export async function getAllDocuments( 11 | url: string, 12 | token?: string, 13 | options: {includeDrafts?: boolean} = {}, 14 | ): Promise { 15 | return pumpify.obj( 16 | await getDocumentStream(url, token), 17 | split(JSON.parse), 18 | options.includeDrafts ? through.obj() : removeDrafts(), 19 | removeSystemDocuments(), 20 | rejectOnApiError(), 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/downloadDocuments.ts: -------------------------------------------------------------------------------- 1 | import {SanityDocument} from '../types/sanity' 2 | import {getAllDocuments} from './getAllDocuments' 3 | 4 | export default function downloadDocuments( 5 | url: string, 6 | token?: string, 7 | options: {includeDrafts?: boolean} = {}, 8 | ): Promise> { 9 | return getAllDocuments(url, token, options).then( 10 | (stream) => 11 | new Promise((resolve, reject) => { 12 | const documents = new Map() 13 | stream.on('data', (doc) => { 14 | documents.set(doc._id, doc) 15 | }) 16 | stream.on('end', () => { 17 | resolve(documents) 18 | }) 19 | stream.on('error', (error) => { 20 | reject(error) 21 | }) 22 | }), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /examples/shared-content-studio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared-content-studio", 3 | "version": "1.0.0", 4 | "private": true, 5 | "keywords": [ 6 | "sanity" 7 | ], 8 | "license": "UNLICENSED", 9 | "main": "package.json", 10 | "scripts": { 11 | "build": "sanity build", 12 | "deploy": "sanity deploy", 13 | "deploy-graphql": "sanity graphql deploy", 14 | "dev": "sanity dev", 15 | "start": "sanity start" 16 | }, 17 | "prettier": { 18 | "bracketSpacing": false, 19 | "printWidth": 100, 20 | "semi": false, 21 | "singleQuote": true 22 | }, 23 | "dependencies": { 24 | "@sanity/vision": "^3.16.0", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-is": "^18.2.0", 28 | "sanity": "^3.16.0", 29 | "styled-components": "^5.3.9" 30 | }, 31 | "devDependencies": { 32 | "@sanity/eslint-config-studio": "^3.0.1", 33 | "@types/react": "^18.0.25", 34 | "@types/styled-components": "^5.1.26", 35 | "eslint": "^8.6.0", 36 | "prettier": "^3.0.2", 37 | "typescript": "^5.1.6" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/test/handleWebhookEvent.test.ts: -------------------------------------------------------------------------------- 1 | import {SanityWebhookDeleteBody} from '../src/types/sanity' 2 | import {validateWebhookPayload} from '../src/util/handleWebhookEvent' 3 | 4 | const DeleteHook: SanityWebhookDeleteBody = { 5 | dataset: 'production', 6 | projectId: 'projectId', 7 | documentId: 'doc', 8 | operation: 'delete', 9 | } 10 | 11 | const DeleteHookFaulty = { 12 | dataset: 'production', 13 | projectId: 'projectId', 14 | } 15 | 16 | const V1Hooks = { 17 | ids: { 18 | created: [], 19 | updated: [], 20 | deleted: [], 21 | }, 22 | } 23 | 24 | test('webhook payload - delete', () => { 25 | expect(validateWebhookPayload(DeleteHook)).toEqual('delete-operation') 26 | }) 27 | 28 | test('webhook payload - delete faulty', () => { 29 | expect(validateWebhookPayload(DeleteHookFaulty)).toEqual(false) 30 | }) 31 | 32 | test('webhook payload - V1', () => { 33 | expect(validateWebhookPayload(V1Hooks)).toEqual(false) 34 | }) 35 | 36 | test('webhook payload - create/update', () => { 37 | expect(validateWebhookPayload({})).toEqual(false) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/getDocumentStream.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import getStream from 'get-stream' 3 | import {Readable} from 'stream' 4 | import {pkgName} from '../index' 5 | 6 | export function getDocumentStream(url: string, token?: string): Promise { 7 | const headers = { 8 | 'User-Agent': `${pkgName}`, 9 | ...(token ? {Authorization: `Bearer ${token}`} : {}), 10 | } 11 | 12 | return axios({ 13 | method: 'get', 14 | responseType: 'stream', 15 | url, 16 | headers, 17 | }) 18 | .then((res) => res.data) 19 | .catch(async (err) => { 20 | if (!err.response || !err.response.data) { 21 | throw err 22 | } 23 | 24 | let error = err 25 | try { 26 | // Try to lift error message out of JSON payload ({error, message, statusCode}) 27 | const data = await getStream(err.response.data) 28 | error = new Error(JSON.parse(data).message) 29 | } catch (jsonErr) { 30 | // Do nothing, throw regular error 31 | } 32 | 33 | throw error 34 | }) as Promise 35 | } 36 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sanity.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/cdrs/gatsby-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configure your Gatsby site with this file. 3 | * 4 | * See: https://www.gatsbyjs.com/docs/reference/config-files/gatsby-config/ 5 | */ 6 | 7 | const path = require("path") 8 | const pluginPath = path.resolve( 9 | __dirname, 10 | "../../packages/gatsby-source-sanity" 11 | ) 12 | 13 | /** 14 | * @type {import('gatsby').GatsbyConfig} 15 | */ 16 | module.exports = { 17 | siteMetadata: { 18 | title: `Gatsby Default Starter`, 19 | description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`, 20 | author: `@gatsbyjs`, 21 | siteUrl: `https://gatsbystarterdefaultsource.gatsbyjs.io/`, 22 | }, 23 | plugins: [ 24 | { 25 | resolve: pluginPath, 26 | options: { 27 | apiHost: "https://api.sanity.work", 28 | projectId: "rz9j51w2", 29 | dataset: "production", 30 | }, 31 | }, 32 | { 33 | resolve: pluginPath, 34 | options: { 35 | apiHost: "https://api.sanity.work", 36 | projectId: "rz9j51w2", 37 | dataset: "shared", 38 | }, 39 | }, 40 | `gatsby-plugin-image`, 41 | ], 42 | } 43 | -------------------------------------------------------------------------------- /examples/cdrs/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: lts/* 20 | cache: npm 21 | - run: npm ci 22 | - run: npm run prepublishOnly 23 | 24 | test: 25 | needs: build 26 | runs-on: ${{ matrix.os }} 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | os: [macos-latest, ubuntu-latest, windows-latest] 31 | node: [lts/*] 32 | include: 33 | - os: ubuntu-latest 34 | node: lts/-2 35 | - os: ubuntu-latest 36 | node: current 37 | steps: 38 | - name: Set git to use LF 39 | if: matrix.os == 'windows-latest' 40 | run: | 41 | git config --global core.autocrlf false 42 | git config --global core.eol lf 43 | - uses: actions/checkout@v5 44 | - uses: actions/setup-node@v4 45 | with: 46 | node-version: ${{ matrix.node }} 47 | cache: npm 48 | - run: npm i 49 | - run: npm test 50 | -------------------------------------------------------------------------------- /examples/cdrs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdrs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "A simple starter to get up and developing quickly with Gatsby", 6 | "keywords": [ 7 | "gatsby" 8 | ], 9 | "bugs": { 10 | "url": "https://github.com/gatsbyjs/gatsby/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 15 | }, 16 | "license": "0BSD", 17 | "author": "Kyle Mathews ", 18 | "scripts": { 19 | "build": "gatsby build", 20 | "clean": "gatsby clean", 21 | "develop": "gatsby develop", 22 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md,css}\"", 23 | "serve": "gatsby serve", 24 | "start": "gatsby develop", 25 | "test": "jest" 26 | }, 27 | "dependencies": { 28 | "gatsby": "^5.12.4", 29 | "gatsby-plugin-image": "^3.12.0", 30 | "gatsby-plugin-manifest": "^5.12.0", 31 | "gatsby-plugin-sharp": "^5.12.0", 32 | "gatsby-source-filesystem": "^5.12.0", 33 | "gatsby-transformer-sharp": "^5.12.0", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0" 36 | }, 37 | "devDependencies": { 38 | "babel-jest": "^29.7.0", 39 | "babel-preset-gatsby": "^3.12.0", 40 | "identity-obj-proxy": "^3.0.0", 41 | "jest": "^29.7.0", 42 | "prettier": "^2.8.8" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/validateConfig.ts: -------------------------------------------------------------------------------- 1 | import {PluginOptions, Reporter} from 'gatsby' 2 | import {prefixId} from './documentIds' 3 | import {ERROR_CODES} from './errors' 4 | 5 | export interface PluginConfig extends PluginOptions { 6 | apiHost?: string 7 | projectId: string 8 | dataset: string 9 | token?: string 10 | version?: string 11 | graphqlTag: string 12 | overlayDrafts?: boolean 13 | watchMode?: boolean 14 | watchModeBuffer?: number 15 | typePrefix?: string 16 | _mocks: { 17 | schemaPath: string 18 | } 19 | } 20 | 21 | export default function validateConfig( 22 | config: Partial, 23 | reporter: Reporter, 24 | ): config is PluginConfig { 25 | if (!config.projectId) { 26 | reporter.panic({ 27 | id: prefixId(ERROR_CODES.MissingProjectId), 28 | context: {sourceMessage: '[sanity] `projectId` must be specified'}, 29 | }) 30 | } 31 | 32 | if (!config.dataset) { 33 | reporter.panic({ 34 | id: prefixId(ERROR_CODES.MissingDataset), 35 | context: {sourceMessage: '[sanity] `dataset` must be specified'}, 36 | }) 37 | } 38 | 39 | if (config.overlayDrafts && !config.token) { 40 | reporter.warn('[sanity] `overlayDrafts` is set to `true`, but no token is given') 41 | } 42 | 43 | const inDevelopMode = process.env.gatsby_executing_command === 'develop' 44 | if (config.watchMode && !inDevelopMode) { 45 | reporter.warn( 46 | '[sanity] Using `watchMode` when not in develop mode might prevent your build from completing', 47 | ) 48 | } 49 | 50 | return true 51 | } 52 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/handleDeltaChanges.ts: -------------------------------------------------------------------------------- 1 | import {SanityClient} from '@sanity/client' 2 | import {SourceNodesArgs} from 'gatsby' 3 | import debug from '../debug' 4 | import {SanityDocument} from '../types/sanity' 5 | import {SyncWithGatsby} from './getSyncWithGatsby' 6 | import {CACHE_KEYS, getCacheKey} from './cache' 7 | import {PluginConfig} from './validateConfig' 8 | 9 | export const sleep = (ms: number) => { 10 | return new Promise((resolve) => setTimeout(resolve, ms)) 11 | } 12 | 13 | // Ensures document changes are persisted to the query engine. 14 | const SLEEP_DURATION = 500 15 | 16 | /** 17 | * Queries all documents changed since last build & adds them to Gatsby's store 18 | */ 19 | export default async function handleDeltaChanges({ 20 | args, 21 | lastBuildTime, 22 | client, 23 | syncWithGatsby, 24 | config, 25 | }: { 26 | args: SourceNodesArgs 27 | lastBuildTime: string 28 | client: SanityClient 29 | syncWithGatsby: SyncWithGatsby 30 | config: PluginConfig 31 | }): Promise { 32 | await sleep(SLEEP_DURATION) 33 | 34 | try { 35 | const changedDocs = await client.fetch( 36 | '*[!(_type match "system.**") && _updatedAt > $timestamp]', 37 | { 38 | timestamp: lastBuildTime, 39 | }, 40 | ) 41 | changedDocs.forEach((doc) => { 42 | syncWithGatsby(doc._id, doc) 43 | }) 44 | await args.cache.set(getCacheKey(config, CACHE_KEYS.LAST_BUILD), new Date().toISOString()) 45 | args.reporter.info(`[sanity] ${changedDocs.length} documents updated.`) 46 | return true 47 | } catch (error) { 48 | debug(`[sanity] failed to handleDeltaChanges`, error) 49 | return false 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/images/extendImageNode.ts: -------------------------------------------------------------------------------- 1 | import {GraphQLEnumType, GraphQLFieldConfigMap} from 'gatsby/graphql' 2 | import {getCacheKey, CACHE_KEYS} from '../util/cache' 3 | import {ImageNode, ImageArgs, getGatsbyImageData} from './getGatsbyImageProps' 4 | 5 | import {getGatsbyImageFieldConfig} from 'gatsby-plugin-image/graphql-utils' 6 | import {PluginConfig} from '../util/validateConfig' 7 | 8 | const ImageFitType = new GraphQLEnumType({ 9 | name: 'SanityImageFit', 10 | values: { 11 | CLIP: {value: 'clip'}, 12 | CROP: {value: 'crop'}, 13 | FILL: {value: 'fill'}, 14 | FILLMAX: {value: 'fillmax'}, 15 | MAX: {value: 'max'}, 16 | SCALE: {value: 'scale'}, 17 | MIN: {value: 'min'}, 18 | }, 19 | }) 20 | 21 | const ImagePlaceholderType = new GraphQLEnumType({ 22 | name: `SanityGatsbyImagePlaceholder`, 23 | values: { 24 | DOMINANT_COLOR: {value: `dominantColor`}, 25 | BLURRED: {value: `blurred`}, 26 | NONE: {value: `none`}, 27 | }, 28 | }) 29 | 30 | const extensions = new Map>() 31 | 32 | export function extendImageNode(config: PluginConfig): GraphQLFieldConfigMap { 33 | const key = getCacheKey(config, CACHE_KEYS.IMAGE_EXTENSIONS) 34 | 35 | if (extensions.has(key)) { 36 | return extensions.get(key) as GraphQLFieldConfigMap 37 | } 38 | 39 | const extension = getExtension(config) 40 | extensions.set(key, extension) 41 | return extension 42 | } 43 | 44 | function getExtension(config: PluginConfig): GraphQLFieldConfigMap { 45 | const location = {projectId: config.projectId, dataset: config.dataset} 46 | return { 47 | gatsbyImageData: getGatsbyImageFieldConfig( 48 | (image: ImageNode, args: ImageArgs) => getGatsbyImageData(image, args, location), 49 | { 50 | placeholder: { 51 | type: ImagePlaceholderType, 52 | defaultValue: `dominantColor`, 53 | description: `Format of generated placeholder image, displayed while the main image loads. 54 | BLURRED: a blurred, low resolution image, encoded as a base64 data URI (default) 55 | DOMINANT_COLOR: a solid color, calculated from the dominant color of the image. 56 | NONE: no placeholder.`, 57 | }, 58 | fit: { 59 | type: ImageFitType, 60 | defaultValue: 'fill', 61 | }, 62 | }, 63 | ) as any, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-source-sanity", 3 | "version": "7.9.2", 4 | "description": "Gatsby source plugin for building websites using Sanity.io as a backend.", 5 | "keywords": [ 6 | "gatsby", 7 | "gatsby-plugin", 8 | "gatsby-source-plugin" 9 | ], 10 | "homepage": "https://github.com/sanity-io/gatsby-source-sanity#readme", 11 | "bugs": { 12 | "url": "https://github.com/sanity-io/gatsby-source-sanity/issues" 13 | }, 14 | "repository": "https://github.com/sanity-io/gatsby-source-sanity", 15 | "license": "MIT", 16 | "author": "Sanity.io ", 17 | "contributors": [ 18 | { 19 | "name": "Henrique Doro", 20 | "email": "opensource@hdoro.dev" 21 | } 22 | ], 23 | "main": "index.js", 24 | "types": "lib-es5/index.d.ts", 25 | "files": [ 26 | "lib", 27 | "lib-es5", 28 | "src", 29 | "gatsby-node.js", 30 | "gatsby-ssr.js" 31 | ], 32 | "scripts": { 33 | "build": "tsc && tsc -t ES5 --outDir lib-es5", 34 | "format": "prettier --write src/**/*.{ts,tsx}", 35 | "prepublishOnly": "npm run build", 36 | "test": "jest", 37 | "watch": "tsc --watch & tsc && tsc -t ES5 --outDir lib-es5 --watch" 38 | }, 39 | "dependencies": { 40 | "@sanity/client": "^3.4.1", 41 | "@sanity/image-url": "^1.0.2", 42 | "@sanity/mutator": "^2.33.2", 43 | "@types/url-parse": "^1.4.8", 44 | "axios": "^0.27.2", 45 | "debug": "^4.3.4", 46 | "gatsby-core-utils": "^4.5.0", 47 | "gatsby-plugin-utils": "^4.5.0", 48 | "get-stream": "^6.0.1", 49 | "lodash": "^4.17.21", 50 | "oneline": "^1.0.3", 51 | "pumpify": "^2.0.1", 52 | "rxjs": "^6.6.7", 53 | "semver": "^7.3.8", 54 | "split2": "^4.1.0", 55 | "through2": "^4.0.2", 56 | "url-parse": "^1.5.10" 57 | }, 58 | "devDependencies": { 59 | "@types/debug": "^4.1.7", 60 | "@types/jest": "^29.4.0", 61 | "@types/lodash": "^4.14.191", 62 | "@types/node": "^18.11.18", 63 | "@types/react-dom": "^18.0.10", 64 | "@types/semver": "^7.3.13", 65 | "@types/split2": "^4.0.0", 66 | "@types/through2": "^2.0.38", 67 | "eslint": "^8.33.0", 68 | "eslint-config-prettier": "^8.6.0", 69 | "eslint-config-sanity": "^6.0.0", 70 | "gatsby": "^5.5.0", 71 | "gatsby-plugin-image": "^3.5.0", 72 | "jest": "^29.4.1", 73 | "mkdirp": "^2.1.3", 74 | "prettier": "^2.8.3", 75 | "prettier-plugin-packagejson": "^2.4.2", 76 | "prettier-plugin-tailwindcss": "^0.4.0", 77 | "react": "^18.2.0", 78 | "ts-jest": "^29.0.5", 79 | "typescript": "^5.0.0" 80 | }, 81 | "peerDependencies": { 82 | "gatsby": "^3.0.0 || ^4.0.0 || ^5.0.0", 83 | "gatsby-plugin-image": "^1.0.0 || ^2.0.0 || ^3.0.0" 84 | }, 85 | "engines": { 86 | "node": ">=14" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/createNodeManifest.ts: -------------------------------------------------------------------------------- 1 | import {Actions, Node, SourceNodesArgs} from 'gatsby' 2 | import {version} from 'gatsby/package.json' 3 | import {lt, prerelease} from 'semver' 4 | import debug from '../debug' 5 | import {SanityInputNode} from '../types/gatsby' 6 | 7 | let warnOnceForNoSupport: boolean 8 | let warnOnceToUpgradeGatsby: boolean 9 | 10 | const GATSBY_VERSION_MANIFEST_V2 = `4.3.0` 11 | const gatsbyVersion = version 12 | const gatsbyVersionIsPrerelease = (() => { 13 | try { 14 | return prerelease(gatsbyVersion) 15 | } catch (error) { 16 | return null 17 | } 18 | })() 19 | const shouldUpgradeGatsbyVersion = (() => { 20 | try { 21 | return lt(gatsbyVersion, GATSBY_VERSION_MANIFEST_V2) && !gatsbyVersionIsPrerelease 22 | } catch (error) { 23 | return true 24 | } 25 | })() 26 | 27 | export default function createNodeManifest( 28 | actions: Actions, 29 | args: SourceNodesArgs, 30 | node: SanityInputNode, 31 | publishedId: string, 32 | ) { 33 | try { 34 | const {unstable_createNodeManifest} = actions as Actions & { 35 | unstable_createNodeManifest: (props: { 36 | manifestId: string 37 | node: Node 38 | updatedAtUTC: string 39 | }) => void 40 | } 41 | const {getNode} = args 42 | const type = node.internal.type 43 | const autogeneratedTypes = ['SanityFileAsset', 'SanityImageAsset'] 44 | 45 | const createNodeManifestIsSupported = typeof unstable_createNodeManifest === 'function' 46 | const nodeTypeNeedsManifest = autogeneratedTypes.includes(type) === false 47 | const shouldCreateNodeManifest = createNodeManifestIsSupported && nodeTypeNeedsManifest 48 | 49 | if (shouldCreateNodeManifest) { 50 | if (shouldUpgradeGatsbyVersion && !warnOnceToUpgradeGatsby) { 51 | console.warn( 52 | `Your site is doing more work than it needs to for Preview, upgrade to Gatsby ^${GATSBY_VERSION_MANIFEST_V2} for better performance`, 53 | ) 54 | warnOnceToUpgradeGatsby = true 55 | } 56 | 57 | const updatedAt = new Date((node._updatedAt as string) || Date.now()) 58 | 59 | const nodeForManifest = getNode(node.id) as Node 60 | const manifestId = `${publishedId}-${updatedAt.toISOString()}` 61 | 62 | unstable_createNodeManifest({ 63 | manifestId, 64 | node: nodeForManifest, 65 | updatedAtUTC: updatedAt.toUTCString(), 66 | }) 67 | } else if (!createNodeManifestIsSupported && !warnOnceForNoSupport) { 68 | args.reporter.warn( 69 | `Sanity: Your version of Gatsby core doesn't support Content Sync (via the unstable_createNodeManifest action). Please upgrade to the latest version to use Content Sync in your site.`, 70 | ) 71 | warnOnceForNoSupport = true 72 | } 73 | } catch (e) { 74 | let result = (e as Error).message 75 | debug(`Cannot create node manifest`, result) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/errors.ts: -------------------------------------------------------------------------------- 1 | export const pluginPrefix = 'gatsby-source-sanity' 2 | 3 | export function prefixId(id: string) { 4 | return `${pluginPrefix}_${id}` 5 | } 6 | 7 | enum ReporterLevel { 8 | Error = 'ERROR', 9 | } 10 | 11 | enum ReporterCategory { 12 | // Error caused by user (typically, site misconfiguration) 13 | User = 'USER', 14 | // Error caused by Sanity plugin ("third party" relative to Gatsby Cloud) 15 | ThirdParty = 'THIRD_PARTY', 16 | // Error caused by Gatsby process 17 | System = 'SYSTEM', 18 | } 19 | 20 | export const ERROR_CODES = { 21 | UnsupportedGatsbyVersion: '10000', 22 | SchemaFetchError: '10001', 23 | MissingProjectId: '10002', 24 | MissingDataset: '10002', 25 | InvalidToken: '10003', 26 | ExpiredToken: '10004', 27 | WrongProjectToken: '10005', 28 | } 29 | 30 | export const ERROR_MAP = { 31 | [ERROR_CODES.UnsupportedGatsbyVersion]: { 32 | text: (context: any) => context.sourceMessage, 33 | level: ReporterLevel.Error, 34 | category: ReporterCategory.User, 35 | }, 36 | [ERROR_CODES.SchemaFetchError]: { 37 | text: (context: any) => context.sourceMessage, 38 | level: ReporterLevel.Error, 39 | category: ReporterCategory.ThirdParty, 40 | }, 41 | [ERROR_CODES.MissingProjectId]: { 42 | text: (context: any) => context.sourceMessage, 43 | level: ReporterLevel.Error, 44 | category: ReporterCategory.User, 45 | }, 46 | [ERROR_CODES.MissingDataset]: { 47 | text: (context: any) => context.sourceMessage, 48 | level: ReporterLevel.Error, 49 | category: ReporterCategory.User, 50 | }, 51 | [ERROR_CODES.InvalidToken]: { 52 | text: (context: any) => context.sourceMessage, 53 | level: ReporterLevel.Error, 54 | category: ReporterCategory.User, 55 | }, 56 | [ERROR_CODES.ExpiredToken]: { 57 | text: (context: any) => context.sourceMessage, 58 | level: ReporterLevel.Error, 59 | category: ReporterCategory.User, 60 | }, 61 | [ERROR_CODES.WrongProjectToken]: { 62 | text: (context: any) => context.sourceMessage, 63 | level: ReporterLevel.Error, 64 | category: ReporterCategory.User, 65 | }, 66 | } 67 | 68 | // Map Sanity API errors to plugin errors 69 | export const SANITY_ERROR_CODE_MAP: Record = { 70 | 'SIO-401-ANF': ERROR_CODES.InvalidToken, 71 | 'SIO-401-AWH': ERROR_CODES.WrongProjectToken, 72 | 'SIO-401-AEX': ERROR_CODES.ExpiredToken, 73 | } 74 | 75 | export const SANITY_ERROR_CODE_MESSAGES: Record = { 76 | 'SIO-401-ANF': 'The token specified is not valid or has been deleted', 77 | 'SIO-401-AWH': 'The token specified does not belong to the configured project', 78 | 'SIO-401-AEX': 79 | 'The token specified is expired - use API tokens instead of user tokens to prevent this from happening', 80 | } 81 | 82 | export class ErrorWithCode extends Error { 83 | public code?: string | number 84 | 85 | constructor(message: string, code?: string | number) { 86 | super(message) 87 | this.code = code 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/handleWebhookEvent.ts: -------------------------------------------------------------------------------- 1 | import {SourceNodesArgs} from 'gatsby' 2 | import {SanityClient} from '@sanity/client' 3 | import debug from '../debug' 4 | import {SanityNode} from '../types/gatsby' 5 | import {SanityWebhookBody, SanityWebhookDeleteBody} from '../types/sanity' 6 | import {ProcessingOptions} from './normalize' 7 | import {unprefixId, safeId} from './documentIds' 8 | 9 | type deleteWebhookArgs = SourceNodesArgs & {webhookBody: SanityWebhookDeleteBody} 10 | 11 | /** 12 | * Gets a document id received from the webhook & delete it in the store. 13 | */ 14 | function handleDeleteWebhook( 15 | args: deleteWebhookArgs, 16 | options: {client: SanityClient; processingOptions: ProcessingOptions}, 17 | ) { 18 | const {webhookBody, reporter} = args 19 | 20 | const {documentId: rawId, dataset, projectId} = webhookBody 21 | 22 | const publishedDocumentId = unprefixId(rawId) 23 | const config = options.client.config() 24 | 25 | if (projectId && dataset && (config.projectId !== projectId || config.dataset !== dataset)) { 26 | return false 27 | } 28 | 29 | // If a draft is deleted, avoid deleting its published counterpart 30 | if (rawId.startsWith('drafts.') && options.processingOptions.overlayDrafts) { 31 | // Sub-optimal: this will skip deleting draft-only documents which should be deleted. 32 | return true 33 | } 34 | 35 | handleDeletedDocuments(args, [publishedDocumentId]) 36 | 37 | reporter.info(`Deleted 1 document`) 38 | return true 39 | } 40 | 41 | export function handleWebhookEvent( 42 | args: SourceNodesArgs & {webhookBody?: SanityWebhookBody}, 43 | options: {client: SanityClient; processingOptions: ProcessingOptions}, 44 | ): boolean { 45 | const {webhookBody, reporter} = args 46 | const validated = validateWebhookPayload(webhookBody) 47 | if (validated === false) { 48 | debug('[sanity] Invalid/non-sanity webhook payload received') 49 | return false 50 | } 51 | 52 | reporter.info('[sanity] Processing changed documents from webhook') 53 | 54 | if (validated === 'delete-operation') { 55 | return handleDeleteWebhook(args as deleteWebhookArgs, options) 56 | } 57 | 58 | return false 59 | } 60 | 61 | function handleDeletedDocuments(context: SourceNodesArgs, ids: string[]) { 62 | const {actions, createNodeId, getNode} = context 63 | const {deleteNode} = actions 64 | 65 | return ids 66 | .map((documentId) => getNode(safeId(unprefixId(documentId), createNodeId))) 67 | .filter((node): node is SanityNode => typeof node !== 'undefined') 68 | .reduce((count, node) => { 69 | debug('Deleted document with ID %s', node._id) 70 | deleteNode(node) 71 | return count + 1 72 | }, 0) 73 | } 74 | 75 | export function validateWebhookPayload( 76 | payload: SanityWebhookBody | undefined, 77 | ): 'delete-operation' | false { 78 | if (!payload) { 79 | return false 80 | } 81 | 82 | if ('operation' in payload && payload.operation === 'delete') { 83 | return 'delete-operation' 84 | } 85 | 86 | return false 87 | } 88 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Migration guide 2 | 3 | ## Migrate from `gatsby-source-sanity` `v6.x` to `v7.x` 4 | 5 | Note: `gatsby-source-sanity` v7 requires Gatsby v3 or newer. See Gatsby's official [migration guide](https://www.gatsbyjs.com/docs/reference/release-notes/migrating-from-v2-to-v3/) for more details about how to migrate to Gatsby v3. 6 | 7 | ### Upgrade gatsby-source-sanity to v7.x 8 | 9 | 💡 In a rush? See [this commit](https://github.com/sanity-io/sanity-template-gatsby-portfolio/commit/7534bb67f9ec627431a4e62b352b02bb1e033fb6) for a real-world example of upgrading a project from Gatsby v2 to v3 (but bear in mind that this diff may not be directly translatable to the specifics of your project). 10 | 11 | 💡 If you get stuck, you can always join our helpful [Slack community](http://slack.sanity.io/) and ask for assistance in the `#gatsby` channel. 12 | 13 | ### Steps required: 14 | 15 | - In the `dependencies` section of your project's `package.json`, upgrade `gatsby` to `^3.0.0` and `gatsby-source-sanity` to `^7.0.0` 16 | - Add `"gatsby-plugin-image": "^1.0.0"` as a dependency to your `package.json`. 17 | Note: If your package json already has a dependency to `gatsby-image`, you should remove it and replace its usage with `gatsby-plugin-image` from source files. See [Gatsby's own migration guide for gatsby-plugin-image](https://www.gatsbyjs.com/docs/reference/release-notes/image-migration-guide/) for more details. 18 | - Migrate usage of `get(Fluid|Fixed)GatsbyImage` from pre v7 of `gatsby-source-sanity` (see section below) 19 | 20 | 💡️ If you get peer dependency errors or warnings during `npm install` after finishing the steps above you may also need to update other Gatsby plugins to the version compatible with Gatsby v3. Refer to the documentation for the individual plugins on how to do this. 21 | 22 | ### Migrate from `getFluidGatsbyImage()` / `getFixedGatsbyImage()` to `getGatsbyImageData()` 23 | 24 | The helper methods `getFluidGatsbyImage` and `getFixedGatsbyImage` have been removed in favor of `getGatsbyImageData()`, which is based on [`gatsby-plugin-image`](https://www.gatsbyjs.com/plugins/gatsby-plugin-image) and supports a number of cool new features and performance optimizations. 25 | 26 | #### Before 27 | 28 | ```jsx 29 | import React from 'react' 30 | import Img from 'gatsby-image' 31 | import {getFluidGatsbyImage} from 'gatsby-source-sanity' 32 | import clientConfig from '../../client-config' 33 | 34 | export function MyImage({node}) { 35 | const fluidProps = getFluidGatsbyImage(node, {maxWidth: 675}, clientConfig.sanity) 36 | return 37 | } 38 | ``` 39 | 40 | #### After 41 | 42 | ```jsx 43 | import React from 'react' 44 | import {GatsbyImage} from 'gatsby-plugin-image' 45 | import {getGatsbyImageData} from 'gatsby-source-sanity' 46 | import clientConfig from '../../client-config' 47 | 48 | export const MyImage = ({node}) => { 49 | const gatsbyImageData = getGatsbyImageData(node, {maxWidth: 675}, clientConfig.sanity) 50 | return 51 | } 52 | ``` 53 | 54 | Now you should be all set. Happy coding! 55 | -------------------------------------------------------------------------------- /examples/blog/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {GatsbyImage} from 'gatsby-plugin-image' 3 | import {graphql} from 'gatsby' 4 | 5 | export default function Page({data}) { 6 | return ( 7 | <> 8 |
9 |
10 |
11 |
12 |
13 |
14 | {data.allSanityPost.edges.map(({node}) => ( 15 |
16 |
17 | {node.mainImage ? ( 18 | 23 | ) : null} 24 |
25 |
26 | 31 |
32 |
33 | {node.author.name} 34 | {node.author?.image ? ( 35 | 40 | ) : null} 41 |
42 |
43 |

44 | {node.author.name} 45 |

46 |
47 | 50 | 51 |
52 |
53 |
54 |
55 |
56 | ))} 57 |
58 |
59 |
60 | 61 | ) 62 | } 63 | 64 | export const Head = () => gatsby-source-sanity 65 | 66 | export const query = graphql` 67 | query { 68 | allSanityPost { 69 | edges { 70 | node { 71 | _id 72 | title 73 | slug { 74 | current 75 | } 76 | mainImage { 77 | asset { 78 | gatsbyImageData(placeholder: BLURRED) 79 | } 80 | } 81 | publishedAt 82 | author { 83 | name 84 | image { 85 | asset { 86 | gatsbyImageData(placeholder: BLURRED) 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | ` 95 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/resolveReferences.ts: -------------------------------------------------------------------------------- 1 | import {NodePluginArgs} from 'gatsby' 2 | import debug from '../debug' 3 | import {safeId, unprefixId} from './documentIds' 4 | 5 | const defaultResolveOptions: ResolveReferencesOptions = { 6 | maxDepth: 5, 7 | overlayDrafts: false, 8 | } 9 | 10 | interface ResolveReferencesOptions { 11 | maxDepth: number 12 | overlayDrafts: boolean 13 | } 14 | 15 | // NOTE: This is now a public API and should be treated as such 16 | export function resolveReferences( 17 | obj: any, 18 | context: Pick, 19 | options: Partial = {}, 20 | currentDepth = 0, 21 | ): any { 22 | const {createNodeId, getNode} = context 23 | const resolveOptions = {...defaultResolveOptions, ...options} 24 | const {overlayDrafts, maxDepth} = resolveOptions 25 | 26 | if (Array.isArray(obj)) { 27 | return currentDepth <= maxDepth 28 | ? obj.map((item) => resolveReferences(item, context, resolveOptions, currentDepth + 1)) 29 | : obj 30 | } 31 | 32 | if (obj === null || typeof obj !== 'object') { 33 | return obj 34 | } 35 | 36 | if (typeof obj._ref === 'string') { 37 | const targetId = 38 | // If the reference starts with a '-', it means it's a Gatsby node ID, 39 | // not a Sanity document ID. Thus, it does not need to be rewritten 40 | obj._ref.startsWith('-') 41 | ? obj._ref 42 | : safeId(overlayDrafts ? unprefixId(obj._ref) : obj._ref, createNodeId) 43 | 44 | debug('Resolve %s (Sanity ID %s)', targetId, obj._ref) 45 | 46 | const node = getNode(targetId) 47 | if (!node && obj._weak) { 48 | return null 49 | } else if (!node) { 50 | debug(`Could not resolve reference to ID "${targetId}"`) 51 | return null 52 | } 53 | 54 | return node && currentDepth <= maxDepth 55 | ? resolveReferences(remapRawFields(node), context, resolveOptions, currentDepth + 1) 56 | : obj 57 | } 58 | 59 | const initial: {[key: string]: any} = {} 60 | return Object.keys(obj).reduce((acc, key) => { 61 | const isRawDataField = key.startsWith('_rawData') 62 | const value = resolveReferences(obj[key], context, resolveOptions, currentDepth + 1) 63 | const targetKey = isRawDataField ? `_raw${key.slice(8)}` : key 64 | acc[targetKey] = value 65 | return acc 66 | }, initial) 67 | } 68 | 69 | /** 70 | * When resolving a Gatsby node through resolveReferences, it's always (through the GraphQL API) 71 | * operation on a "raw" field. The expectation is to have the return value be as close to the 72 | * Sanity document as possible. Thus, when we've resolved the node, we want to remap the raw 73 | * fields to be named as the original field name. `_rawSections` becomes `sections`. Since the 74 | * value is fetched from the "raw" variant, the references inside it do not have their IDs 75 | * rewired to their Gatsby equivalents. 76 | */ 77 | function remapRawFields(obj: {[key: string]: any}) { 78 | const initial: {[key: string]: any} = {} 79 | return Object.keys(obj).reduce((acc, key) => { 80 | if (key === 'internal') { 81 | return acc 82 | } 83 | if (key.startsWith('_rawData')) { 84 | let targetKey = key.slice(8) 85 | 86 | // Look for UpperCase variant first, if not found, try camelCase 87 | targetKey = 88 | typeof obj[targetKey] === 'undefined' 89 | ? targetKey[0].toLowerCase() + targetKey.slice(1) 90 | : targetKey 91 | 92 | acc[targetKey] = obj[key] 93 | } else if (!acc[key]) { 94 | acc[key] = obj[key] 95 | } 96 | return acc 97 | }, initial) 98 | } 99 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/getGraphQLResolverMap.ts: -------------------------------------------------------------------------------- 1 | import {camelCase} from 'lodash' 2 | import {CreateResolversArgs} from 'gatsby' 3 | import {GraphQLFieldResolver} from 'gatsby/graphql' 4 | import {GatsbyResolverMap, GatsbyGraphQLContext, GatsbyNodeModel} from '../types/gatsby' 5 | import {TypeMap, FieldDef} from './remoteGraphQLSchema' 6 | import {resolveReferences} from './resolveReferences' 7 | import {PluginConfig} from './validateConfig' 8 | import {getConflictFreeFieldName, getTypeName} from './normalize' 9 | import {SanityRef} from '../types/sanity' 10 | 11 | export function getGraphQLResolverMap( 12 | typeMap: TypeMap, 13 | pluginConfig: PluginConfig, 14 | context: CreateResolversArgs, 15 | ): GatsbyResolverMap { 16 | const resolvers: GatsbyResolverMap = {} 17 | Object.keys(typeMap.objects).forEach((typeName) => { 18 | const objectType = typeMap.objects[typeName] 19 | const fieldNames = Object.keys(objectType.fields) 20 | 21 | // Add resolvers for lists 22 | resolvers[objectType.name] = fieldNames 23 | .map((fieldName) => ({fieldName, ...objectType.fields[fieldName]})) 24 | .filter((field) => field.isList) 25 | .reduce((fields, field) => { 26 | const targetField = objectType.isDocument 27 | ? getConflictFreeFieldName(field.fieldName, pluginConfig.typePrefix) 28 | : field.fieldName 29 | 30 | fields[targetField] = {resolve: getResolver(field, pluginConfig.typePrefix)} 31 | return fields 32 | }, resolvers[objectType.name] || {}) 33 | 34 | // Add raw resolvers 35 | resolvers[objectType.name] = fieldNames 36 | .map((fieldName) => ({fieldName, ...objectType.fields[fieldName]})) 37 | .filter((field) => field.aliasFor) 38 | .reduce((fields, field) => { 39 | fields[field.fieldName] = {resolve: getRawResolver(field, pluginConfig, context)} 40 | return fields 41 | }, resolvers[objectType.name] || {}) 42 | }) 43 | 44 | return resolvers 45 | } 46 | 47 | function getResolver( 48 | field: FieldDef & {fieldName: string}, 49 | typePrefix?: string, 50 | ): GraphQLFieldResolver<{[key: string]: any}, GatsbyGraphQLContext> { 51 | return (source, args, context) => { 52 | if (field.isList) { 53 | const items: SanityRef[] = source[field.fieldName] || [] 54 | return items && Array.isArray(items) 55 | ? items.map((item) => maybeResolveReference(item, context.nodeModel, typePrefix)) 56 | : [] 57 | } 58 | 59 | const item: SanityRef | undefined = source[field.fieldName] 60 | return maybeResolveReference(item, context.nodeModel, typePrefix) 61 | } 62 | } 63 | 64 | function maybeResolveReference( 65 | item: {_ref?: string; _type?: string; internal?: {}} | undefined, 66 | nodeModel: GatsbyNodeModel, 67 | typePrefix?: string, 68 | ) { 69 | if (item && typeof item._ref === 'string') { 70 | return nodeModel.getNodeById({id: item._ref}) 71 | } 72 | 73 | if (item && typeof item._type === 'string' && !item.internal) { 74 | return {...item, internal: {type: getTypeName(item._type, typePrefix)}} 75 | } 76 | 77 | return item 78 | } 79 | 80 | function getRawResolver( 81 | field: FieldDef & {fieldName: string}, 82 | pluginConfig: PluginConfig, 83 | context: CreateResolversArgs, 84 | ): GraphQLFieldResolver<{[key: string]: any}, GatsbyGraphQLContext> { 85 | const {fieldName} = field 86 | const aliasName = '_' + camelCase(`raw ${fieldName}`) 87 | return (obj, args) => { 88 | const raw = `_${camelCase(`raw_data_${field.aliasFor || fieldName}`)}` 89 | const value = obj[raw] || obj[aliasName] || obj[field.aliasFor || fieldName] || obj[fieldName] 90 | return args.resolveReferences 91 | ? resolveReferences(value, context, { 92 | maxDepth: args.resolveReferences.maxDepth, 93 | overlayDrafts: pluginConfig.overlayDrafts, 94 | }) 95 | : value 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/getSyncWithGatsby.ts: -------------------------------------------------------------------------------- 1 | import {Node, SourceNodesArgs} from 'gatsby' 2 | import debug from '../debug' 3 | import {SanityInputNode} from '../types/gatsby' 4 | import {SanityDocument} from '../types/sanity' 5 | import createNodeManifest from './createNodeManifest' 6 | import {prefixId, unprefixId} from './documentIds' 7 | import {getTypeName, ProcessingOptions, toGatsbyNode} from './normalize' 8 | import {TypeMap} from './remoteGraphQLSchema' 9 | 10 | export type SyncWithGatsby = (id: string, document?: SanityDocument) => void 11 | 12 | /** 13 | * @returns function to sync a single document from the local cache of known documents with Gatsby 14 | */ 15 | export default function getSyncWithGatsby(props: { 16 | documents: Map 17 | gatsbyNodes: Map 18 | typeMap: TypeMap 19 | processingOptions: ProcessingOptions 20 | args: SourceNodesArgs 21 | }): SyncWithGatsby { 22 | const {documents, gatsbyNodes, processingOptions, args} = props 23 | const {typeMap, overlayDrafts, typePrefix} = processingOptions 24 | const {reporter, actions} = args 25 | const {createNode, deleteNode} = actions 26 | 27 | return (id, updatedDocument) => { 28 | const publishedId = unprefixId(id) 29 | const draftId = prefixId(id) 30 | 31 | // `handleDeltaChanges` uses updatedDocument to avoid having to be responsible for updating `documents` 32 | if (updatedDocument) { 33 | documents.set(id, updatedDocument) 34 | } 35 | 36 | const published = documents.get(publishedId) 37 | const draft = documents.get(draftId) 38 | 39 | const doc = draft || published 40 | if (doc) { 41 | const type = getTypeName(doc._type, typePrefix) 42 | if (!typeMap.objects[type]) { 43 | reporter.warn( 44 | `[sanity] Document "${doc._id}" has type ${doc._type} (${type}), which is not declared in the GraphQL schema. Make sure you run "graphql deploy". Skipping document.`, 45 | ) 46 | return 47 | } 48 | } 49 | 50 | if (id === draftId && !overlayDrafts) { 51 | // do nothing, we're not overlaying drafts 52 | debug('overlayDrafts is not enabled, so skipping createNode for draft') 53 | return 54 | } 55 | 56 | if (id === publishedId) { 57 | if (draft && overlayDrafts) { 58 | // we have a draft, and overlayDrafts is enabled, so skip to the draft document instead 59 | debug( 60 | 'skipping createNode of %s since there is a draft and overlayDrafts is enabled', 61 | publishedId, 62 | ) 63 | return 64 | } 65 | 66 | if (gatsbyNodes.has(publishedId)) { 67 | // sync existing gatsby node with document from updated cache 68 | if (published) { 69 | debug('updating gatsby node for %s', publishedId) 70 | const node = toGatsbyNode(published, processingOptions) 71 | gatsbyNodes.set(publishedId, node) 72 | createNode(node) 73 | createNodeManifest(actions, args, node, publishedId) 74 | } else { 75 | // the published document has been removed (note - we either have no draft or overlayDrafts is not enabled so merely removing is ok here) 76 | debug( 77 | 'deleting gatsby node for %s since there is no draft and overlayDrafts is not enabled', 78 | publishedId, 79 | ) 80 | deleteNode(gatsbyNodes.get(publishedId)!) 81 | gatsbyNodes.delete(publishedId) 82 | } 83 | } else if (published) { 84 | // when we don't have a gatsby node for the published document 85 | debug('creating gatsby node for %s', publishedId) 86 | const node = toGatsbyNode(published, processingOptions) 87 | gatsbyNodes.set(publishedId, node) 88 | createNode(node) 89 | createNodeManifest(actions, args, node, publishedId) 90 | } 91 | } 92 | if (id === draftId && overlayDrafts) { 93 | // we're syncing a draft version and overlayDrafts is enabled 94 | if (gatsbyNodes.has(publishedId) && !draft && !published) { 95 | // have stale gatsby node for a published document that has neither a draft or a published (e.g. it's been deleted) 96 | debug( 97 | 'deleting gatsby node for %s since there is neither a draft nor a published version of it any more', 98 | publishedId, 99 | ) 100 | deleteNode(gatsbyNodes.get(publishedId)!) 101 | gatsbyNodes.delete(publishedId) 102 | return 103 | } 104 | 105 | debug( 106 | 'Replacing gatsby node for %s using the %s document', 107 | publishedId, 108 | draft ? 'draft' : 'published', 109 | ) 110 | // pick the draft if we can, otherwise pick the published 111 | const node = toGatsbyNode((draft || published)!, processingOptions) 112 | gatsbyNodes.set(publishedId, node) 113 | createNode(node) 114 | createNodeManifest(actions, args, node, publishedId) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/test/getGatsbyImageProps.test.ts: -------------------------------------------------------------------------------- 1 | import {getGatsbyImageData, ImageNode} from '../src/images/getGatsbyImageProps' 2 | 3 | // Smallish image 4 | const jpegId = 'image-abc123-300x200-jpg' 5 | const jpegRef = {_ref: 'image-abc123-300x200-jpg'} 6 | const jpegResolved: ImageNode = { 7 | _id: 'image-abc123-300x200-jpg', 8 | url: 'https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg', 9 | assetId: 'abc123', 10 | extension: 'jpg', 11 | metadata: { 12 | lqip: 'data:image/jpeg;base64,someString', 13 | dimensions: { 14 | width: 300, 15 | height: 200, 16 | aspectRatio: 300 / 200, 17 | }, 18 | palette: { 19 | dominant: { 20 | background: 'rebeccapurple', 21 | }, 22 | }, 23 | }, 24 | } 25 | 26 | // Largeish image 27 | const webpResolved: ImageNode = { 28 | _id: 'image-def456-4240x2832-webp', 29 | url: 'https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp', 30 | assetId: 'def456', 31 | extension: 'webp', 32 | metadata: { 33 | lqip: 'data:image/webp;base64,someString', 34 | dimensions: { 35 | width: 4240, 36 | height: 2832, 37 | aspectRatio: 4240 / 2832, 38 | }, 39 | palette: { 40 | dominant: { 41 | background: 'papayawhip', 42 | }, 43 | }, 44 | }, 45 | } 46 | 47 | // Tiny image, should generally not be processed apart from format 48 | const smallId = 'image-bf1942-70x70-jpg' 49 | 50 | const location = { 51 | projectId: 'projectId', 52 | dataset: 'dataset', 53 | } 54 | 55 | test('[resolved] gatabyImageData jpg without params', () => { 56 | expect(getGatsbyImageData(jpegResolved, {}, location)).toMatchSnapshot() 57 | }) 58 | 59 | test('[resolved] gatsbyImageData fullWidth jpg', () => { 60 | expect(getGatsbyImageData(jpegResolved, {layout: 'fullWidth'}, location)).toMatchSnapshot() 61 | }) 62 | 63 | test('[resolved] gatsbyImageData fixed jpg', () => { 64 | expect(getGatsbyImageData(jpegResolved, {layout: 'fixed'}, location)).toMatchSnapshot() 65 | }) 66 | 67 | test('[resolved] gatsbyImageData blurred placeholder', () => { 68 | expect(getGatsbyImageData(jpegResolved, {placeholder: 'blurred'}, location)).toMatchSnapshot() 69 | }) 70 | 71 | test('[resolved] gatsbyImageData dominantColor placeholder', () => { 72 | expect( 73 | getGatsbyImageData(jpegResolved, {placeholder: 'dominantColor'}, location), 74 | ).toMatchSnapshot() 75 | }) 76 | 77 | test('[resolved] gatsbyImageData constrained jpg with width (600)', () => { 78 | expect(getGatsbyImageData(jpegResolved, {width: 600}, location)).toMatchSnapshot() 79 | }) 80 | 81 | test('[ref] gatabyImageData jpg without params', () => { 82 | expect(getGatsbyImageData(jpegRef, {}, location)).toMatchSnapshot() 83 | }) 84 | 85 | test('[ref] gatsbyImageData fullWidth jpg', () => { 86 | expect(getGatsbyImageData(jpegRef, {layout: 'fullWidth'}, location)).toMatchSnapshot() 87 | }) 88 | 89 | test('[ref] gatsbyImageData fixed jpg', () => { 90 | expect(getGatsbyImageData(jpegRef, {layout: 'fixed'}, location)).toMatchSnapshot() 91 | }) 92 | 93 | test('[ref] gatsbyImageData constrained jpg with width (600)', () => { 94 | expect(getGatsbyImageData(jpegRef, {width: 600}, location)).toMatchSnapshot() 95 | }) 96 | 97 | test('[id] gatsbyImageData jpg without params', () => { 98 | expect(getGatsbyImageData(jpegId, {}, location)).toMatchSnapshot() 99 | }) 100 | 101 | test('[id] gatsbyImageData fullWidth jpg', () => { 102 | expect(getGatsbyImageData(jpegId, {layout: 'fullWidth'}, location)).toMatchSnapshot() 103 | }) 104 | 105 | test('[id] gatsbyImageData fixed jpg', () => { 106 | expect(getGatsbyImageData(jpegId, {layout: 'fixed'}, location)).toMatchSnapshot() 107 | }) 108 | 109 | test('[id] gatsbyImageData constrained jpg with width (600)', () => { 110 | expect(getGatsbyImageData(jpegId, {width: 600}, location)).toMatchSnapshot() 111 | }) 112 | 113 | // WebP, largish image 114 | test('[resolved] gatsbyImageData webp without params', () => { 115 | expect(getGatsbyImageData(webpResolved, {}, location)).toMatchSnapshot() 116 | }) 117 | 118 | test('[resolved] gatsbyImageData webp dominant color', () => { 119 | expect(getGatsbyImageData(webpResolved, {}, location)).toMatchSnapshot() 120 | }) 121 | 122 | test('[resolved] gatsbyImageData webp fullWidth', () => { 123 | expect(getGatsbyImageData(webpResolved, {layout: 'fullWidth'}, location)).toMatchSnapshot() 124 | }) 125 | 126 | // No upscaling 127 | test('[id] gatsbyImageData, jpeg with width (300) > original size', () => { 128 | expect(getGatsbyImageData(smallId, {width: 300}, location)).toMatchSnapshot() 129 | }) 130 | 131 | // No upscaling for fixed with same aspect ratio 132 | test('[id] gatsbyImageData jpeg with width/height (300x300) > original size, same aspect', () => { 133 | expect(getGatsbyImageData(smallId, {width: 300, height: 300}, location)).toMatchSnapshot() 134 | }) 135 | 136 | // Upscale for fixed with different aspect ratio 137 | test('[id] gatsbyImageData jpeg with width/height (320x240) > original size, different aspect', () => { 138 | expect(getGatsbyImageData(smallId, {width: 320, height: 240}, location)).toMatchSnapshot() 139 | }) 140 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/images/getGatsbyImageProps.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generateImageData, 3 | IGatsbyImageData, 4 | IGatsbyImageHelperArgs, 5 | Layout, 6 | } from 'gatsby-plugin-image' 7 | 8 | export type ImageNode = ImageAsset | ImageObject | ImageRef | string | null | undefined 9 | import imageUrlBuilder from '@sanity/image-url' 10 | import {ImageUrlBuilder} from '@sanity/image-url/lib/types/builder' 11 | 12 | export const EVERY_BREAKPOINT = [ 13 | 320, 654, 768, 1024, 1366, 1600, 1920, 2048, 2560, 3440, 3840, 4096, 14 | ] 15 | 16 | export enum ImageFormat { 17 | NO_CHANGE = '', 18 | WEBP = 'webp', 19 | JPG = 'jpg', 20 | PNG = 'png', 21 | } 22 | 23 | type ImagePalette = { 24 | darkMuted?: ImagePaletteSwatch 25 | lightVibrant?: ImagePaletteSwatch 26 | darkVibrant?: ImagePaletteSwatch 27 | vibrant?: ImagePaletteSwatch 28 | dominant?: ImagePaletteSwatch 29 | lightMuted?: ImagePaletteSwatch 30 | muted?: ImagePaletteSwatch 31 | } 32 | 33 | type ImagePaletteSwatch = { 34 | background?: string 35 | foreground?: string 36 | population?: number 37 | title?: string 38 | } 39 | 40 | type ImageDimensions = { 41 | width: number 42 | height: number 43 | aspectRatio: number 44 | } 45 | 46 | type ImageMetadata = { 47 | palette?: ImagePalette 48 | dimensions: ImageDimensions 49 | lqip?: string 50 | } 51 | 52 | type ImageAssetStub = { 53 | url: string 54 | assetId: string 55 | extension: string 56 | metadata: ImageMetadata 57 | } 58 | 59 | type ImageAsset = ImageAssetStub & { 60 | _id: string 61 | } 62 | 63 | type ImageRef = { 64 | _ref: string 65 | } 66 | 67 | type ImageObject = { 68 | asset: ImageRef | ImageAsset 69 | } 70 | 71 | export type ImageArgs = { 72 | maxWidth?: number 73 | maxHeight?: number 74 | sizes?: string 75 | toFormat?: ImageFormat 76 | } 77 | 78 | type SanityLocation = { 79 | projectId: string 80 | dataset: string 81 | } 82 | 83 | const idPattern = /^image-[A-Za-z0-9]+-\d+x\d+-[a-z]+$/ 84 | 85 | function buildImageUrl(loc: SanityLocation, stub: ImageAssetStub) { 86 | const {projectId, dataset} = loc 87 | const {assetId, extension, metadata} = stub 88 | const {width, height} = metadata.dimensions 89 | const base = 'https://cdn.sanity.io/images' 90 | 91 | return `${base}/${projectId}/${dataset}/${assetId}-${width}x${height}.${extension}` 92 | } 93 | 94 | function getBasicImageProps(node: ImageNode, loc: SanityLocation): ImageAssetStub | false { 95 | if (!node) { 96 | return false 97 | } 98 | 99 | const obj = node as ImageObject 100 | const ref = node as ImageRef 101 | const img = node as ImageAsset 102 | 103 | let id: string = '' 104 | if (typeof node === 'string') { 105 | id = node 106 | } else if (obj.asset) { 107 | id = (obj.asset as ImageRef)._ref || (obj.asset as ImageAsset)._id 108 | } else { 109 | id = ref._ref || img._id 110 | } 111 | 112 | const hasId = !id || idPattern.test(id) 113 | if (!hasId) { 114 | return false 115 | } 116 | 117 | const [, assetId, dimensions, extension] = id.split('-') 118 | const [width, height] = dimensions.split('x').map((num) => parseInt(num, 10)) 119 | const aspectRatio = width / height 120 | const metadata = img.metadata || {dimensions: {width, height, aspectRatio}} 121 | const url = img.url || buildImageUrl(loc, {url: '', assetId, extension, metadata}) 122 | 123 | return { 124 | url, 125 | assetId, 126 | extension, 127 | metadata, 128 | } 129 | } 130 | 131 | const fitMap = new Map([ 132 | [`clip`, `inside`], 133 | [`crop`, `cover`], 134 | [`fill`, `contain`], 135 | [`fillmax`, `contain`], 136 | [`max`, `inside`], 137 | [`scale`, `fill`], 138 | [`min`, `inside`], 139 | ]) 140 | 141 | const generateImageSource: IGatsbyImageHelperArgs['generateImageSource'] = ( 142 | filename, 143 | width, 144 | height, 145 | toFormat, 146 | fit, 147 | options, 148 | ) => { 149 | const {builder} = options as {builder: ImageUrlBuilder} 150 | const src = builder.width(width).height(height).auto('format').url() as string 151 | return {width, height, format: 'auto', src} 152 | } 153 | 154 | type ImageFit = 'clip' | 'crop' | 'fill' | 'fillmax' | 'max' | 'scale' | 'min' 155 | 156 | export type GatsbyImageDataArgs = { 157 | width?: number 158 | height?: number 159 | aspectRatio?: number 160 | layout?: Layout 161 | sizes?: string 162 | placeholder?: 'blurred' | 'dominantColor' | 'none' 163 | fit?: ImageFit 164 | } 165 | 166 | // gatsby-plugin-image 167 | export function getGatsbyImageData( 168 | image: ImageNode, 169 | {fit, ...args}: GatsbyImageDataArgs, 170 | loc: SanityLocation, 171 | ): IGatsbyImageData | null { 172 | const imageStub = getBasicImageProps(image, loc) 173 | 174 | if (!imageStub || !image) { 175 | return null 176 | } 177 | 178 | const {width, height} = imageStub.metadata.dimensions 179 | 180 | const builder = imageUrlBuilder(loc).image(image) 181 | 182 | const imageProps = generateImageData({ 183 | ...args, 184 | pluginName: `gatsby-source-sanity`, 185 | sourceMetadata: { 186 | format: 'auto', 187 | width, 188 | height, 189 | }, 190 | fit: fit ? fitMap.get(fit) : undefined, 191 | filename: imageStub.url, 192 | generateImageSource, 193 | options: {builder}, 194 | formats: ['auto'], 195 | breakpoints: EVERY_BREAKPOINT, 196 | }) 197 | 198 | let placeholderDataURI: string | undefined 199 | 200 | if (args.placeholder === `dominantColor`) { 201 | imageProps.backgroundColor = imageStub.metadata.palette?.dominant?.background 202 | } 203 | 204 | if (args.placeholder === `blurred`) { 205 | imageProps.placeholder = imageStub.metadata.lqip 206 | ? {fallback: imageStub.metadata.lqip} 207 | : undefined 208 | } 209 | 210 | if (placeholderDataURI) { 211 | imageProps.placeholder = {fallback: placeholderDataURI} 212 | } 213 | 214 | return imageProps 215 | } 216 | -------------------------------------------------------------------------------- /examples/cdrs/test/graphqlReferences.test.js: -------------------------------------------------------------------------------- 1 | const { assert } = require("console") 2 | 3 | beforeAll(async () => { 4 | // Test if Gatsby server is running 5 | const response = await fetch("http://localhost:8000/___graphql") 6 | if (response.status !== 200) { 7 | throw new Error("Gatsby server is not running.") 8 | } 9 | }) 10 | 11 | async function fetchGraphQL(query, variables) { 12 | return fetch("http://localhost:8000/___graphql", { 13 | method: "POST", 14 | headers: { 15 | "Content-Type": "application/json", 16 | }, 17 | body: JSON.stringify({ 18 | query, 19 | variables, 20 | }), 21 | }).then(res => { 22 | return res.json() 23 | }) 24 | } 25 | 26 | test("resolves regular field reference", async () => { 27 | const res = await fetchGraphQL(` 28 | query { 29 | allSanityBook { 30 | edges { 31 | node { 32 | publisher { 33 | name 34 | } 35 | } 36 | } 37 | } 38 | } 39 | `) 40 | 41 | expect(res.errors).toBeUndefined() 42 | const node = res.data.allSanityBook.edges[0].node 43 | expect(node.publisher.name).toBe("Bantam Books") 44 | }) 45 | 46 | test("resolves inline cross dataset field reference", async () => { 47 | const res = await fetchGraphQL(` 48 | query { 49 | allSanityBook { 50 | edges { 51 | node { 52 | author { 53 | name 54 | } 55 | } 56 | } 57 | } 58 | } 59 | `) 60 | 61 | expect(res.errors).toBeUndefined() 62 | const node = res.data.allSanityBook.edges[0].node 63 | expect(node.author.name).toBe("Neal Stephenson") 64 | }) 65 | 66 | test('resolves inline cross dataset field reference that has multiple "to" types', async () => { 67 | const res = await fetchGraphQL(` 68 | query { 69 | allSanityBook { 70 | edges { 71 | node { 72 | authorOrEditorInline { 73 | ... on SanityAuthor { 74 | name 75 | } 76 | ... on SanityEditor { 77 | name 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | `) 85 | 86 | expect(res.errors).toBeUndefined() 87 | const node = res.data.allSanityBook.edges[0].node 88 | expect(node.authorOrEditorInline.name).toBe("Mrs Book Editor") 89 | }) 90 | 91 | test("resolves named cross dataset field reference", async () => { 92 | const res = await fetchGraphQL(` 93 | query { 94 | allSanityBook { 95 | edges { 96 | node { 97 | extraAuthor { 98 | name 99 | } 100 | } 101 | } 102 | } 103 | } 104 | `) 105 | 106 | expect(res.errors).toBeUndefined() 107 | const node = res.data.allSanityBook.edges[0].node 108 | expect(node.extraAuthor.name).toBe("Neal Stephenson") 109 | }) 110 | 111 | test('resolves named cross dataset field reference with multiple "to" types', async () => { 112 | const res = await fetchGraphQL(` 113 | query { 114 | allSanityBook { 115 | edges { 116 | node { 117 | authorOrEditor { 118 | ... on SanityAuthor { 119 | name 120 | } 121 | ... on SanityEditor { 122 | name 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | `) 130 | 131 | expect(res.errors).toBeUndefined() 132 | const node = res.data.allSanityBook.edges[0].node 133 | expect(node.authorOrEditor.name).toBe("Neal Stephenson") 134 | }) 135 | 136 | test("it resolves arrays of references", async () => { 137 | const res = await fetchGraphQL(` 138 | query { 139 | allSanityBook { 140 | edges { 141 | node { 142 | genres { 143 | title 144 | } 145 | } 146 | } 147 | } 148 | } 149 | `) 150 | 151 | expect(res.errors).toBeUndefined() 152 | const node = res.data.allSanityBook.edges[0].node 153 | const expected = [ 154 | { 155 | title: "Science Fiction", 156 | }, 157 | { 158 | title: "Cyberpunk", 159 | }, 160 | ] 161 | 162 | expect(node.genres).toEqual(expected) 163 | }) 164 | 165 | test("it resolves arrays of cross dataset references", async () => { 166 | const res = await fetchGraphQL(` 167 | query { 168 | allSanityBook { 169 | edges { 170 | node { 171 | coauthors { 172 | name 173 | } 174 | } 175 | } 176 | } 177 | } 178 | `) 179 | 180 | expect(res.errors).toBeUndefined() 181 | const node = res.data.allSanityBook.edges[0].node 182 | const expected = [ 183 | { 184 | name: "Nom de Plume", 185 | }, 186 | { 187 | name: "Neal Stephenson", 188 | }, 189 | ] 190 | 191 | expect(node.coauthors).toEqual(expected) 192 | }) 193 | 194 | test("it resolves cross dataset references that are part of an Union", async () => { 195 | const res = await fetchGraphQL(` 196 | query { 197 | allSanityBook { 198 | edges { 199 | node { 200 | mixedArray { 201 | ... on SanityAuthor { 202 | name 203 | } 204 | } 205 | } 206 | } 207 | } 208 | } 209 | `) 210 | 211 | expect(res.errors).toBeUndefined() 212 | const node = res.data.allSanityBook.edges[0].node 213 | const expected = [ 214 | { 215 | name: "Neal Stephenson", 216 | }, 217 | ] 218 | 219 | // expect node.mixedArray to include expected object 220 | expect(node.mixedArray).toEqual(expect.arrayContaining(expected)) 221 | }) 222 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/test/resolveReferences.test.ts: -------------------------------------------------------------------------------- 1 | import {Node} from 'gatsby' 2 | import {resolveReferences} from '../src/util/resolveReferences' 3 | 4 | function reverse(id: string) { 5 | return id.split('').reverse().join('') 6 | } 7 | 8 | // Gatsby's `getNode()` is typed to _always_ return a node, which... aint true. 9 | const noNode: Node = undefined as unknown as Node 10 | const createNodeId = (id: string) => id 11 | 12 | test('resolves Sanity references', () => { 13 | const _id = 'abc123' 14 | const getNode = (id: string) => 15 | id === '-abc123' 16 | ? { 17 | _id, 18 | id: _id, 19 | bar: 'baz', 20 | parent: `someParent`, 21 | internal: { 22 | owner: `asdf`, 23 | type: `asdf`, 24 | contentDigest: `asdf`, 25 | }, 26 | children: [], 27 | } 28 | : noNode 29 | 30 | expect( 31 | resolveReferences( 32 | {foo: {_ref: _id}}, 33 | {createNodeId, getNode}, 34 | {maxDepth: 5, overlayDrafts: true}, 35 | ), 36 | ).toEqual({ 37 | foo: { 38 | _id, 39 | id: _id, 40 | bar: 'baz', 41 | parent: `someParent`, 42 | children: [], 43 | }, 44 | }) 45 | }) 46 | 47 | test('uses non-draft if overlayDrafts is set to true', () => { 48 | const _id = 'abc123' 49 | const getNode = (id: string) => 50 | id === '-abc123' 51 | ? { 52 | _id, 53 | id: _id, 54 | bar: 'baz', 55 | parent: `someParent`, 56 | internal: { 57 | owner: `asdf`, 58 | type: `asdf`, 59 | contentDigest: `asdf`, 60 | }, 61 | children: [], 62 | } 63 | : noNode 64 | 65 | expect( 66 | resolveReferences( 67 | {foo: {_ref: `drafts.${_id}`}}, 68 | {createNodeId, getNode}, 69 | {maxDepth: 5, overlayDrafts: true}, 70 | ), 71 | ).toEqual({ 72 | foo: { 73 | _id, 74 | id: _id, 75 | bar: 'baz', 76 | parent: `someParent`, 77 | children: [], 78 | }, 79 | }) 80 | }) 81 | 82 | test('uses draft id if overlayDrafts is set to false', () => { 83 | const _id = 'abc123' 84 | const getNode = (id: string) => 85 | id === '-abc123' 86 | ? { 87 | _id, 88 | id: _id, 89 | bar: 'baz', 90 | parent: `someParent`, 91 | internal: { 92 | owner: `asdf`, 93 | type: `asdf`, 94 | contentDigest: `asdf`, 95 | }, 96 | children: [], 97 | } 98 | : noNode 99 | 100 | expect( 101 | resolveReferences( 102 | {foo: {_ref: `drafts.${_id}`}}, 103 | {createNodeId, getNode}, 104 | {maxDepth: 5, overlayDrafts: false}, 105 | ), 106 | ).toEqual({ 107 | foo: null, 108 | }) 109 | }) 110 | 111 | test('resolves references in arrays', () => { 112 | const _id = 'abc123' 113 | const getNode = (id: string) => 114 | id === '-abc123' 115 | ? { 116 | _id, 117 | id: _id, 118 | bar: 'baz', 119 | parent: `someParent`, 120 | internal: { 121 | owner: `asdf`, 122 | type: `asdf`, 123 | contentDigest: `asdf`, 124 | }, 125 | children: [], 126 | } 127 | : noNode 128 | 129 | expect( 130 | resolveReferences( 131 | {foo: [{_ref: _id}]}, 132 | {createNodeId, getNode}, 133 | {maxDepth: 5, overlayDrafts: true}, 134 | ), 135 | ).toEqual({ 136 | foo: [ 137 | { 138 | _id, 139 | id: _id, 140 | bar: 'baz', 141 | parent: `someParent`, 142 | children: [], 143 | }, 144 | ], 145 | }) 146 | }) 147 | 148 | test('resolves to max depth specified', () => { 149 | const _id = 'abc123' 150 | const node = { 151 | _id, 152 | id: _id, 153 | bar: 'baz', 154 | child: {_ref: _id}, 155 | parent: `someParent`, 156 | internal: { 157 | owner: `asdf`, 158 | type: `asdf`, 159 | contentDigest: `asdf`, 160 | }, 161 | children: [], 162 | } 163 | 164 | const getNode = (id: string) => (id === '-abc123' ? node : noNode) 165 | expect( 166 | resolveReferences( 167 | {foo: {_ref: _id}}, 168 | {createNodeId, getNode}, 169 | {maxDepth: 5, overlayDrafts: false}, 170 | ), 171 | ).toEqual({ 172 | foo: { 173 | _id: 'abc123', 174 | bar: 'baz', 175 | child: { 176 | _id: 'abc123', 177 | bar: 'baz', 178 | child: { 179 | _id: 'abc123', 180 | bar: 'baz', 181 | child: { 182 | _ref: 'abc123', 183 | }, 184 | id: 'abc123', 185 | parent: `someParent`, 186 | children: [], 187 | }, 188 | id: 'abc123', 189 | parent: `someParent`, 190 | children: [], 191 | }, 192 | id: 'abc123', 193 | parent: `someParent`, 194 | children: [], 195 | }, 196 | }) 197 | }) 198 | 199 | test('remaps raw fields from returned nodes', () => { 200 | const _id = 'abc123' // Sanity ID 201 | const id = '-321cba' // Gatsby ID 202 | const getNode = (id: string) => { 203 | switch (id) { 204 | case '-321cba': 205 | return { 206 | _id, 207 | id, 208 | bar: 'baz', 209 | foo: [{_ref: '-gatsbyId'}], 210 | _rawDataFoo: [{_ref: 'def'}], 211 | parent: `someParent`, 212 | internal: { 213 | owner: `asdf`, 214 | type: `asdf`, 215 | contentDigest: `asdf`, 216 | }, 217 | children: [], 218 | } 219 | case '-fed': 220 | return { 221 | _id: 'def', 222 | id: '-fed', 223 | its: 'def', 224 | parent: `someParent`, 225 | internal: { 226 | owner: `asdf`, 227 | type: `asdf`, 228 | contentDigest: `asdf`, 229 | }, 230 | children: [], 231 | } 232 | default: 233 | return noNode 234 | } 235 | } 236 | 237 | expect( 238 | resolveReferences( 239 | {foo: [{_ref: _id}]}, 240 | {createNodeId: reverse, getNode}, 241 | {maxDepth: 5, overlayDrafts: true}, 242 | ), 243 | ).toEqual({ 244 | foo: [ 245 | { 246 | _id, 247 | id, 248 | bar: 'baz', 249 | foo: [ 250 | { 251 | _id: 'def', 252 | id: '-fed', 253 | its: 'def', 254 | parent: `someParent`, 255 | children: [], 256 | }, 257 | ], 258 | parent: `someParent`, 259 | children: [], 260 | }, 261 | ], 262 | }) 263 | }) 264 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/mapCrossDatasetReferences.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parse, 3 | visit, 4 | print, 5 | Kind, 6 | type DirectiveNode, 7 | type NamedTypeNode, 8 | type UnionTypeDefinitionNode, 9 | } from 'graphql' 10 | 11 | const CDR_DIRECTIVE = 'crossDatasetReference' 12 | 13 | // Function to extract array of string value from a ValueNode 14 | const getStringArrayArgumentValuesFromDirective = (directive: DirectiveNode, argument: string) => { 15 | const values: string[] = [] 16 | for (const arg of directive.arguments || []) { 17 | if (arg.name.value === argument) { 18 | if (arg.value.kind === Kind.LIST) { 19 | arg.value.values.forEach((value) => { 20 | if (value.kind === Kind.STRING) { 21 | values.push(value.value) 22 | } 23 | }) 24 | } 25 | } 26 | } 27 | return values 28 | } 29 | 30 | const referenceDirectiveNode: DirectiveNode = { 31 | kind: Kind.DIRECTIVE, 32 | name: { 33 | kind: Kind.NAME, 34 | value: 'reference', 35 | }, 36 | } 37 | 38 | const typeNameForMapping = (typeNames: string[]) => { 39 | return typeNames.join('Or') 40 | } 41 | 42 | // This function rewrites the Schema to treat crossDatasetReference fields as 43 | // references, since we will assume the required schemas and content is added as 44 | // additional source plugin configurations. 45 | export function mapCrossDatasetReferences(api: string) { 46 | const astNode = parse(api) 47 | 48 | // First pass: Find any named types that have cdr directives on them 49 | const cdrMapping: Record = {} 50 | visit(astNode, { 51 | enter(node) {}, 52 | [Kind.OBJECT_TYPE_DEFINITION](node) { 53 | if (node.name.value) { 54 | const cdrDirective = node.directives?.find((d) => d.name.value === CDR_DIRECTIVE) 55 | if (cdrDirective) { 56 | // This is a type that has a cdr directive on it 57 | //let typeName = getStringArgumentValueFromDirective(cdrDirective, 'typeName') 58 | let typeNames = getStringArrayArgumentValuesFromDirective(cdrDirective, 'typeNames') 59 | if (typeNames) { 60 | cdrMapping[node.name.value] = typeNames 61 | } 62 | } 63 | } 64 | }, 65 | }) 66 | 67 | const unionDefinitions: UnionTypeDefinitionNode[] = [] 68 | // Find all the values in cdrMapping that have more than 1 value 69 | for (const key in cdrMapping) { 70 | if (cdrMapping[key].length > 1) { 71 | // Create a union type for this set of types 72 | unionDefinitions.push({ 73 | kind: Kind.UNION_TYPE_DEFINITION, 74 | name: { 75 | kind: Kind.NAME, 76 | value: typeNameForMapping(cdrMapping[key]), 77 | }, 78 | types: cdrMapping[key].map((key) => { 79 | return { 80 | kind: Kind.NAMED_TYPE, 81 | name: { 82 | kind: Kind.NAME, 83 | value: key, 84 | }, 85 | } 86 | }), 87 | }) 88 | } 89 | } 90 | 91 | // Add our new Union types 92 | const newAST = { 93 | ...astNode, 94 | definitions: [...astNode.definitions, ...unionDefinitions], 95 | } 96 | 97 | // Second pass: Rewrite the schema to replace CDR types with the appropriate 98 | // target type and add @reference directive where appropriate 99 | const modifiedTypes = visit(newAST, { 100 | [Kind.FIELD_DEFINITION](fieldNode) { 101 | const cdrDirective = fieldNode.directives?.find((d) => d.name.value === CDR_DIRECTIVE) 102 | let mappedTypeNames: string[] | undefined = undefined 103 | 104 | if (fieldNode.type.kind === Kind.NAMED_TYPE) { 105 | if (cdrDirective) { 106 | // This is a field that has a cdr directive on it 107 | // TODO: look up any configured typePrefixes for the dataset 108 | mappedTypeNames = getStringArrayArgumentValuesFromDirective(cdrDirective, 'typeNames') 109 | } else { 110 | // This is a field that does not have a cdr directive on it 111 | // Check if the field type is a CDR type 112 | const fieldName = fieldNode.type.name.value 113 | if (fieldName in cdrMapping) { 114 | mappedTypeNames = cdrMapping[fieldName] 115 | } 116 | } 117 | } 118 | 119 | if (fieldNode.type.kind === Kind.LIST_TYPE) { 120 | const innerType = fieldNode.type.type as NamedTypeNode // TypeScript type assertion 121 | const fieldName = innerType.name.value 122 | // Check if the field type is a CDR type 123 | if (fieldName in cdrMapping) { 124 | mappedTypeNames = cdrMapping[fieldName] 125 | } 126 | } 127 | 128 | if (mappedTypeNames) { 129 | // Replace the cdr directive with a reference directive and replace 130 | // the type name with the actual target type 131 | let directives: DirectiveNode[] = (fieldNode.directives || []).filter( 132 | (d) => d.name.value !== CDR_DIRECTIVE, 133 | ) 134 | if (cdrDirective) { 135 | // If there was a cdr directive, replace it with a reference directive 136 | directives = [...directives, referenceDirectiveNode] 137 | } 138 | 139 | if (fieldNode.type.kind === Kind.NAMED_TYPE) { 140 | return { 141 | ...fieldNode, 142 | directives, 143 | type: { 144 | ...fieldNode.type, 145 | name: { 146 | ...fieldNode.type.name, 147 | value: typeNameForMapping(mappedTypeNames), 148 | }, 149 | }, 150 | } 151 | } 152 | 153 | if (fieldNode.type.kind === Kind.LIST_TYPE) { 154 | return { 155 | ...fieldNode, 156 | type: { 157 | ...fieldNode.type, 158 | type: { 159 | kind: Kind.NAMED_TYPE, 160 | name: { 161 | kind: Kind.NAME, 162 | value: typeNameForMapping(mappedTypeNames), 163 | }, 164 | }, 165 | }, 166 | } 167 | } 168 | } 169 | return fieldNode 170 | }, 171 | [Kind.UNION_TYPE_DEFINITION](node) { 172 | // Check if any of the union types match keys in the cdrMapping 173 | const modifiedTypes = node.types?.map((typeNode) => { 174 | const typeName = typeNode.name.value 175 | if (cdrMapping[typeName]) { 176 | return { 177 | ...typeNode, 178 | name: { 179 | kind: Kind.NAME, 180 | value: cdrMapping[typeName], 181 | }, 182 | } 183 | } 184 | return typeNode // return the type unchanged if it's not in the cdrMapping 185 | }) 186 | 187 | // Return the modified union node 188 | return { 189 | ...node, 190 | types: modifiedTypes, 191 | } 192 | }, 193 | }) 194 | 195 | return print(modifiedTypes) 196 | } 197 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/remoteGraphQLSchema.ts: -------------------------------------------------------------------------------- 1 | import {get, camelCase, groupBy} from 'lodash' 2 | import { 3 | FieldDefinitionNode, 4 | GraphQLString, 5 | ListTypeNode, 6 | NamedTypeNode, 7 | NonNullTypeNode, 8 | ObjectTypeDefinitionNode, 9 | parse, 10 | ScalarTypeDefinitionNode, 11 | specifiedScalarTypes, 12 | TypeNode, 13 | UnionTypeDefinitionNode, 14 | valueFromAST, 15 | } from 'gatsby/graphql' 16 | import {SanityClient} from '@sanity/client' 17 | import {getTypeName} from './normalize' 18 | import {ErrorWithCode} from './errors' 19 | import {PluginConfig} from './validateConfig' 20 | 21 | export type FieldDef = { 22 | type: NamedTypeNode | ListTypeNode | NonNullTypeNode 23 | namedType: NamedTypeNode 24 | isList: boolean 25 | aliasFor: string | null 26 | isReference: boolean 27 | } 28 | 29 | export type ObjectTypeDef = { 30 | name: string 31 | kind: 'Object' 32 | isDocument: boolean 33 | fields: {[key: string]: FieldDef} 34 | } 35 | 36 | export type UnionTypeDef = { 37 | name: string 38 | types: string[] 39 | } 40 | 41 | export type TypeMap = { 42 | scalars: string[] 43 | objects: {[key: string]: ObjectTypeDef} 44 | unions: {[key: string]: UnionTypeDef} 45 | } 46 | 47 | export const defaultTypeMap: TypeMap = { 48 | scalars: [], 49 | objects: {}, 50 | unions: {}, 51 | } 52 | 53 | export async function getRemoteGraphQLSchema(client: SanityClient, config: PluginConfig) { 54 | const {graphqlTag} = config 55 | const {dataset} = client.config() 56 | try { 57 | const api = await client.request({ 58 | url: `/apis/graphql/${dataset}/${graphqlTag}?tag=sanity.gatsby.get-schema`, 59 | headers: {Accept: 'application/graphql'}, 60 | }) 61 | 62 | return api 63 | } catch (err: any) { 64 | const statusCode = get(err, 'response.statusCode') 65 | const errorCode = get(err, 'response.body.errorCode') 66 | const message = 67 | get(err, 'response.body.message') || get(err, 'response.statusMessage') || err.message 68 | 69 | const is404 = statusCode === 404 || /schema not found/i.test(message) 70 | const error = new ErrorWithCode( 71 | is404 72 | ? `GraphQL API not deployed - see https://github.com/sanity-io/gatsby-source-sanity#graphql-api for more info\n\n` 73 | : `${message}`, 74 | errorCode || statusCode, 75 | ) 76 | 77 | throw error 78 | } 79 | } 80 | 81 | export function getTypeMapFromGraphQLSchema(sdl: string, typePrefix: string | undefined): TypeMap { 82 | const typeMap: TypeMap = {objects: {}, scalars: [], unions: {}} 83 | const remoteSchema = parse(sdl) 84 | const groups = { 85 | ObjectTypeDefinition: [], 86 | ScalarTypeDefinition: [], 87 | UnionTypeDefinition: [], 88 | ...groupBy(remoteSchema.definitions, 'kind'), 89 | } 90 | 91 | typeMap.scalars = specifiedScalarTypes 92 | .map((scalar) => scalar.name) 93 | .concat( 94 | groups.ScalarTypeDefinition.map((typeDef: ScalarTypeDefinitionNode) => typeDef.name.value), 95 | ) 96 | 97 | const objects: {[key: string]: ObjectTypeDef} = {} 98 | typeMap.objects = groups.ObjectTypeDefinition.reduce((acc, typeDef: ObjectTypeDefinitionNode) => { 99 | if (typeDef.name.value === 'RootQuery') { 100 | return acc 101 | } 102 | 103 | const name = getTypeName(typeDef.name.value, typePrefix) 104 | acc[name] = { 105 | name, 106 | kind: 'Object', 107 | isDocument: Boolean( 108 | (typeDef.interfaces || []).find((iface) => iface.name.value === 'Document'), 109 | ), 110 | fields: (typeDef.fields || []).reduce((fields, fieldDef) => { 111 | if (isAlias(fieldDef)) { 112 | const aliasFor = getAliasDirective(fieldDef) || '' 113 | fields[aliasFor] = { 114 | type: fieldDef.type, 115 | namedType: {kind: 'NamedType' as any, name: {kind: 'Name' as any, value: 'JSON'}}, 116 | isList: false, 117 | aliasFor: null, 118 | isReference: false, 119 | } 120 | 121 | const aliasName = '_' + camelCase(`raw ${aliasFor}`) 122 | fields[aliasName] = { 123 | type: {kind: 'NamedType' as any, name: {kind: 'Name' as any, value: 'JSON'}}, 124 | namedType: {kind: 'NamedType' as any, name: {kind: 'Name' as any, value: 'JSON'}}, 125 | aliasFor, 126 | isList: false, 127 | isReference: false, 128 | } 129 | return fields 130 | } 131 | 132 | const namedType = unwrapType(fieldDef.type) 133 | fields[fieldDef.name.value] = { 134 | type: fieldDef.type, 135 | namedType, 136 | isList: isListType(fieldDef.type), 137 | aliasFor: null, 138 | isReference: Boolean(getReferenceDirective(fieldDef)), 139 | } 140 | 141 | // Add raw alias if not scalar 142 | if (!typeMap.scalars.includes(namedType.name.value)) { 143 | const aliasName = '_' + camelCase(`raw ${fieldDef.name.value}`) 144 | fields[aliasName] = { 145 | type: {kind: 'NamedType' as any, name: {kind: 'Name' as any, value: 'JSON'}}, 146 | namedType: {kind: 'NamedType' as any, name: {kind: 'Name' as any, value: 'JSON'}}, 147 | aliasFor: fieldDef.name.value, 148 | isList: false, 149 | isReference: false, 150 | } 151 | } 152 | 153 | return fields 154 | }, {} as {[key: string]: FieldDef}), 155 | } 156 | return acc 157 | }, objects) 158 | 159 | const unions: {[key: string]: UnionTypeDef} = {} 160 | typeMap.unions = groups.UnionTypeDefinition.reduce((acc, typeDef: UnionTypeDefinitionNode) => { 161 | const name = getTypeName(typeDef.name.value, typePrefix) 162 | acc[name] = { 163 | name, 164 | types: (typeDef.types || []).map((type) => getTypeName(type.name.value, typePrefix)), 165 | } 166 | return acc 167 | }, unions) 168 | 169 | return typeMap 170 | } 171 | 172 | function isAlias(field: FieldDefinitionNode): boolean { 173 | return getAliasDirective(field) !== null 174 | } 175 | 176 | function unwrapType(typeNode: TypeNode): NamedTypeNode { 177 | if (['NonNullType', 'ListType'].includes(typeNode.kind)) { 178 | const wrappedType = typeNode as NonNullTypeNode 179 | return unwrapType(wrappedType.type) 180 | } 181 | 182 | return typeNode as NamedTypeNode 183 | } 184 | 185 | function isListType(typeNode: TypeNode): boolean { 186 | if (typeNode.kind === 'ListType') { 187 | return true 188 | } 189 | 190 | if (typeNode.kind === 'NonNullType') { 191 | const node = typeNode as NonNullTypeNode 192 | return isListType(node.type) 193 | } 194 | 195 | return false 196 | } 197 | 198 | function getAliasDirective(field: FieldDefinitionNode): string | null { 199 | const alias = (field.directives || []).find((dir) => dir.name.value === 'jsonAlias') 200 | if (!alias) { 201 | return null 202 | } 203 | 204 | const forArg = (alias.arguments || []).find((arg) => arg.name.value === 'for') 205 | if (!forArg) { 206 | return null 207 | } 208 | 209 | return valueFromAST(forArg.value, GraphQLString, {}) as any 210 | } 211 | 212 | function getReferenceDirective(field: FieldDefinitionNode) { 213 | return (field.directives || []).find((dir) => dir.name.value === 'reference') 214 | } 215 | -------------------------------------------------------------------------------- /examples/shared-content-studio/sanity.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, defineType} from 'sanity' 2 | import {deskTool} from 'sanity/desk' 3 | import {visionTool} from '@sanity/vision' 4 | 5 | export default defineConfig([ 6 | { 7 | name: 'books', 8 | basePath: '/books', 9 | 10 | projectId: 'rz9j51w2', 11 | dataset: 'production', 12 | apiHost: 'https://api.sanity.work', 13 | 14 | plugins: [deskTool(), visionTool()], 15 | 16 | schema: { 17 | types: [ 18 | defineType({ 19 | name: 'bookMetadata', 20 | title: 'Book metadata', 21 | type: 'object' as const, 22 | fields: [ 23 | { 24 | name: 'isbn', 25 | title: 'ISBN', 26 | type: 'string', 27 | }, 28 | ], 29 | }), 30 | defineType({ 31 | name: 'publisherReference', 32 | title: 'Publisher', 33 | type: 'reference' as const, 34 | to: [{type: 'publisher'}], 35 | }), 36 | defineType({ 37 | name: 'authorReference', 38 | title: 'Author', 39 | type: 'crossDatasetReference' as const, 40 | dataset: 'shared', 41 | studioUrl: ({id, type}) => `/authors/desk/${type};${id}`, 42 | to: [{type: 'author', preview: {select: {title: 'name'}}}], 43 | }), 44 | defineType({ 45 | name: 'authorOrEditorReference', 46 | title: 'Author or Editor', 47 | type: 'crossDatasetReference' as const, 48 | dataset: 'shared', 49 | studioUrl: ({id, type}) => `/authors/desk/${type};${id}`, 50 | to: [ 51 | {type: 'author', preview: {select: {title: 'name'}}}, 52 | {type: 'editor', preview: {select: {title: 'name'}}}, 53 | ], 54 | }), 55 | defineType({ 56 | name: 'book', 57 | title: 'Book', 58 | type: 'document' as const, 59 | fields: [ 60 | { 61 | name: 'title', 62 | title: 'Title', 63 | type: 'string', 64 | }, 65 | { 66 | name: 'cover', 67 | title: 'Cover', 68 | type: 'image', 69 | }, 70 | { 71 | name: 'author', 72 | title: 'Author', 73 | description: 'as inline definition', 74 | type: 'crossDatasetReference' as const, 75 | dataset: 'shared', 76 | studioUrl: ({id, type}) => `/authors/desk/${type};${id}`, 77 | to: [{type: 'author', preview: {select: {title: 'name'}}}], 78 | }, 79 | { 80 | name: 'authorOrEditorInline', 81 | title: 'Author or Editor', 82 | description: 'as inline definition', 83 | type: 'crossDatasetReference' as const, 84 | dataset: 'shared', 85 | studioUrl: ({id, type}) => `/authors/desk/${type};${id}`, 86 | to: [ 87 | {type: 'author', preview: {select: {title: 'name'}}}, 88 | {type: 'editor', preview: {select: {title: 'name'}}}, 89 | ], 90 | }, 91 | { 92 | name: 'authorOrEditor', 93 | description: 'as named type authorOrEditorReference', 94 | type: 'authorOrEditorReference', 95 | }, 96 | { 97 | name: 'extraAuthor', 98 | title: 'Author', 99 | description: 'as named type authorReference', 100 | type: 'authorReference', 101 | }, 102 | { 103 | name: 'coauthors', 104 | title: 'Coauthors', 105 | type: 'array', 106 | of: [{type: 'authorReference'}], 107 | }, 108 | { 109 | name: 'mixedArray', 110 | type: 'array', 111 | of: [ 112 | {type: 'bookMetadata'}, 113 | {type: 'authorReference'}, 114 | {type: 'reference', to: [{type: 'book'}]}, 115 | ], 116 | }, 117 | { 118 | name: 'genres', 119 | title: 'Genre', 120 | type: 'array', 121 | of: [ 122 | { 123 | type: 'reference', 124 | to: [{type: 'genre'}], 125 | }, 126 | ], 127 | }, 128 | { 129 | name: 'publisher', 130 | title: 'Publisher', 131 | type: 'reference', 132 | to: [{type: 'publisher'}], 133 | }, 134 | { 135 | name: 'extraPublisher', 136 | title: 'Publisher as publisherReference type', 137 | type: 'publisherReference', 138 | }, 139 | { 140 | name: 'blurb', 141 | title: 'Blurb', 142 | type: 'array', 143 | of: [ 144 | { 145 | type: 'block', 146 | of: [ 147 | { 148 | type: 'authorReference', 149 | }, 150 | { 151 | type: 'reference', 152 | to: [ 153 | { 154 | type: 'publisher', 155 | }, 156 | ], 157 | }, 158 | ], 159 | }, 160 | { 161 | type: 'authorReference', 162 | }, 163 | { 164 | type: 'bookMetadata', 165 | }, 166 | ], 167 | }, 168 | ], 169 | }), 170 | defineType({ 171 | name: 'genre', 172 | title: 'Genre', 173 | type: 'document' as const, 174 | fields: [ 175 | { 176 | name: 'title', 177 | title: 'Title', 178 | type: 'string', 179 | }, 180 | ], 181 | }), 182 | defineType({ 183 | name: 'publisher', 184 | title: 'Publisher', 185 | type: 'document' as const, 186 | fields: [ 187 | { 188 | name: 'name', 189 | title: 'Name', 190 | type: 'string', 191 | }, 192 | ], 193 | }), 194 | ], 195 | }, 196 | }, 197 | { 198 | name: 'authors', 199 | basePath: '/authors', 200 | 201 | projectId: 'rz9j51w2', 202 | dataset: 'shared', 203 | apiHost: 'https://api.sanity.work', 204 | 205 | plugins: [deskTool(), visionTool()], 206 | 207 | schema: { 208 | types: [ 209 | defineType({ 210 | name: 'editor', 211 | type: 'document' as const, 212 | fields: [ 213 | { 214 | name: 'name', 215 | title: 'Name', 216 | type: 'string', 217 | }, 218 | ], 219 | }), 220 | defineType({ 221 | name: 'author', 222 | title: 'Author', 223 | type: 'document' as const, 224 | fields: [ 225 | { 226 | name: 'name', 227 | title: 'Name', 228 | type: 'string', 229 | }, 230 | { 231 | name: 'profilePicture', 232 | title: 'Profile picture', 233 | type: 'image', 234 | }, 235 | ], 236 | }), 237 | ], 238 | }, 239 | }, 240 | ]) 241 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/normalize.ts: -------------------------------------------------------------------------------- 1 | import {Actions, NodePluginArgs} from 'gatsby' 2 | import {extractWithPath} from '@sanity/mutator' 3 | import {specifiedScalarTypes} from 'gatsby/graphql' 4 | import {get, set, startCase, camelCase, cloneDeep, upperFirst} from 'lodash' 5 | import {SanityDocument} from '../types/sanity' 6 | import {safeId, unprefixId} from './documentIds' 7 | import {TypeMap} from './remoteGraphQLSchema' 8 | import {SanityInputNode} from '../types/gatsby' 9 | 10 | import imageUrlBuilder from '@sanity/image-url' 11 | import {SanityClient} from '@sanity/client' 12 | 13 | const scalarTypeNames = specifiedScalarTypes.map((def) => def.name).concat(['JSON', 'Date']) 14 | 15 | // Node fields used internally by Gatsby. 16 | export const RESTRICTED_NODE_FIELDS = ['id', 'children', 'parent', 'fields', 'internal'] 17 | 18 | export interface ProcessingOptions { 19 | typeMap: TypeMap 20 | createNode: Actions['createNode'] 21 | createNodeId: NodePluginArgs['createNodeId'] 22 | createContentDigest: NodePluginArgs['createContentDigest'] 23 | createParentChildLink: Actions['createParentChildLink'] 24 | overlayDrafts: boolean 25 | client: SanityClient 26 | typePrefix?: string 27 | } 28 | 29 | // Transform a Sanity document into a Gatsby node 30 | export function toGatsbyNode(doc: SanityDocument, options: ProcessingOptions): SanityInputNode { 31 | const {createNodeId, createContentDigest, overlayDrafts} = options 32 | 33 | const rawAliases = getRawAliases(doc, options) 34 | const safe = prefixConflictingKeys(doc, options.typePrefix) 35 | const withRefs = rewriteNodeReferences(safe, options) 36 | 37 | addInternalTypesToUnionFields(withRefs, options) 38 | 39 | const type = getTypeName(doc._type, options.typePrefix) 40 | const urlBuilder = imageUrlBuilder(options.client) 41 | 42 | const gatsbyImageCdnFields = [`SanityImageAsset`, `SanityFileAsset`].includes(type) 43 | ? { 44 | filename: withRefs.originalFilename, 45 | width: withRefs?.metadata?.dimensions?.width, 46 | height: withRefs?.metadata?.dimensions?.height, 47 | url: withRefs?.url, 48 | placeholderUrl: 49 | type === `SanityImageAsset` 50 | ? urlBuilder 51 | .image(withRefs.url) 52 | .width(20) 53 | .height(30) 54 | .quality(80) 55 | .url() 56 | // this makes placeholder urls dynamic in the gatsbyImage resolver 57 | ?.replace(`w=20`, `w=%width%`) 58 | ?.replace(`h=30`, `h=%height%`) 59 | : null, 60 | } 61 | : {} 62 | 63 | return { 64 | ...withRefs, 65 | ...rawAliases, 66 | ...gatsbyImageCdnFields, 67 | 68 | id: safeId(overlayDrafts ? unprefixId(doc._id) : doc._id, createNodeId), 69 | children: [], 70 | internal: { 71 | type, 72 | contentDigest: createContentDigest(JSON.stringify(withRefs)), 73 | }, 74 | } 75 | } 76 | 77 | // movie => SanityMovie 78 | // blog_post => SanityBlogPost 79 | // sanity.imageAsset => SanityImageAsset 80 | export function getTypeName(type: string, typePrefix: string | undefined) { 81 | if (!type) { 82 | return type 83 | } 84 | 85 | if (typePrefix && type.startsWith(typePrefix)) { 86 | return type 87 | } 88 | 89 | const typeName = startCase(type) 90 | if (scalarTypeNames.includes(typeName)) { 91 | return typeName 92 | } 93 | 94 | const sanitized = typeName.replace(/\s+/g, '') 95 | 96 | const prefix = `${typePrefix ?? ''}${sanitized.startsWith('Sanity') ? '' : 'Sanity'}` 97 | return sanitized.startsWith(prefix) ? sanitized : `${prefix}${sanitized}` 98 | } 99 | 100 | // {foo: 'bar', children: []} => {foo: 'bar', sanityChildren: []} 101 | function prefixConflictingKeys(obj: SanityDocument, typePrefix: string | undefined) { 102 | // Will be overwritten, but initialize for type safety 103 | const initial: SanityDocument = {_id: '', _type: '', _rev: '', _createdAt: '', _updatedAt: ''} 104 | 105 | return Object.keys(obj).reduce((target, key) => { 106 | const targetKey = getConflictFreeFieldName(key, typePrefix) 107 | target[targetKey] = obj[key] 108 | 109 | return target 110 | }, initial) 111 | } 112 | 113 | export function getConflictFreeFieldName(fieldName: string, typePrefix: string | undefined) { 114 | return RESTRICTED_NODE_FIELDS.includes(fieldName) 115 | ? `${camelCase(typePrefix)}${upperFirst(fieldName)}` 116 | : fieldName 117 | } 118 | 119 | function getRawAliases(doc: SanityDocument, options: ProcessingOptions) { 120 | const {typeMap} = options 121 | const typeName = getTypeName(doc._type, options.typePrefix) 122 | const type = typeMap.objects[typeName] 123 | if (!type) { 124 | return {} 125 | } 126 | const initial: {[key: string]: any} = {} 127 | return Object.keys(type.fields).reduce((acc, fieldName) => { 128 | const field = type.fields[fieldName] 129 | const namedType = field.namedType.name.value 130 | if (field.aliasFor) { 131 | const aliasName = '_' + camelCase(`raw_data_${field.aliasFor}`) 132 | acc[aliasName] = doc[field.aliasFor] 133 | return acc 134 | } 135 | if (typeMap.scalars.includes(namedType)) { 136 | return acc 137 | } 138 | const aliasName = '_' + camelCase(`raw_data_${fieldName}`) 139 | acc[aliasName] = doc[fieldName] 140 | return acc 141 | }, initial) 142 | } 143 | 144 | // Tranform Sanity refs ({_ref: 'foo'}) to Gatsby refs ({_ref: 'someOtherId'}) 145 | function rewriteNodeReferences(doc: SanityDocument, options: ProcessingOptions) { 146 | const {createNodeId} = options 147 | 148 | const refs = extractWithPath('..[_ref]', doc) 149 | if (refs.length === 0) { 150 | return doc 151 | } 152 | 153 | const newDoc = cloneDeep(doc) 154 | refs.forEach((match) => { 155 | set(newDoc, match.path, safeId(match.value, createNodeId)) 156 | }) 157 | 158 | return newDoc 159 | } 160 | // Adds `internal: { type: 'TheTypeName' }` to union fields nodes, to allow runtime 161 | // type resolution. 162 | function addInternalTypesToUnionFields(doc: SanityDocument, options: ProcessingOptions) { 163 | const {typeMap} = options 164 | const types = extractWithPath('..[_type]', doc) 165 | 166 | const typeName = getTypeName(doc._type, options.typePrefix) 167 | const thisType = typeMap.objects[typeName] 168 | if (!thisType) { 169 | return 170 | } 171 | 172 | for (const type of types) { 173 | // Not needed for references or root objects 174 | if (type.value === 'reference' || type.path.length < 2) { 175 | continue 176 | } 177 | 178 | // extractWithPath returns integers to indicate array indices for list types 179 | const isListType = Number.isInteger(type.path[type.path.length - 2]) 180 | 181 | // For list types we need to go up an extra level to get the actual field name 182 | const parentOffset = isListType ? 3 : 2 183 | 184 | const parentNode = 185 | type.path.length === parentOffset ? doc : get(doc, type.path.slice(0, -parentOffset)) 186 | const parentTypeName = getTypeName(parentNode._type, options.typePrefix) 187 | const parentType = typeMap.objects[parentTypeName] 188 | 189 | if (!parentType) { 190 | continue 191 | } 192 | 193 | const field = parentType.fields[type.path[type.path.length - parentOffset]] 194 | 195 | if (!field) { 196 | continue 197 | } 198 | 199 | const fieldTypeName = getTypeName(field.namedType.name.value, options.typePrefix) 200 | 201 | // All this was just to check if we're dealing with a union field 202 | if (!typeMap.unions[fieldTypeName]) { 203 | continue 204 | } 205 | const typeName = getTypeName(type.value, options.typePrefix) 206 | 207 | // Add the internal type to the field 208 | set(doc, type.path.slice(0, -1).concat('internal'), {type: typeName}) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/util/rewriteGraphQLSchema.ts: -------------------------------------------------------------------------------- 1 | import {Reporter} from 'gatsby' 2 | import { 3 | ASTNode, 4 | DefinitionNode, 5 | DocumentNode, 6 | FieldDefinitionNode, 7 | GraphQLString, 8 | InterfaceTypeDefinitionNode, 9 | NamedTypeNode, 10 | NameNode, 11 | NonNullTypeNode, 12 | ObjectTypeDefinitionNode, 13 | parse, 14 | print, 15 | TypeDefinitionNode, 16 | TypeNode, 17 | UnionTypeDefinitionNode, 18 | valueFromAST, 19 | DirectiveNode, 20 | ScalarTypeDefinitionNode, 21 | specifiedScalarTypes, 22 | Kind, 23 | } from 'gatsby/graphql' 24 | import {camelCase} from 'lodash' 25 | import {RESTRICTED_NODE_FIELDS, getConflictFreeFieldName, getTypeName} from './normalize' 26 | import {PluginConfig} from './validateConfig' 27 | 28 | interface AstRewriterContext { 29 | reporter: Reporter 30 | config: PluginConfig 31 | } 32 | 33 | const builtins = ['ID', 'String', 'Boolean', 'Int', 'Float', 'JSON', 'DateTime', 'Date'] 34 | const wantedNodeTypes = ['ObjectTypeDefinition', 'UnionTypeDefinition', 'InterfaceTypeDefinition'] 35 | 36 | export const rewriteGraphQLSchema = (schemaSdl: string, context: AstRewriterContext): string => { 37 | const ast = parse(schemaSdl) 38 | const transformedAst = transformAst(ast, context) 39 | const transformed = print(transformedAst) 40 | return transformed 41 | } 42 | 43 | function transformAst(ast: DocumentNode, context: AstRewriterContext): ASTNode { 44 | return { 45 | ...ast, 46 | definitions: ast.definitions 47 | .filter(isWantedAstNode) 48 | .map((node) => transformDefinitionNode(node, context, ast)) 49 | .concat(getResolveReferencesConfigType()), 50 | } 51 | } 52 | 53 | function isWantedAstNode(astNode: DefinitionNode) { 54 | const node = astNode as TypeDefinitionNode 55 | return wantedNodeTypes.includes(node.kind) && node.name.value !== 'RootQuery' 56 | } 57 | 58 | function transformDefinitionNode( 59 | node: DefinitionNode, 60 | context: AstRewriterContext, 61 | ast: DocumentNode, 62 | ): DefinitionNode { 63 | switch (node.kind) { 64 | case 'ObjectTypeDefinition': 65 | return transformObjectTypeDefinition(node, context, ast) 66 | case 'UnionTypeDefinition': 67 | return transformUnionTypeDefinition(node, context) 68 | case 'InterfaceTypeDefinition': 69 | return transformInterfaceTypeDefinition(node, context) as any 70 | default: 71 | return node 72 | } 73 | } 74 | 75 | function transformObjectTypeDefinition( 76 | node: ObjectTypeDefinitionNode, 77 | context: AstRewriterContext, 78 | ast: DocumentNode, 79 | ): ObjectTypeDefinitionNode { 80 | const scalars = ast.definitions 81 | .filter((def): def is ScalarTypeDefinitionNode => def.kind === 'ScalarTypeDefinition') 82 | .map((scalar) => scalar.name.value) 83 | .concat(specifiedScalarTypes.map((scalar) => scalar.name)) 84 | 85 | const fields = node.fields || [] 86 | const jsonTargets = fields 87 | .map(getJsonAliasTarget) 88 | .filter((target): target is string => target !== null) 89 | 90 | const blockFields = jsonTargets.map((target) => makeBlockField(target, context)) 91 | const interfaces = (node.interfaces || []).map((iface) => 92 | maybeRewriteType(iface, context), 93 | ) as NamedTypeNode[] 94 | const rawFields = getRawFields(fields, scalars) 95 | 96 | // Implement Gatsby node interface if it is a document 97 | if (isDocumentType(node, context)) { 98 | interfaces.push({kind: Kind.NAMED_TYPE, name: {kind: Kind.NAME, value: 'Node'}}) 99 | } 100 | 101 | return { 102 | ...node, 103 | name: {...node.name, value: getTypeName(node.name.value, context.config.typePrefix)}, 104 | interfaces, 105 | directives: [{kind: Kind.DIRECTIVE, name: {kind: Kind.NAME, value: 'dontInfer'}}], 106 | fields: [ 107 | ...fields 108 | .filter((field) => !isJsonAlias(field)) 109 | .map((field) => transformFieldNodeAst(field, node, context)), 110 | ...blockFields, 111 | ...rawFields, 112 | ] as any, 113 | } 114 | } 115 | 116 | function getRawFields( 117 | fields: readonly FieldDefinitionNode[], 118 | scalars: string[], 119 | ): FieldDefinitionNode[] { 120 | return fields 121 | .filter((field) => isJsonAlias(field) || !isScalar(field, scalars)) 122 | .reduce((acc, field) => { 123 | const jsonAlias = getJsonAliasTarget(field) 124 | const name = jsonAlias || field.name.value 125 | 126 | acc.push({ 127 | kind: field.kind, 128 | name: {kind: Kind.NAME, value: '_' + camelCase(`raw ${name}`)}, 129 | type: {kind: Kind.NAMED_TYPE, name: {kind: Kind.NAME, value: 'JSON'}}, 130 | arguments: [ 131 | { 132 | kind: Kind.INPUT_VALUE_DEFINITION, 133 | name: {kind: Kind.NAME, value: 'resolveReferences'}, 134 | type: { 135 | kind: Kind.NAMED_TYPE, 136 | name: {kind: Kind.NAME, value: 'SanityResolveReferencesConfiguration'}, 137 | }, 138 | }, 139 | ], 140 | }) 141 | 142 | return acc 143 | }, [] as FieldDefinitionNode[]) 144 | } 145 | 146 | function isScalar(field: FieldDefinitionNode, scalars: string[]) { 147 | return scalars.includes(unwrapType(field.type).name.value) 148 | } 149 | 150 | function transformUnionTypeDefinition( 151 | node: UnionTypeDefinitionNode, 152 | context: AstRewriterContext, 153 | ): UnionTypeDefinitionNode { 154 | return { 155 | ...node, 156 | types: (node.types || []).map((type) => maybeRewriteType(type, context)) as NamedTypeNode[], 157 | name: {...node.name, value: getTypeName(node.name.value, context.config.typePrefix)}, 158 | } 159 | } 160 | 161 | function transformInterfaceTypeDefinition( 162 | node: InterfaceTypeDefinitionNode, 163 | context: AstRewriterContext, 164 | ) { 165 | const fields = node.fields || [] 166 | return { 167 | ...node, 168 | fields: fields.map((field) => transformFieldNodeAst(field, node, context)), 169 | name: {...node.name, value: getTypeName(node.name.value, context.config.typePrefix)}, 170 | } 171 | } 172 | 173 | function unwrapType(typeNode: TypeNode): NamedTypeNode { 174 | if (['NonNullType', 'ListType'].includes(typeNode.kind)) { 175 | const wrappedType = typeNode as NonNullTypeNode 176 | return unwrapType(wrappedType.type) 177 | } 178 | 179 | return typeNode as NamedTypeNode 180 | } 181 | 182 | function getJsonAliasTarget(field: FieldDefinitionNode): string | null { 183 | const alias = (field.directives || []).find((dir) => dir.name.value === 'jsonAlias') 184 | if (!alias) { 185 | return null 186 | } 187 | 188 | const forArg = (alias.arguments || []).find((arg) => arg.name.value === 'for') 189 | if (!forArg) { 190 | return null 191 | } 192 | 193 | return valueFromAST(forArg.value, GraphQLString, {}) as any 194 | } 195 | 196 | function isJsonAlias(field: FieldDefinitionNode): boolean { 197 | return getJsonAliasTarget(field) !== null 198 | } 199 | 200 | function makeBlockField(name: string, context: AstRewriterContext): FieldDefinitionNode { 201 | return { 202 | kind: Kind.FIELD_DEFINITION, 203 | name: { 204 | kind: Kind.NAME, 205 | value: name, 206 | }, 207 | arguments: [], 208 | directives: [], 209 | type: { 210 | kind: Kind.LIST_TYPE, 211 | type: { 212 | kind: Kind.NAMED_TYPE, 213 | name: { 214 | kind: Kind.NAME, 215 | value: getTypeName('Block', context.config.typePrefix), 216 | }, 217 | }, 218 | }, 219 | } 220 | } 221 | 222 | function makeNullable(nodeType: TypeNode, context: AstRewriterContext): TypeNode { 223 | if (nodeType.kind === 'NamedType') { 224 | return maybeRewriteType(nodeType, context) 225 | } 226 | 227 | if (nodeType.kind === 'ListType') { 228 | const unwrapped = maybeRewriteType(unwrapType(nodeType), context) 229 | return { 230 | kind: Kind.LIST_TYPE, 231 | type: makeNullable(unwrapped, context), 232 | } 233 | } 234 | 235 | return maybeRewriteType(nodeType.type, context) as NamedTypeNode 236 | } 237 | 238 | function isReferenceField(field: FieldDefinitionNode): boolean { 239 | return (field.directives || []).some((dir) => dir.name.value === 'reference') 240 | } 241 | 242 | function transformFieldNodeAst( 243 | node: FieldDefinitionNode, 244 | parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, 245 | context: AstRewriterContext, 246 | ) { 247 | const field = { 248 | ...node, 249 | name: maybeRewriteFieldName(node, parent, context), 250 | type: rewireIdType(makeNullable(node.type, context)), 251 | description: undefined, 252 | directives: [] as DirectiveNode[], 253 | } 254 | 255 | if (field.type.kind === 'NamedType' && field.type.name.value === 'Date') { 256 | field.directives.push({ 257 | kind: Kind.DIRECTIVE, 258 | name: {kind: Kind.NAME, value: 'dateformat'}, 259 | }) 260 | } 261 | 262 | if (isReferenceField(node)) { 263 | field.directives.push({ 264 | kind: Kind.DIRECTIVE, 265 | name: {kind: Kind.NAME, value: 'link'}, 266 | arguments: [ 267 | { 268 | kind: Kind.ARGUMENT, 269 | name: {kind: Kind.NAME, value: 'from'}, 270 | value: {kind: Kind.STRING, value: `${field.name.value}._ref`}, 271 | }, 272 | ], 273 | }) 274 | } 275 | 276 | return field 277 | } 278 | 279 | function rewireIdType(nodeType: TypeNode): TypeNode { 280 | if (nodeType.kind === 'NamedType' && nodeType.name.value === 'ID') { 281 | return {...nodeType, name: {kind: Kind.NAME, value: 'String'}} 282 | } 283 | 284 | return nodeType 285 | } 286 | function maybeRewriteType(nodeType: TypeNode, context: AstRewriterContext): TypeNode { 287 | const type = nodeType as NamedTypeNode 288 | if (typeof type.name === 'undefined') { 289 | return nodeType 290 | } 291 | 292 | // Gatsby has a date type, but not a datetime, so rewire it 293 | if (type.name.value === 'DateTime') { 294 | return {...type, name: {kind: Kind.NAME, value: 'Date'}} 295 | } 296 | 297 | if (builtins.includes(type.name.value)) { 298 | return type 299 | } 300 | 301 | return { 302 | ...type, 303 | name: {kind: Kind.NAME, value: getTypeName(type.name.value, context.config.typePrefix)}, 304 | } 305 | } 306 | 307 | function maybeRewriteFieldName( 308 | field: FieldDefinitionNode, 309 | parent: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode, 310 | context: AstRewriterContext, 311 | ): NameNode { 312 | if (!RESTRICTED_NODE_FIELDS.includes(field.name.value)) { 313 | return field.name 314 | } 315 | 316 | if (parent.kind === 'ObjectTypeDefinition' && !isDocumentType(parent, context)) { 317 | return field.name 318 | } 319 | 320 | const parentTypeName = parent.name.value 321 | const newFieldName = getConflictFreeFieldName(field.name.value, context.config.typePrefix) 322 | 323 | context.reporter.warn( 324 | `[sanity] Type \`${parentTypeName}\` has field with name \`${field.name.value}\`, which conflicts with Gatsby's internal properties. Renaming to \`${newFieldName}\``, 325 | ) 326 | 327 | return { 328 | ...field.name, 329 | value: newFieldName, 330 | } 331 | } 332 | 333 | function isDocumentType(node: ObjectTypeDefinitionNode, context: AstRewriterContext): boolean { 334 | const docTypes = [ 335 | getTypeName('SanityDocument', context.config.typePrefix), 336 | 'SanityDocument', 337 | 'Document', 338 | ] 339 | return (node.interfaces || []).some( 340 | (iface) => iface.kind === 'NamedType' && docTypes.includes(iface.name.value), 341 | ) 342 | } 343 | 344 | function getResolveReferencesConfigType(): DefinitionNode { 345 | return { 346 | kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, 347 | name: {kind: Kind.NAME, value: 'SanityResolveReferencesConfiguration'}, 348 | fields: [ 349 | { 350 | kind: Kind.INPUT_VALUE_DEFINITION, 351 | name: {kind: Kind.NAME, value: 'maxDepth'}, 352 | type: { 353 | kind: Kind.NON_NULL_TYPE, 354 | type: {kind: Kind.NAMED_TYPE, name: {kind: Kind.NAME, value: 'Int'}}, 355 | }, 356 | description: {kind: Kind.STRING, value: 'Max depth to resolve references to'}, 357 | }, 358 | ], 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/test/__snapshots__/getGatsbyImageProps.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`[id] gatsbyImageData constrained jpg with width (600) 1`] = ` 4 | { 5 | "backgroundColor": undefined, 6 | "height": 400, 7 | "images": { 8 | "fallback": { 9 | "sizes": "(min-width: 300px) 300px, 100vw", 10 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 11 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 12 | }, 13 | "sources": [], 14 | }, 15 | "layout": "constrained", 16 | "width": 600, 17 | } 18 | `; 19 | 20 | exports[`[id] gatsbyImageData fixed jpg 1`] = ` 21 | { 22 | "backgroundColor": undefined, 23 | "height": 200, 24 | "images": { 25 | "fallback": { 26 | "sizes": "300px", 27 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 28 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 29 | }, 30 | "sources": [], 31 | }, 32 | "layout": "fixed", 33 | "width": 300, 34 | } 35 | `; 36 | 37 | exports[`[id] gatsbyImageData fullWidth jpg 1`] = ` 38 | { 39 | "backgroundColor": undefined, 40 | "height": 0.6666666666666666, 41 | "images": { 42 | "fallback": { 43 | "sizes": "100vw", 44 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 45 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 46 | }, 47 | "sources": [], 48 | }, 49 | "layout": "fullWidth", 50 | "width": 1, 51 | } 52 | `; 53 | 54 | exports[`[id] gatsbyImageData jpeg with width/height (300x300) > original size, same aspect 1`] = ` 55 | { 56 | "backgroundColor": undefined, 57 | "height": 300, 58 | "images": { 59 | "fallback": { 60 | "sizes": "(min-width: 70px) 70px, 100vw", 61 | "src": "https://cdn.sanity.io/images/projectId/dataset/bf1942-70x70.jpg?w=70&h=70&auto=format", 62 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/bf1942-70x70.jpg?w=70&h=70&auto=format 70w", 63 | }, 64 | "sources": [], 65 | }, 66 | "layout": "constrained", 67 | "width": 300, 68 | } 69 | `; 70 | 71 | exports[`[id] gatsbyImageData jpeg with width/height (320x240) > original size, different aspect 1`] = ` 72 | { 73 | "backgroundColor": undefined, 74 | "height": 240, 75 | "images": { 76 | "fallback": { 77 | "sizes": "(min-width: 70px) 70px, 100vw", 78 | "src": "https://cdn.sanity.io/images/projectId/dataset/bf1942-70x70.jpg?rect=0,9,70,53&w=70&h=53&auto=format", 79 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/bf1942-70x70.jpg?rect=0,9,70,53&w=70&h=53&auto=format 70w", 80 | }, 81 | "sources": [], 82 | }, 83 | "layout": "constrained", 84 | "width": 320, 85 | } 86 | `; 87 | 88 | exports[`[id] gatsbyImageData jpg without params 1`] = ` 89 | { 90 | "backgroundColor": undefined, 91 | "height": 200, 92 | "images": { 93 | "fallback": { 94 | "sizes": "(min-width: 300px) 300px, 100vw", 95 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 96 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 97 | }, 98 | "sources": [], 99 | }, 100 | "layout": "constrained", 101 | "width": 300, 102 | } 103 | `; 104 | 105 | exports[`[id] gatsbyImageData, jpeg with width (300) > original size 1`] = ` 106 | { 107 | "backgroundColor": undefined, 108 | "height": 300, 109 | "images": { 110 | "fallback": { 111 | "sizes": "(min-width: 70px) 70px, 100vw", 112 | "src": "https://cdn.sanity.io/images/projectId/dataset/bf1942-70x70.jpg?w=70&h=70&auto=format", 113 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/bf1942-70x70.jpg?w=70&h=70&auto=format 70w", 114 | }, 115 | "sources": [], 116 | }, 117 | "layout": "constrained", 118 | "width": 300, 119 | } 120 | `; 121 | 122 | exports[`[ref] gatabyImageData jpg without params 1`] = ` 123 | { 124 | "backgroundColor": undefined, 125 | "height": 200, 126 | "images": { 127 | "fallback": { 128 | "sizes": "(min-width: 300px) 300px, 100vw", 129 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 130 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 131 | }, 132 | "sources": [], 133 | }, 134 | "layout": "constrained", 135 | "width": 300, 136 | } 137 | `; 138 | 139 | exports[`[ref] gatsbyImageData constrained jpg with width (600) 1`] = ` 140 | { 141 | "backgroundColor": undefined, 142 | "height": 400, 143 | "images": { 144 | "fallback": { 145 | "sizes": "(min-width: 300px) 300px, 100vw", 146 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 147 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 148 | }, 149 | "sources": [], 150 | }, 151 | "layout": "constrained", 152 | "width": 600, 153 | } 154 | `; 155 | 156 | exports[`[ref] gatsbyImageData fixed jpg 1`] = ` 157 | { 158 | "backgroundColor": undefined, 159 | "height": 200, 160 | "images": { 161 | "fallback": { 162 | "sizes": "300px", 163 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 164 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 165 | }, 166 | "sources": [], 167 | }, 168 | "layout": "fixed", 169 | "width": 300, 170 | } 171 | `; 172 | 173 | exports[`[ref] gatsbyImageData fullWidth jpg 1`] = ` 174 | { 175 | "backgroundColor": undefined, 176 | "height": 0.6666666666666666, 177 | "images": { 178 | "fallback": { 179 | "sizes": "100vw", 180 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 181 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 182 | }, 183 | "sources": [], 184 | }, 185 | "layout": "fullWidth", 186 | "width": 1, 187 | } 188 | `; 189 | 190 | exports[`[resolved] gatabyImageData jpg without params 1`] = ` 191 | { 192 | "backgroundColor": undefined, 193 | "height": 200, 194 | "images": { 195 | "fallback": { 196 | "sizes": "(min-width: 300px) 300px, 100vw", 197 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 198 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 199 | }, 200 | "sources": [], 201 | }, 202 | "layout": "constrained", 203 | "width": 300, 204 | } 205 | `; 206 | 207 | exports[`[resolved] gatsbyImageData blurred placeholder 1`] = ` 208 | { 209 | "backgroundColor": undefined, 210 | "height": 200, 211 | "images": { 212 | "fallback": { 213 | "sizes": "(min-width: 300px) 300px, 100vw", 214 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 215 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 216 | }, 217 | "sources": [], 218 | }, 219 | "layout": "constrained", 220 | "placeholder": { 221 | "fallback": "data:image/jpeg;base64,someString", 222 | }, 223 | "width": 300, 224 | } 225 | `; 226 | 227 | exports[`[resolved] gatsbyImageData constrained jpg with width (600) 1`] = ` 228 | { 229 | "backgroundColor": undefined, 230 | "height": 400, 231 | "images": { 232 | "fallback": { 233 | "sizes": "(min-width: 300px) 300px, 100vw", 234 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 235 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 236 | }, 237 | "sources": [], 238 | }, 239 | "layout": "constrained", 240 | "width": 600, 241 | } 242 | `; 243 | 244 | exports[`[resolved] gatsbyImageData dominantColor placeholder 1`] = ` 245 | { 246 | "backgroundColor": "rebeccapurple", 247 | "height": 200, 248 | "images": { 249 | "fallback": { 250 | "sizes": "(min-width: 300px) 300px, 100vw", 251 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 252 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 253 | }, 254 | "sources": [], 255 | }, 256 | "layout": "constrained", 257 | "width": 300, 258 | } 259 | `; 260 | 261 | exports[`[resolved] gatsbyImageData fixed jpg 1`] = ` 262 | { 263 | "backgroundColor": undefined, 264 | "height": 200, 265 | "images": { 266 | "fallback": { 267 | "sizes": "300px", 268 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 269 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 270 | }, 271 | "sources": [], 272 | }, 273 | "layout": "fixed", 274 | "width": 300, 275 | } 276 | `; 277 | 278 | exports[`[resolved] gatsbyImageData fullWidth jpg 1`] = ` 279 | { 280 | "backgroundColor": undefined, 281 | "height": 0.6666666666666666, 282 | "images": { 283 | "fallback": { 284 | "sizes": "100vw", 285 | "src": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format", 286 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/abc123-300x200.jpg?w=300&h=200&auto=format 300w", 287 | }, 288 | "sources": [], 289 | }, 290 | "layout": "fullWidth", 291 | "width": 1, 292 | } 293 | `; 294 | 295 | exports[`[resolved] gatsbyImageData webp dominant color 1`] = ` 296 | { 297 | "backgroundColor": undefined, 298 | "height": 2832, 299 | "images": { 300 | "fallback": { 301 | "sizes": "(min-width: 4240px) 4240px, 100vw", 302 | "src": "https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=4240&h=2832&auto=format", 303 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=3,0,4235,2832&w=320&h=214&auto=format 320w, 304 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=1,0,4238,2832&w=654&h=437&auto=format 654w, 305 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=768&h=513&auto=format 768w, 306 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=1024&h=684&auto=format 1024w, 307 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=0,1,4240,2831&w=1366&h=912&auto=format 1366w, 308 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=1,0,4239,2832&w=1600&h=1069&auto=format 1600w, 309 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=0,1,4240,2831&w=1920&h=1282&auto=format 1920w, 310 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=2048&h=1368&auto=format 2048w, 311 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=2560&h=1710&auto=format 2560w, 312 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=1,0,4239,2832&w=3440&h=2298&auto=format 3440w, 313 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=3840&h=2565&auto=format 3840w, 314 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=4096&h=2736&auto=format 4096w, 315 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=4240&h=2832&auto=format 4240w", 316 | }, 317 | "sources": [], 318 | }, 319 | "layout": "constrained", 320 | "width": 4240, 321 | } 322 | `; 323 | 324 | exports[`[resolved] gatsbyImageData webp fullWidth 1`] = ` 325 | { 326 | "backgroundColor": undefined, 327 | "height": 0.6679245283018868, 328 | "images": { 329 | "fallback": { 330 | "sizes": "100vw", 331 | "src": "https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=3,0,4235,2832&w=320&h=214&auto=format", 332 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=3,0,4235,2832&w=320&h=214&auto=format 320w, 333 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=1,0,4238,2832&w=654&h=437&auto=format 654w, 334 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=768&h=513&auto=format 768w, 335 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=1024&h=684&auto=format 1024w, 336 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=0,1,4240,2831&w=1366&h=912&auto=format 1366w, 337 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=1,0,4239,2832&w=1600&h=1069&auto=format 1600w, 338 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=0,1,4240,2831&w=1920&h=1282&auto=format 1920w, 339 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=2048&h=1368&auto=format 2048w, 340 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=2560&h=1710&auto=format 2560w, 341 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=1,0,4239,2832&w=3440&h=2298&auto=format 3440w, 342 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=3840&h=2565&auto=format 3840w, 343 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=4096&h=2736&auto=format 4096w", 344 | }, 345 | "sources": [], 346 | }, 347 | "layout": "fullWidth", 348 | "width": 1, 349 | } 350 | `; 351 | 352 | exports[`[resolved] gatsbyImageData webp without params 1`] = ` 353 | { 354 | "backgroundColor": undefined, 355 | "height": 2832, 356 | "images": { 357 | "fallback": { 358 | "sizes": "(min-width: 4240px) 4240px, 100vw", 359 | "src": "https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=4240&h=2832&auto=format", 360 | "srcSet": "https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=3,0,4235,2832&w=320&h=214&auto=format 320w, 361 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=1,0,4238,2832&w=654&h=437&auto=format 654w, 362 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=768&h=513&auto=format 768w, 363 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=1024&h=684&auto=format 1024w, 364 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=0,1,4240,2831&w=1366&h=912&auto=format 1366w, 365 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=1,0,4239,2832&w=1600&h=1069&auto=format 1600w, 366 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=0,1,4240,2831&w=1920&h=1282&auto=format 1920w, 367 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=2048&h=1368&auto=format 2048w, 368 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=2560&h=1710&auto=format 2560w, 369 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?rect=1,0,4239,2832&w=3440&h=2298&auto=format 3440w, 370 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=3840&h=2565&auto=format 3840w, 371 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=4096&h=2736&auto=format 4096w, 372 | https://cdn.sanity.io/images/projectId/dataset/def456-4240x2832.webp?w=4240&h=2832&auto=format 4240w", 373 | }, 374 | "sources": [], 375 | }, 376 | "layout": "constrained", 377 | "width": 4240, 378 | } 379 | `; 380 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/src/gatsby-node.ts: -------------------------------------------------------------------------------- 1 | import sanityClient, {SanityClient} from '@sanity/client' 2 | import { 3 | CreateSchemaCustomizationArgs, 4 | GatsbyNode, 5 | Node, 6 | ParentSpanPluginArgs, 7 | PluginOptions, 8 | SetFieldsOnGraphQLNodeTypeArgs, 9 | SourceNodesArgs, 10 | } from 'gatsby' 11 | import {GraphQLFieldConfig} from 'gatsby/graphql' 12 | import {polyfillImageServiceDevRoutes} from 'gatsby-plugin-utils/polyfill-remote-file' 13 | import {addRemoteFilePolyfillInterface} from 'gatsby-plugin-utils/polyfill-remote-file' 14 | import gatsbyPkg from 'gatsby/package.json' 15 | import {uniq} from 'lodash' 16 | import oneline from 'oneline' 17 | import {fromEvent, merge, of} from 'rxjs' 18 | import {bufferWhen, debounceTime, filter, map, tap} from 'rxjs/operators' 19 | import debug from './debug' 20 | import {extendImageNode} from './images/extendImageNode' 21 | import {SanityInputNode} from './types/gatsby' 22 | import {SanityDocument, SanityWebhookBody} from './types/sanity' 23 | import {CACHE_KEYS, getCacheKey} from './util/cache' 24 | import {prefixId, unprefixId} from './util/documentIds' 25 | import downloadDocuments from './util/downloadDocuments' 26 | import { 27 | ERROR_CODES, 28 | ERROR_MAP, 29 | prefixId as prefixErrorId, 30 | SANITY_ERROR_CODE_MAP, 31 | SANITY_ERROR_CODE_MESSAGES, 32 | } from './util/errors' 33 | import {getGraphQLResolverMap} from './util/getGraphQLResolverMap' 34 | import getSyncWithGatsby from './util/getSyncWithGatsby' 35 | import handleDeltaChanges from './util/handleDeltaChanges' 36 | import {handleWebhookEvent} from './util/handleWebhookEvent' 37 | import { 38 | defaultTypeMap, 39 | getRemoteGraphQLSchema, 40 | getTypeMapFromGraphQLSchema, 41 | TypeMap, 42 | } from './util/remoteGraphQLSchema' 43 | import {rewriteGraphQLSchema} from './util/rewriteGraphQLSchema' 44 | import validateConfig, {PluginConfig} from './util/validateConfig' 45 | import {ProcessingOptions} from './util/normalize' 46 | import {readFileSync} from 'fs' 47 | import {mapCrossDatasetReferences} from './util/mapCrossDatasetReferences' 48 | 49 | let coreSupportsOnPluginInit: 'unstable' | 'stable' | undefined 50 | 51 | try { 52 | const {isGatsbyNodeLifecycleSupported} = require(`gatsby-plugin-utils`) 53 | if (isGatsbyNodeLifecycleSupported(`onPluginInit`)) { 54 | coreSupportsOnPluginInit = 'stable' 55 | } else if (isGatsbyNodeLifecycleSupported(`unstable_onPluginInit`)) { 56 | coreSupportsOnPluginInit = 'unstable' 57 | } 58 | } catch (e) { 59 | console.error(`Could not check if Gatsby supports onPluginInit lifecycle`) 60 | } 61 | 62 | const defaultConfig = { 63 | version: '1', 64 | overlayDrafts: false, 65 | graphqlTag: 'default', 66 | watchModeBuffer: 150, 67 | } 68 | 69 | const stateCache: {[key: string]: any} = {} 70 | 71 | const initializePlugin = async ( 72 | {reporter}: ParentSpanPluginArgs, 73 | pluginOptions?: PluginOptions, 74 | ) => { 75 | const config = {...defaultConfig, ...pluginOptions} 76 | 77 | if (Number(gatsbyPkg.version.split('.')[0]) < 3) { 78 | const unsupportedVersionMessage = oneline` 79 | You are using a version of Gatsby not supported by gatsby-source-sanity. 80 | Upgrade gatsby to >= 3.0.0 to continue.` 81 | 82 | reporter.panic({ 83 | id: prefixErrorId(ERROR_CODES.UnsupportedGatsbyVersion), 84 | context: {sourceMessage: unsupportedVersionMessage}, 85 | }) 86 | 87 | return 88 | } 89 | 90 | // Actually throws in validation function, but helps typescript perform type narrowing 91 | if (!validateConfig(config, reporter)) { 92 | throw new Error('Invalid config') 93 | } 94 | 95 | try { 96 | let api: string = '' 97 | if (config._mocks) { 98 | reporter.warn('[sanity] Using mocked GraphQL schema') 99 | api = readFileSync(config._mocks.schemaPath, 'utf8') 100 | } else { 101 | reporter.info('[sanity] Fetching remote GraphQL schema') 102 | const client = getClient(config) 103 | api = await getRemoteGraphQLSchema(client, config) 104 | } 105 | 106 | api = mapCrossDatasetReferences(api) 107 | 108 | reporter.info('[sanity] Transforming to Gatsby-compatible GraphQL SDL') 109 | const graphqlSdl = await rewriteGraphQLSchema(api, {config, reporter}) 110 | const graphqlSdlKey = getCacheKey(config, CACHE_KEYS.GRAPHQL_SDL) 111 | stateCache[graphqlSdlKey] = graphqlSdl 112 | 113 | reporter.info('[sanity] Stitching GraphQL schemas from SDL') 114 | const typeMap = getTypeMapFromGraphQLSchema(api, config.typePrefix) 115 | const typeMapKey = getCacheKey(config, CACHE_KEYS.TYPE_MAP) 116 | stateCache[typeMapKey] = typeMap 117 | } catch (err: any) { 118 | if (err.isWarning) { 119 | err.message.split('\n').forEach((line: string) => reporter.warn(line)) 120 | return 121 | } 122 | 123 | if (typeof err.code === 'string' && SANITY_ERROR_CODE_MAP[err.code]) { 124 | reporter.panic({ 125 | id: prefixId(SANITY_ERROR_CODE_MAP[err.code]), 126 | context: {sourceMessage: `[sanity] ${SANITY_ERROR_CODE_MESSAGES[err.code]}`}, 127 | }) 128 | } 129 | 130 | const prefix = typeof err.code === 'string' ? `[${err.code}] ` : '' 131 | reporter.panic({ 132 | id: prefixId(ERROR_CODES.SchemaFetchError), 133 | context: {sourceMessage: `${prefix}${err.message}`}, 134 | }) 135 | } 136 | } 137 | 138 | export const onPreInit: GatsbyNode['onPreInit'] = async ({reporter}: ParentSpanPluginArgs) => { 139 | // onPluginInit replaces onPreInit in Gatsby V4 140 | // Old versions of Gatsby does not have the method setErrorMap 141 | if (!coreSupportsOnPluginInit && reporter.setErrorMap) { 142 | reporter.setErrorMap(ERROR_MAP) 143 | } 144 | } 145 | 146 | export const onPreBootstrap: GatsbyNode['onPreBootstrap'] = async (args, pluginOptions?) => { 147 | // Because we are setting global state here, this code now needs to run in onPluginInit if using Gatsby V4 148 | if (!coreSupportsOnPluginInit) { 149 | await initializePlugin(args, pluginOptions) 150 | } 151 | } 152 | 153 | const onPluginInit = async (args: ParentSpanPluginArgs, pluginOptions?: PluginOptions) => { 154 | args.reporter.setErrorMap(ERROR_MAP) 155 | await initializePlugin(args, pluginOptions) 156 | } 157 | 158 | if (coreSupportsOnPluginInit === 'stable') { 159 | // to properly initialize plugin in worker (`onPreBootstrap` won't run in workers) 160 | // need to conditionally export otherwise it throw an error for older versions 161 | exports.onPluginInit = onPluginInit 162 | } else if (coreSupportsOnPluginInit === 'unstable') { 163 | exports.unstable_onPluginInit = onPluginInit 164 | } 165 | 166 | export const createResolvers: GatsbyNode['createResolvers'] = ( 167 | args, 168 | pluginOptions: PluginConfig, 169 | ): any => { 170 | const typeMapKey = getCacheKey(pluginOptions, CACHE_KEYS.TYPE_MAP) 171 | const typeMap = (stateCache[typeMapKey] || defaultTypeMap) as TypeMap 172 | 173 | args.createResolvers(getGraphQLResolverMap(typeMap, pluginOptions, args)) 174 | } 175 | 176 | export const createSchemaCustomization: GatsbyNode['createSchemaCustomization'] = ( 177 | {actions, schema}: CreateSchemaCustomizationArgs, 178 | pluginConfig: PluginConfig, 179 | ): any => { 180 | const {createTypes} = actions 181 | const graphqlSdlKey = getCacheKey(pluginConfig, CACHE_KEYS.GRAPHQL_SDL) 182 | const graphqlSdl = stateCache[graphqlSdlKey] 183 | 184 | createTypes([ 185 | graphqlSdl, 186 | /** 187 | * The following type is for the Gatsby Image CDN resolver `gatsbyImage`. SanityImageAsset already exists in `graphqlSdl` above and then this type will be merged into it, extending it with image CDN support. 188 | */ 189 | addRemoteFilePolyfillInterface( 190 | schema.buildObjectType({ 191 | name: `SanityImageAsset`, 192 | fields: {}, 193 | interfaces: [`Node`, `RemoteFile`], 194 | }), 195 | // @ts-expect-error TS2345: Argument of type '{ schema: NodePluginSchema; actions: Actions; }' is not assignable to parameter of type '{ schema: NodePluginSchema; actions: Actions; store: Store; }'. 196 | { 197 | schema, 198 | actions, 199 | }, 200 | ), 201 | ]) 202 | } 203 | 204 | const getDocumentIds = async (client: SanityClient): Promise => { 205 | // Largish batch size to reduce network round trips without putting too much stress on API 206 | const batchSize = 30000 207 | 208 | let prevId: string | undefined 209 | let ids = [] as string[] 210 | 211 | while (true) { 212 | const batch = await client.fetch( 213 | prevId !== undefined 214 | ? `*[!(_type match "system.**") && _id > $prevId]|order(_id asc)[0...$batchSize]._id` 215 | : `*[!(_type match "system.**")]|order(_id asc)[0...$batchSize]._id`, 216 | { 217 | prevId: prevId || null, 218 | batchSize, 219 | }, 220 | ) 221 | ids.push(...batch) 222 | if (batch.length < batchSize) { 223 | break 224 | } 225 | prevId = batch[batch.length - 1] 226 | } 227 | 228 | return ids 229 | } 230 | 231 | export const sourceNodes: GatsbyNode['sourceNodes'] = async ( 232 | args: SourceNodesArgs & {webhookBody?: SanityWebhookBody}, 233 | pluginConfig: PluginConfig, 234 | ) => { 235 | const config = {...defaultConfig, ...pluginConfig} 236 | const {dataset, overlayDrafts, watchMode} = config 237 | const {actions, createNodeId, createContentDigest, reporter, webhookBody} = args 238 | const {createNode, createParentChildLink} = actions 239 | const typeMapKey = getCacheKey(pluginConfig, CACHE_KEYS.TYPE_MAP) 240 | const typeMap = (stateCache[typeMapKey] || defaultTypeMap) as TypeMap 241 | 242 | const client = getClient(config) 243 | const url = client.getUrl(`/data/export/${dataset}?tag=sanity.gatsby.source-nodes`) 244 | 245 | // Stitches together required methods from within the context and actions objects 246 | const processingOptions: ProcessingOptions = { 247 | typeMap, 248 | createNodeId, 249 | createNode, 250 | createContentDigest, 251 | createParentChildLink, 252 | overlayDrafts, 253 | client, 254 | typePrefix: config.typePrefix, 255 | } 256 | 257 | // PREVIEW UPDATES THROUGH WEBHOOKS 258 | // ======= 259 | // `webhookBody` is always present, even when sourceNodes is called in Gatsby's initialization. 260 | // As such, we need to check if it has any key to work with it. 261 | if (webhookBody && Object.keys(webhookBody).length > 0) { 262 | const webhookHandled = handleWebhookEvent(args, {client, processingOptions}) 263 | 264 | // Even if the webhook body is invalid, let's avoid re-fetching all documents. 265 | // Otherwise, we'd be overloading Gatsby's preview servers on large datasets. 266 | if (!webhookHandled) { 267 | reporter.warn( 268 | '[sanity] Received webhook is invalid. Make sure your Sanity webhook is configured correctly.', 269 | ) 270 | reporter.info(`[sanity] Webhook data: ${JSON.stringify(webhookBody, null, 2)}`) 271 | } 272 | 273 | return 274 | } 275 | 276 | const gatsbyNodes = new Map() 277 | let documents = new Map() 278 | let syncWithGatsby = getSyncWithGatsby({ 279 | documents, 280 | gatsbyNodes, 281 | args, 282 | processingOptions, 283 | typeMap, 284 | }) 285 | 286 | // If we have a warm build, let's fetch only those which changed since the last build 287 | const lastBuildTime = await args.cache.get(getCacheKey(config, CACHE_KEYS.LAST_BUILD)) 288 | let deltaHandled = false 289 | if (lastBuildTime) { 290 | try { 291 | // Let's make sure we keep documents nodes already in the cache (3 steps) 292 | // ========= 293 | // 1/4. Get all valid document IDs from Sanity 294 | const documentIds = new Set(await getDocumentIds(client)) 295 | 296 | // 2/4. Get all document types implemented in the GraphQL layer 297 | // @initializePlugin() will populate `stateCache` with 1+ TypeMaps 298 | const typeMapStateKeys = Object.keys(stateCache).filter((key) => key.endsWith('typeMap')) 299 | // Let's take all document types from these TypeMaps 300 | const sanityDocTypes = Array.from( 301 | // De-duplicate types with a Set 302 | new Set( 303 | typeMapStateKeys.reduce((types, curKey) => { 304 | const map = stateCache[curKey] as TypeMap 305 | const documentTypes = Object.keys(map.objects).filter( 306 | (key) => map.objects[key].isDocument, 307 | ) 308 | return [...types, ...documentTypes] 309 | }, [] as string[]), 310 | ), 311 | ) 312 | 313 | // 3/4. From these types, get all nodes from store that are created from this plugin. 314 | // (we didn't use args.getNodes() as that'd be too expensive - hence why we limit it to Sanity-only types) 315 | for (const docType of sanityDocTypes) { 316 | args 317 | .getNodesByType(docType) 318 | // 4/4. touch valid documents to prevent Gatsby from deleting them 319 | .forEach((node) => { 320 | // If a document isn't included in documentIds, that means it was deleted since lastBuildTime. Don't touch it. 321 | if ( 322 | node.internal.owner === 'gatsby-source-sanity' && 323 | typeof node._id === 'string' && 324 | (documentIds.has(node._id) || documentIds.has(unprefixId(node._id))) 325 | ) { 326 | actions.touchNode(node) 327 | gatsbyNodes.set(unprefixId(node._id), node) 328 | documents.set(node._id, node as unknown as SanityDocument) 329 | } 330 | }) 331 | } 332 | 333 | // With existing documents cached, let's handle those that changed since last build 334 | deltaHandled = await handleDeltaChanges({ 335 | args, 336 | lastBuildTime, 337 | client, 338 | syncWithGatsby, 339 | config, 340 | }) 341 | if (!deltaHandled) { 342 | reporter.warn( 343 | "[sanity] Couldn't retrieve latest changes. Will fetch all documents instead.", 344 | ) 345 | } 346 | } catch (error) { 347 | // lastBuildTime isn't a date, ignore it 348 | } 349 | } 350 | 351 | function syncAllWithGatsby() { 352 | for (const id of documents.keys()) { 353 | syncWithGatsby(id) 354 | } 355 | } 356 | 357 | function syncIdsWithGatsby(ids: string[]) { 358 | for (const id of ids) { 359 | syncWithGatsby(id) 360 | } 361 | } 362 | if (watchMode) { 363 | // Note: since we don't setup the listener before *after* all documents has been fetched here we will miss any events that 364 | // happened in the time window between the documents was fetched and the listener connected. If this happens, the 365 | // preview will show an outdated version of the document. 366 | reporter.info('[sanity] Watch mode enabled, starting a listener') 367 | 368 | if (pluginConfig.watchModeBuffer) { 369 | reporter.warn( 370 | "[sanity] watchModeBuffer isn't a supported option. The plugin will automatically apply changes when Gatsby can handle them.", 371 | ) 372 | } 373 | 374 | const gatsbyEvents = fromEvent(args.emitter, '*') 375 | 376 | client 377 | .listen('*[!(_id in path("_.**"))]') 378 | .pipe( 379 | filter((event) => overlayDrafts || !event.documentId.startsWith('drafts.')), 380 | tap((event) => { 381 | if (event.result) { 382 | documents.set(event.documentId, event.result) 383 | } else { 384 | documents.delete(event.documentId) 385 | } 386 | }), 387 | map((event) => event.documentId), 388 | // Wait `x`ms since the last internal change from Gatsby to let it rest before we add the nodes to GraphQL 389 | bufferWhen(() => 390 | merge( 391 | gatsbyEvents, 392 | // If no Gatsby event, emit a dummy event just to unlock bufferWhen 393 | of(0), 394 | ).pipe(debounceTime(config.watchModeBuffer)), 395 | ), 396 | filter((ids) => ids.length > 0), 397 | map((ids) => uniq(ids)), 398 | tap((updateIds) => 399 | debug('The following documents updated and will be synced with gatsby: ', updateIds), 400 | ), 401 | tap((updatedIds) => syncIdsWithGatsby(updatedIds)), 402 | ) 403 | .subscribe() 404 | } 405 | 406 | if (!deltaHandled) { 407 | reporter.info('[sanity] Fetching export stream for dataset') 408 | documents = await downloadDocuments(url, config.token, {includeDrafts: overlayDrafts}) 409 | reporter.info(`[sanity] Done! Exported ${documents.size} documents.`) 410 | // Renew syncWithGatsby w/ latest documents Map 411 | syncWithGatsby = getSyncWithGatsby({ 412 | documents, 413 | gatsbyNodes, 414 | args, 415 | processingOptions, 416 | typeMap, 417 | }) 418 | // do the initial sync from sanity documents to gatsby nodes 419 | syncAllWithGatsby() 420 | } 421 | 422 | // register the current build time for accessing it in handleDeltaChanges for future builds 423 | await args.cache.set(getCacheKey(config, CACHE_KEYS.LAST_BUILD), new Date().toISOString()) 424 | } 425 | 426 | export const setFieldsOnGraphQLNodeType: GatsbyNode['setFieldsOnGraphQLNodeType'] = async ( 427 | context: SetFieldsOnGraphQLNodeTypeArgs, 428 | pluginConfig: PluginConfig, 429 | ) => { 430 | const {type} = context 431 | let fields: {[key: string]: GraphQLFieldConfig} = {} 432 | if (type.name === 'SanityImageAsset') { 433 | fields = {...fields, ...extendImageNode(pluginConfig)} 434 | } 435 | 436 | return fields 437 | } 438 | 439 | function getClient(config: PluginConfig) { 440 | const {projectId, dataset, apiHost = 'https://api.sanity.io', token} = config 441 | return sanityClient({ 442 | apiHost, 443 | projectId, 444 | dataset, 445 | token, 446 | apiVersion: '1', 447 | useCdn: false, 448 | }) 449 | } 450 | 451 | export const onCreateDevServer = async ({app}: {app: any}) => { 452 | polyfillImageServiceDevRoutes(app) 453 | } 454 | -------------------------------------------------------------------------------- /packages/gatsby-source-sanity/README.md: -------------------------------------------------------------------------------- 1 | # gatsby-source-sanity 2 | 3 | Gatsby source plugin for pulling data from [Sanity.io](https://www.sanity.io/) into [Gatsby](https://www.gatsbyjs.com) websites. Develop with real-time preview of all content changes. Compatible with `gatsby-plugin-image`. Uses your project's GraphQL schema definitions to avoid accidental missing fields (no dummy-content needed). 4 | 5 | Get up and running in minutes with a fully configured starter project: 6 | 7 | - [Blog with Gatsby](https://www.sanity.io/create?template=sanity-io/sanity-template-gatsby-blog) 8 | - [Portfolio with Gatsby](https://www.sanity.io/create?template=sanity-io/sanity-template-gatsby-portfolio). 9 | 10 | [![Watch a video about the company website built with Gatsby using Sanity.io as a headless CMS](https://cdn.sanity.io/images/3do82whm/production/4f652e6d114e7010aa633b81cbcb97c335980fc8-1920x1080.png?w=500)](https://www.youtube.com/watch?v=STtpXBvJmDA) 11 | 12 | ## Table of contents 13 | 14 | - [Install](#install) 15 | - [Basic usage](#basic-usage) 16 | - [Options](#options) 17 | - [Preview of unpublished content](#preview-of-unpublished-content) 18 | - [GraphQL API](#graphql-api) 19 | - [Using images](#using-images) 20 | - [Using Gatsby's Image CDN (beta)](#using-gatsbys-image-cdn-beta) 21 | - [Using images outside of GraphQL](#using-images-outside-of-graphql) 22 | - [Generating pages](#generating-pages) 23 | - ["Raw" fields](#raw-fields) 24 | - [Portable Text / Block Content](#portable-text--block-content) 25 | - [Using multiple datasets](#using-multiple-datasets) 26 | - [Real-time content preview with watch mode](#real-time-content-preview-with-watch-mode) 27 | - [Updating content for editors with preview servers](#updating-content-for-editors-with-preview-servers) 28 | - [Using .env variables](#using-env-variables) 29 | - [How this source plugin works](#how-this-source-plugin-works) 30 | - [Credits](#credits) 31 | - [Develop](#develop) 32 | - [Release new version](#release-new-version) 33 | 34 | [See the getting started video](https://www.youtube.com/watch?v=qU4lFYp3KiQ) 35 | 36 | ## Install 37 | 38 | From the command line, use npm (node package manager) to install the plugin: 39 | 40 | ```console 41 | npm install gatsby-source-sanity gatsby-plugin-image 42 | ``` 43 | 44 | ⚠️ Warning: If using Gatsby v4, make sure you've installed version 7.1.0 or higher. The plugin has a dependency on `gatsby-plugin-image` which itself has dependencies. Check [`gatsby-plugin-image` README](https://www.gatsbyjs.com/plugins/gatsby-plugin-image/#installation) for instructions. 45 | 46 | In the `gatsby-config.js` file in the Gatsby project's root directory, add the plugin configuration inside of the `plugins` section: 47 | 48 | ```js 49 | module.exports = { 50 | // ... 51 | plugins: [ 52 | { 53 | resolve: `gatsby-source-sanity`, 54 | options: { 55 | projectId: `abc123`, 56 | dataset: `blog`, 57 | // a token with read permissions is required 58 | // if you have a private dataset 59 | token: process.env.SANITY_TOKEN, 60 | 61 | // If the Sanity GraphQL API was deployed using `--tag `, 62 | // use `graphqlTag` to specify the tag name. Defaults to `default`. 63 | graphqlTag: 'default', 64 | }, 65 | }, 66 | `gatsby-plugin-image`, 67 | // ... 68 | ], 69 | // ... 70 | } 71 | ``` 72 | 73 | You can access `projectId` and `dataset` by executing `sanity debug --secrets` in the Sanity studio folder. Note that the token printed may be used for development, but is tied to your Sanity CLI login session using your personal Sanity account - make sure to keep it secure and not include it in version control! For production, you'll want to make sure you use a read token generate in the Sanity [management interface](https://manage.sanity.io/). 74 | 75 | ## Basic usage 76 | 77 | At this point you should [set up a GraphQL API](https://www.sanity.io/docs/graphql) for your Sanity dataset, if you have not done so already. This will help the plugin in knowing which types and fields exists, so you can query for them even without them being present in any current documents. 78 | 79 | **You should redeploy the GraphQL API everytime you make changes to the schema that you want to use in Gatsby by running `sanity graphql deploy` from within your Sanity project directory** 80 | 81 | Explore `http://localhost:8000/___graphql` after running `gatsby develop` to understand the created data and create a new query and checking available collections and fields by typing Ctrl + Space. 82 | 83 | ## Options 84 | 85 | | Options | Type | Default | Description | 86 | | --------------- | ------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 87 | | projectId | string | | **[required]** Your Sanity project's ID | 88 | | dataset | string | | **[required]** The dataset to fetch from | 89 | | token | string | | Authentication token for fetching data from private datasets, or when using `overlayDrafts` [Learn more](https://www.sanity.io/docs/http-auth) | 90 | | graphqlTag | string | `default` | If the Sanity GraphQL API was deployed using `--tag `, use this to specify the tag name. | 91 | | overlayDrafts | boolean | `false` | Set to `true` in order for drafts to replace their published version. By default, drafts will be skipped. | 92 | | watchMode | boolean | `false` | Set to `true` to keep a listener open and update with the latest changes in realtime. If you add a `token` you will get all content updates down to each key press. | 93 | | watchModeBuffer | number | `150` | How many milliseconds to wait on watchMode changes before applying them to Gatsby's GraphQL layer. Introduced in 7.2.0. | 94 | | typePrefix | string | | Prefix to use for the GraphQL types. This is prepended to `Sanity` in the type names and allows you to have multiple instances of the plugin in your Gatsby project. | 95 | 96 | ## Preview of unpublished content 97 | 98 | Sometimes you might be working on some new content that is not yet published, which you want to make sure looks alright within your Gatsby site. By setting the `overlayDrafts` setting to `true`, the draft versions will as the option says "overlay" the regular document. In terms of Gatsby nodes, it will _replace_ the published document with the draft. 99 | 100 | Keep in mind that drafts do not have to conform to any validation rules, so your frontend will usually want to double-check all nested properties before attempting to use them. 101 | 102 | ## GraphQL API 103 | 104 | By [deploying a GraphQL API](https://www.sanity.io/docs/graphql) for your dataset, we are able to introspect and figure out which schema types and fields are available and make informed choices based on this information. 105 | 106 | Previous versions did not _require_ this, but often lead to very confusing and unpredictable behavior, which is why we have now made it a requirement. 107 | 108 | ## Using images 109 | 110 | Image fields will have the image URL available under the `field.asset.url` key, but you can also use [gatsby-plugin-image](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-image) for a smooth experience. It's a React component that enables responsive images and advanced image loading techniques. It works great with this source plugin, without requiring any additional build steps. 111 | 112 | ```js 113 | import React from 'react' 114 | import {GatsbyImage} from 'gatsby-plugin-image' 115 | 116 | const sanityConfig = {projectId: 'abc123', dataset: 'blog'} 117 | 118 | const Person = ({data}) => { 119 | return ( 120 |
121 |

{data.sanityPerson.name}

122 | 123 |
124 | ) 125 | } 126 | 127 | export default Person 128 | 129 | export const query = graphql` 130 | query PersonQuery { 131 | sanityPerson { 132 | name 133 | profileImage { 134 | asset { 135 | gatsbyImageData(fit: FILLMAX, placeholder: BLURRED) 136 | } 137 | } 138 | } 139 | } 140 | ` 141 | ``` 142 | 143 | **Note**: we currently [don't support the `format` option of `gatsbyImageData`](https://github.com/sanity-io/gatsby-source-sanity/issues/134#issuecomment-951876221). Our image CDN automatically serves the best format for the user depending on their device, so you don't need to define formats manually. 144 | 145 | ### Using Gatsby's Image CDN (beta) 146 | 147 | This plugin supports [Gatsby's new Image CDN feature](https://gatsby.dev/img). To use it, follow the instructions in the section above, but substitute the `gatsbyImageData` field for `gatsbyImage`. 148 | 149 | ### Using images outside of GraphQL 150 | 151 | If you are using the raw fields, or simply have an image asset ID you would like to use `gatsby-plugin-image` for, you can import and call the utility function `getGatsbyImageData` 152 | 153 | ```jsx 154 | import {GatsbyImage} from 'gatsby-plugin-image' 155 | import {getGatsbyImageData} from 'gatsby-source-sanity' 156 | 157 | const sanityConfig = {projectId: 'abc123', dataset: 'blog'} 158 | const imageAssetId = 'image-488e172a7283400a57e57ffa5762ac3bd837b2ee-4240x2832-jpg' 159 | 160 | const imageData = getGatsbyImageData(imageAssetId, {maxWidth: 1024}, sanityConfig) 161 | 162 | 163 | ``` 164 | 165 | ## Generating pages 166 | 167 | Sanity does not have any concept of a "page", since it's built to be totally agnostic to how you want to present your content and in which medium, but since you're using Gatsby, you'll probably want some pages! 168 | 169 | As with any Gatsby site, you'll want to create a `gatsby-node.js` in the root of your Gatsby site repository (if it doesn't already exist), and declare a `createPages` function. Within it, you'll use GraphQL to query for the data you need to build the pages. 170 | 171 | For instance, if you have a `project` document type in Sanity that you want to generate pages for, you could do something along the lines of this: 172 | 173 | ```js 174 | exports.createPages = async ({graphql, actions}) => { 175 | const {createPage} = actions 176 | 177 | const result = await graphql(` 178 | { 179 | allSanityProject(filter: {slug: {current: {ne: null}}}) { 180 | edges { 181 | node { 182 | title 183 | description 184 | tags 185 | launchDate(format: "DD.MM.YYYY") 186 | slug { 187 | current 188 | } 189 | image { 190 | asset { 191 | url 192 | } 193 | } 194 | } 195 | } 196 | } 197 | } 198 | `) 199 | 200 | if (result.errors) { 201 | throw result.errors 202 | } 203 | 204 | const projects = result.data.allSanityProject.edges || [] 205 | projects.forEach((edge, index) => { 206 | const path = `/project/${edge.node.slug.current}` 207 | 208 | createPage({ 209 | path, 210 | component: require.resolve('./src/templates/project.js'), 211 | context: {slug: edge.node.slug.current}, 212 | }) 213 | }) 214 | } 215 | ``` 216 | 217 | The above query will fetch all projects that have a `slug.current` field set, and generate pages for them, available as `/project/`. It will use the template defined in `src/templates/project.js` as the basis for these pages. 218 | 219 | Checkout [Creating Pages from Data Programmatically](https://www.gatsbyjs.com/docs/programmatically-create-pages-from-data/) to learn more. 220 | 221 | Remember to use the GraphiQL interface to help write the queries you need - it's usually running at `http://localhost:8000/___graphql` while running `gatsby develop`. 222 | 223 | ## "Raw" fields 224 | 225 | Arrays and object types at the root of documents will get an additional "raw JSON" representation in a field called `_raw`. For instance, a field named `body` will be mapped to `_rawBody`. It's important to note that this is only done for top-level nodes (documents). 226 | 227 | Quite often, you'll want to replace reference fields (eg `_ref: ''`), with the actual document that is referenced. This is done automatically for regular fields, but within raw fields, you have to explicitly enable this behavior, by using the field-level `resolveReferences` argument: 228 | 229 | ```graphql 230 | { 231 | allSanityProject { 232 | edges { 233 | node { 234 | _rawTasks(resolveReferences: {maxDepth: 5}) 235 | } 236 | } 237 | } 238 | } 239 | ``` 240 | 241 | ## Portable Text / Block Content 242 | 243 | Rich text in Sanity is usually represented as [Portable Text](https://www.portabletext.org/) (previously known as "Block Content"). 244 | 245 | These data structures can be deep and a chore to query (specifying all the possible fields). As [noted above](#raw-fields), there is a "raw" alternative available for these fields which is usually what you'll want to use. 246 | 247 | You can install [@portabletext/react](https://www.npmjs.com/package/@portabletext/react) from npm and use it in your Gatsby project to serialize Portable Text. It lets you use your own React components to override defaults and render custom content types. [Learn more about Portable Text in our documentation](https://www.sanity.io/docs/content-studio/what-you-need-to-know-about-block-text). 248 | 249 | ## Using multiple datasets 250 | 251 | If you want to use more than one dataset in your site, you can do so by adding multiple instances of the plugin to your `gatsby-config.js` file. To avoid conflicting type names you can use the `typeName` option to set a custom prefix for the GraphQL types. These can be datasets from different projects, or different datasets within the same project. 252 | 253 | ```js 254 | // In your gatsby-config.js 255 | module.exports = { 256 | plugins: [ 257 | { 258 | resolve: 'gatsby-source-sanity', 259 | options: { 260 | projectId: 'abc123', 261 | dataset: 'production', 262 | }, 263 | }, 264 | { 265 | resolve: 'gatsby-source-sanity', 266 | options: { 267 | projectId: 'abc123', 268 | dataset: 'staging', 269 | typePrefix: 'Staging', 270 | }, 271 | }, 272 | ], 273 | } 274 | ``` 275 | 276 | In this case, the type names for the first instance will be `Sanity`, while the second will be `StagingSanity`. 277 | 278 | ## Real-time content preview with watch mode 279 | 280 | While developing, it can often be beneficial to get updates without having to manually restart the build process. By setting `watchMode` to true, this plugin will set up a listener which watches for changes. When it detects a change, the document in question is updated in real-time and will be reflected immediately. 281 | 282 | If you add [a `token` with read rights](https://www.sanity.io/docs/http-auth#robot-tokens) and set `overlayDrafts` to true, each small change to the draft will immediately be applied. Keep in mind that this is mainly intended for development, see next section for how to enable previews for your entire team. 283 | 284 | ## Updating content for editors with preview servers 285 | 286 | You can use [Gatsby preview servers](https://www.gatsbyjs.com/docs/how-to/local-development/running-a-gatsby-preview-server) (often through [Gatsby Cloud](https://www.gatsbyjs.com/products/cloud/)) to update your content on a live URL your team can use. 287 | 288 | In order to have previews working, you'll need to activate `overlayDrafts` in the plugin's configuration inside preview environments. To do so, we recommend following a pattern similar to this: 289 | 290 | ```js 291 | // In gatsby-config.js 292 | 293 | const isProd = process.env.NODE_ENV === "production" 294 | const previewEnabled = (process.env.GATSBY_IS_PREVIEW || "false").toLowerCase() === "true" 295 | 296 | module.exports = { 297 | // ... 298 | plugins: [ 299 | resolve: "gatsby-source-sanity", 300 | options: { 301 | // ... 302 | watchMode: !isProd, // watchMode only in dev mode 303 | overlayDrafts: !isProd || previewEnabled, // drafts in dev & Gatsby Cloud Preview 304 | }, 305 | ] 306 | } 307 | ``` 308 | 309 | Then, you'll need to set-up a Sanity webhook pointing to your Gatsby preview URL. Create your webhook from **[this template](https://www.sanity.io/manage/webhooks/share?name=Gatsby+Cloud+Preview&description=Find+more+information+here%3A+https%3A%2F%2Fwww.notion.so%2Fsanityio%2FGatsby-Cloud-previews-with-Sanity-s-Webhooks-v2-c3c1abad37f743febc17cd4e0b81431c&url=GATSBY_PREVIEW_WEBHOOK_URL&on=create&on=update&on=delete&filter=&projection=select%28delta%3A%3Aoperation%28%29+%3D%3D+%22delete%22+%3D%3E+%7B%0A++%22operation%22%3A+delta%3A%3Aoperation%28%29%2C%0A++%22documentId%22%3A+coalesce%28before%28%29._id%2C+after%28%29._id%29%2C%0A++%22projectId%22%3A+sanity%3A%3AprojectId%28%29%2C%0A++%22dataset%22%3A+sanity%3A%3Adataset%28%29%2C%0A%7D%2C+%7B%7D%29&httpMethod=POST&apiVersion=v2021-03-25&includeDrafts=true)**, making sure you update the URL. 310 | 311 | If using Gatsby Cloud, this should be auto-configured during your initial set-up. 312 | 313 | ⚠️ **Warning:** if you have Gatsby Cloud previews v1 enabled on your site, you'll need to reach out to their support for enabling an upgrade. The method described here only works with the newer "Incremental CMS Preview", webhook-based system. 314 | 315 | --- 316 | 317 | You can also follow the manual steps below: 318 | 319 | 1. Get the webhook endpoint needed for triggering Gatsby Cloud previews in [their dashboard](https://www.gatsbyjs.com/dashboard/). 320 | 2. Go to [sanity.io/manage](http://sanity.io/manage) and navigate to your project 321 | 3. Under the "API" tab, scroll to Webhooks or "GROQ-powered webhooks" 322 | 4. Add a new webhook and name it as you see fit 323 | 5. Choose the appropriate dataset and add the Gatsby Cloud webhook endpoint to the URL field 324 | 6. Keep the HTTP method set to POST, skip "HTTP Headers" 325 | 7. Set the hook to trigger on Create, Update and Delete 326 | 8. Skip the filter field 327 | 9. Specify the following projection: 328 | 329 | ```jsx 330 | select(delta::operation() == "delete" => { 331 | "operation": delta::operation(), 332 | "documentId": coalesce(before()._id, after()._id), 333 | "projectId": sanity::projectId(), 334 | "dataset": sanity::dataset(), 335 | }, {}) 336 | ``` 337 | 338 | 10. Set the API version to `v2021-03-25` 339 | 11. And set it to fire on drafts 340 | 12. Save the webhook 341 | 342 | ## Using .env variables 343 | 344 | If you don't want to attach your Sanity project's ID to the repo, you can easily store it in .env files by doing the following: 345 | 346 | ```js 347 | // In your .env file 348 | SANITY_PROJECT_ID = abc123 349 | SANITY_DATASET = production 350 | SANITY_TOKEN = my_super_secret_token 351 | 352 | // In your gatsby-config.js file 353 | require('dotenv').config({ 354 | path: `.env.${process.env.NODE_ENV}`, 355 | }) 356 | 357 | module.exports = { 358 | // ... 359 | plugins: [ 360 | { 361 | resolve: 'gatsby-source-sanity', 362 | options: { 363 | projectId: process.env.SANITY_PROJECT_ID, 364 | dataset: process.env.SANITY_DATASET, 365 | token: process.env.SANITY_TOKEN, 366 | // ... 367 | }, 368 | }, 369 | ], 370 | // ... 371 | } 372 | ``` 373 | 374 | This example is based off [Gatsby Docs' implementation](https://www.gatsbyjs.org/docs/environment-variables/). 375 | 376 | ## How this source plugin works 377 | 378 | When starting Gatsby in development or building a website, the source plugin will first fetch the GraphQL Schema Definitions from a Sanity deployed GraphQL API. The source plugin uses this to tell Gatsby which fields should be available to prevent it from breaking if the content for certain fields happens to disappear. Then it will hit the project’s export endpoint, which streams all the accessible documents to Gatsby’s in-memory datastore. 379 | 380 | In other words, the whole site is built with two requests. Running the development server, will also set up a listener that pushes whatever changes come from Sanity to Gatsby in real-time, without doing additional API queries. If you give the source plugin a token with permission to read drafts, you’ll see the changes instantly. 381 | 382 | ## Credits 383 | 384 | Huge thanks to [Henrique Doro](https://github.com/hdoro) for doing the initial implementation of this plugin, and for donating it to the Sanity team. Mad props! 385 | 386 | Big thanks to the good people backing Gatsby for bringing such a joy to our developer days! 387 | 388 | ## Develop 389 | 390 | ### Release new version 391 | 392 | Run ["CI & Release" workflow](https://github.com/sanity-io/gatsby-source-sanity/actions/workflows/main.yml). 393 | Make sure to select the main branch and check "Release new version". 394 | 395 | Semantic release will only release on configured branches, so it is safe to run release on any branch. 396 | -------------------------------------------------------------------------------- /examples/shared-content-studio/production.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: RootQuery 3 | } 4 | 5 | """ 6 | Field is a "raw" JSON alias for a different field 7 | """ 8 | directive @jsonAlias( 9 | """ 10 | Source field name 11 | """ 12 | for: String! 13 | ) on FIELD_DEFINITION 14 | 15 | """ 16 | Field references one or more documents 17 | """ 18 | directive @reference on FIELD_DEFINITION 19 | 20 | type AuthorReference { 21 | _key: String 22 | _type: String 23 | _ref: String 24 | _weak: Boolean 25 | _dataset: String 26 | _projectId: String 27 | } 28 | 29 | input AuthorReferenceFilter { 30 | _key: StringFilter 31 | _type: StringFilter 32 | _ref: StringFilter 33 | _weak: BooleanFilter 34 | _dataset: StringFilter 35 | _projectId: StringFilter 36 | } 37 | 38 | union AuthorReferenceOrBlockOrBookMetadata = AuthorReference | Block | BookMetadata 39 | 40 | union AuthorReferenceOrBookOrBookMetadata = AuthorReference | Book | BookMetadata 41 | 42 | union AuthorReferenceOrPublisherOrSpan = AuthorReference | Publisher | Span 43 | 44 | input AuthorReferenceSorting { 45 | _key: SortOrder 46 | _type: SortOrder 47 | _ref: SortOrder 48 | _weak: SortOrder 49 | _dataset: SortOrder 50 | _projectId: SortOrder 51 | } 52 | 53 | type Block { 54 | _key: String 55 | _type: String 56 | children: [Span] 57 | style: String 58 | list: String 59 | } 60 | 61 | type Book implements Document { 62 | """ 63 | Document ID 64 | """ 65 | _id: ID 66 | 67 | """ 68 | Document type 69 | """ 70 | _type: String 71 | 72 | """ 73 | Date the document was created 74 | """ 75 | _createdAt: DateTime 76 | 77 | """ 78 | Date the document was last modified 79 | """ 80 | _updatedAt: DateTime 81 | 82 | """ 83 | Current document revision 84 | """ 85 | _rev: String 86 | _key: String 87 | title: String 88 | cover: Image 89 | author: CrossDatasetReference 90 | extraAuthor: AuthorReference 91 | coauthors: [AuthorReference] 92 | mixedArray: [AuthorReferenceOrBookOrBookMetadata] 93 | genres: [Genre] 94 | publisher: Publisher 95 | extraPublisher: Publisher 96 | blurbRaw: JSON 97 | } 98 | 99 | input BookFilter { 100 | """ 101 | Apply filters on document level 102 | """ 103 | _: Sanity_DocumentFilter 104 | _id: IDFilter 105 | _type: StringFilter 106 | _createdAt: DatetimeFilter 107 | _updatedAt: DatetimeFilter 108 | _rev: StringFilter 109 | _key: StringFilter 110 | title: StringFilter 111 | cover: ImageFilter 112 | author: CrossDatasetReferenceFilter 113 | extraAuthor: AuthorReferenceFilter 114 | publisher: PublisherFilter 115 | extraPublisher: PublisherFilter 116 | } 117 | 118 | type BookMetadata { 119 | _key: String 120 | _type: String 121 | isbn: String 122 | } 123 | 124 | input BookMetadataFilter { 125 | _key: StringFilter 126 | _type: StringFilter 127 | isbn: StringFilter 128 | } 129 | 130 | input BookMetadataSorting { 131 | _key: SortOrder 132 | _type: SortOrder 133 | isbn: SortOrder 134 | } 135 | 136 | input BookSorting { 137 | _id: SortOrder 138 | _type: SortOrder 139 | _createdAt: SortOrder 140 | _updatedAt: SortOrder 141 | _rev: SortOrder 142 | _key: SortOrder 143 | title: SortOrder 144 | cover: ImageSorting 145 | author: CrossDatasetReferenceSorting 146 | extraAuthor: AuthorReferenceSorting 147 | } 148 | 149 | input BooleanFilter { 150 | """ 151 | Checks if the value is equal to the given input. 152 | """ 153 | eq: Boolean 154 | 155 | """ 156 | Checks if the value is not equal to the given input. 157 | """ 158 | neq: Boolean 159 | 160 | """ 161 | Checks if the value is defined. 162 | """ 163 | is_defined: Boolean 164 | } 165 | 166 | type CrossDatasetReference { 167 | _key: String 168 | _type: String 169 | _ref: String 170 | _weak: Boolean 171 | _dataset: String 172 | _projectId: String 173 | } 174 | 175 | input CrossDatasetReferenceFilter { 176 | _key: StringFilter 177 | _type: StringFilter 178 | _ref: StringFilter 179 | _weak: BooleanFilter 180 | _dataset: StringFilter 181 | _projectId: StringFilter 182 | } 183 | 184 | input CrossDatasetReferenceSorting { 185 | _key: SortOrder 186 | _type: SortOrder 187 | _ref: SortOrder 188 | _weak: SortOrder 189 | _dataset: SortOrder 190 | _projectId: SortOrder 191 | } 192 | 193 | """ 194 | A date string, such as 2007-12-03, compliant with the `full-date` format 195 | outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for 196 | representation of dates and times using the Gregorian calendar. 197 | """ 198 | scalar Date 199 | 200 | input DateFilter { 201 | """ 202 | Checks if the value is equal to the given input. 203 | """ 204 | eq: Date 205 | 206 | """ 207 | Checks if the value is not equal to the given input. 208 | """ 209 | neq: Date 210 | 211 | """ 212 | Checks if the value is greater than the given input. 213 | """ 214 | gt: Date 215 | 216 | """ 217 | Checks if the value is greater than or equal to the given input. 218 | """ 219 | gte: Date 220 | 221 | """ 222 | Checks if the value is lesser than the given input. 223 | """ 224 | lt: Date 225 | 226 | """ 227 | Checks if the value is lesser than or equal to the given input. 228 | """ 229 | lte: Date 230 | 231 | """ 232 | Checks if the value is defined. 233 | """ 234 | is_defined: Boolean 235 | } 236 | 237 | """ 238 | A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the 239 | `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 240 | 8601 standard for representation of dates and times using the Gregorian calendar. 241 | """ 242 | scalar DateTime 243 | 244 | input DatetimeFilter { 245 | """ 246 | Checks if the value is equal to the given input. 247 | """ 248 | eq: DateTime 249 | 250 | """ 251 | Checks if the value is not equal to the given input. 252 | """ 253 | neq: DateTime 254 | 255 | """ 256 | Checks if the value is greater than the given input. 257 | """ 258 | gt: DateTime 259 | 260 | """ 261 | Checks if the value is greater than or equal to the given input. 262 | """ 263 | gte: DateTime 264 | 265 | """ 266 | Checks if the value is lesser than the given input. 267 | """ 268 | lt: DateTime 269 | 270 | """ 271 | Checks if the value is lesser than or equal to the given input. 272 | """ 273 | lte: DateTime 274 | 275 | """ 276 | Checks if the value is defined. 277 | """ 278 | is_defined: Boolean 279 | } 280 | 281 | """ 282 | A Sanity document 283 | """ 284 | interface Document { 285 | """ 286 | Document ID 287 | """ 288 | _id: ID 289 | 290 | """ 291 | Document type 292 | """ 293 | _type: String 294 | 295 | """ 296 | Date the document was created 297 | """ 298 | _createdAt: DateTime 299 | 300 | """ 301 | Date the document was last modified 302 | """ 303 | _updatedAt: DateTime 304 | 305 | """ 306 | Current document revision 307 | """ 308 | _rev: String 309 | } 310 | 311 | input DocumentFilter { 312 | """ 313 | Apply filters on document level 314 | """ 315 | _: Sanity_DocumentFilter 316 | _id: IDFilter 317 | _type: StringFilter 318 | _createdAt: DatetimeFilter 319 | _updatedAt: DatetimeFilter 320 | _rev: StringFilter 321 | } 322 | 323 | input DocumentSorting { 324 | _id: SortOrder 325 | _type: SortOrder 326 | _createdAt: SortOrder 327 | _updatedAt: SortOrder 328 | _rev: SortOrder 329 | } 330 | 331 | type File { 332 | _key: String 333 | _type: String 334 | asset: SanityFileAsset 335 | } 336 | 337 | input FileFilter { 338 | _key: StringFilter 339 | _type: StringFilter 340 | asset: SanityFileAssetFilter 341 | } 342 | 343 | input FileSorting { 344 | _key: SortOrder 345 | _type: SortOrder 346 | } 347 | 348 | input FloatFilter { 349 | """ 350 | Checks if the value is equal to the given input. 351 | """ 352 | eq: Float 353 | 354 | """ 355 | Checks if the value is not equal to the given input. 356 | """ 357 | neq: Float 358 | 359 | """ 360 | Checks if the value is greater than the given input. 361 | """ 362 | gt: Float 363 | 364 | """ 365 | Checks if the value is greater than or equal to the given input. 366 | """ 367 | gte: Float 368 | 369 | """ 370 | Checks if the value is lesser than the given input. 371 | """ 372 | lt: Float 373 | 374 | """ 375 | Checks if the value is lesser than or equal to the given input. 376 | """ 377 | lte: Float 378 | 379 | """ 380 | Checks if the value is defined. 381 | """ 382 | is_defined: Boolean 383 | } 384 | 385 | type Genre implements Document { 386 | """ 387 | Document ID 388 | """ 389 | _id: ID 390 | 391 | """ 392 | Document type 393 | """ 394 | _type: String 395 | 396 | """ 397 | Date the document was created 398 | """ 399 | _createdAt: DateTime 400 | 401 | """ 402 | Date the document was last modified 403 | """ 404 | _updatedAt: DateTime 405 | 406 | """ 407 | Current document revision 408 | """ 409 | _rev: String 410 | _key: String 411 | title: String 412 | } 413 | 414 | input GenreFilter { 415 | """ 416 | Apply filters on document level 417 | """ 418 | _: Sanity_DocumentFilter 419 | _id: IDFilter 420 | _type: StringFilter 421 | _createdAt: DatetimeFilter 422 | _updatedAt: DatetimeFilter 423 | _rev: StringFilter 424 | _key: StringFilter 425 | title: StringFilter 426 | } 427 | 428 | input GenreSorting { 429 | _id: SortOrder 430 | _type: SortOrder 431 | _createdAt: SortOrder 432 | _updatedAt: SortOrder 433 | _rev: SortOrder 434 | _key: SortOrder 435 | title: SortOrder 436 | } 437 | 438 | type Geopoint { 439 | _key: String 440 | _type: String 441 | lat: Float 442 | lng: Float 443 | alt: Float 444 | } 445 | 446 | input GeopointFilter { 447 | _key: StringFilter 448 | _type: StringFilter 449 | lat: FloatFilter 450 | lng: FloatFilter 451 | alt: FloatFilter 452 | } 453 | 454 | input GeopointSorting { 455 | _key: SortOrder 456 | _type: SortOrder 457 | lat: SortOrder 458 | lng: SortOrder 459 | alt: SortOrder 460 | } 461 | 462 | input IDFilter { 463 | """ 464 | Checks if the value is equal to the given input. 465 | """ 466 | eq: ID 467 | 468 | """ 469 | Checks if the value is not equal to the given input. 470 | """ 471 | neq: ID 472 | 473 | """ 474 | Checks if the value matches the given word/words. 475 | """ 476 | matches: ID 477 | in: [ID!] 478 | nin: [ID!] 479 | } 480 | 481 | type Image { 482 | _key: String 483 | _type: String 484 | asset: SanityImageAsset 485 | hotspot: SanityImageHotspot 486 | crop: SanityImageCrop 487 | } 488 | 489 | input ImageFilter { 490 | _key: StringFilter 491 | _type: StringFilter 492 | asset: SanityImageAssetFilter 493 | hotspot: SanityImageHotspotFilter 494 | crop: SanityImageCropFilter 495 | } 496 | 497 | input ImageSorting { 498 | _key: SortOrder 499 | _type: SortOrder 500 | hotspot: SanityImageHotspotSorting 501 | crop: SanityImageCropSorting 502 | } 503 | 504 | input IntFilter { 505 | """ 506 | Checks if the value is equal to the given input. 507 | """ 508 | eq: Int 509 | 510 | """ 511 | Checks if the value is not equal to the given input. 512 | """ 513 | neq: Int 514 | 515 | """ 516 | Checks if the value is greater than the given input. 517 | """ 518 | gt: Int 519 | 520 | """ 521 | Checks if the value is greater than or equal to the given input. 522 | """ 523 | gte: Int 524 | 525 | """ 526 | Checks if the value is lesser than the given input. 527 | """ 528 | lt: Int 529 | 530 | """ 531 | Checks if the value is lesser than or equal to the given input. 532 | """ 533 | lte: Int 534 | 535 | """ 536 | Checks if the value is defined. 537 | """ 538 | is_defined: Boolean 539 | } 540 | 541 | """ 542 | The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). 543 | """ 544 | scalar JSON 545 | 546 | type Publisher implements Document { 547 | """ 548 | Document ID 549 | """ 550 | _id: ID 551 | 552 | """ 553 | Document type 554 | """ 555 | _type: String 556 | 557 | """ 558 | Date the document was created 559 | """ 560 | _createdAt: DateTime 561 | 562 | """ 563 | Date the document was last modified 564 | """ 565 | _updatedAt: DateTime 566 | 567 | """ 568 | Current document revision 569 | """ 570 | _rev: String 571 | _key: String 572 | name: String 573 | } 574 | 575 | input PublisherFilter { 576 | """ 577 | Apply filters on document level 578 | """ 579 | _: Sanity_DocumentFilter 580 | _id: IDFilter 581 | _type: StringFilter 582 | _createdAt: DatetimeFilter 583 | _updatedAt: DatetimeFilter 584 | _rev: StringFilter 585 | _key: StringFilter 586 | name: StringFilter 587 | } 588 | 589 | input PublisherSorting { 590 | _id: SortOrder 591 | _type: SortOrder 592 | _createdAt: SortOrder 593 | _updatedAt: SortOrder 594 | _rev: SortOrder 595 | _key: SortOrder 596 | name: SortOrder 597 | } 598 | 599 | type RootQuery { 600 | Book( 601 | """ 602 | Book document ID 603 | """ 604 | id: ID! 605 | ): Book 606 | Genre( 607 | """ 608 | Genre document ID 609 | """ 610 | id: ID! 611 | ): Genre 612 | Publisher( 613 | """ 614 | Publisher document ID 615 | """ 616 | id: ID! 617 | ): Publisher 618 | SanityImageAsset( 619 | """ 620 | SanityImageAsset document ID 621 | """ 622 | id: ID! 623 | ): SanityImageAsset 624 | SanityFileAsset( 625 | """ 626 | SanityFileAsset document ID 627 | """ 628 | id: ID! 629 | ): SanityFileAsset 630 | Document( 631 | """ 632 | Document document ID 633 | """ 634 | id: ID! 635 | ): Document 636 | allBook( 637 | where: BookFilter 638 | sort: [BookSorting!] 639 | 640 | """ 641 | Max documents to return 642 | """ 643 | limit: Int 644 | 645 | """ 646 | Offset at which to start returning documents from 647 | """ 648 | offset: Int 649 | ): [Book!]! 650 | allGenre( 651 | where: GenreFilter 652 | sort: [GenreSorting!] 653 | 654 | """ 655 | Max documents to return 656 | """ 657 | limit: Int 658 | 659 | """ 660 | Offset at which to start returning documents from 661 | """ 662 | offset: Int 663 | ): [Genre!]! 664 | allPublisher( 665 | where: PublisherFilter 666 | sort: [PublisherSorting!] 667 | 668 | """ 669 | Max documents to return 670 | """ 671 | limit: Int 672 | 673 | """ 674 | Offset at which to start returning documents from 675 | """ 676 | offset: Int 677 | ): [Publisher!]! 678 | allSanityImageAsset( 679 | where: SanityImageAssetFilter 680 | sort: [SanityImageAssetSorting!] 681 | 682 | """ 683 | Max documents to return 684 | """ 685 | limit: Int 686 | 687 | """ 688 | Offset at which to start returning documents from 689 | """ 690 | offset: Int 691 | ): [SanityImageAsset!]! 692 | allSanityFileAsset( 693 | where: SanityFileAssetFilter 694 | sort: [SanityFileAssetSorting!] 695 | 696 | """ 697 | Max documents to return 698 | """ 699 | limit: Int 700 | 701 | """ 702 | Offset at which to start returning documents from 703 | """ 704 | offset: Int 705 | ): [SanityFileAsset!]! 706 | allDocument( 707 | where: DocumentFilter 708 | sort: [DocumentSorting!] 709 | 710 | """ 711 | Max documents to return 712 | """ 713 | limit: Int 714 | 715 | """ 716 | Offset at which to start returning documents from 717 | """ 718 | offset: Int 719 | ): [Document!]! 720 | } 721 | 722 | input Sanity_DocumentFilter { 723 | """ 724 | All documents referencing the given document ID. 725 | """ 726 | references: ID 727 | 728 | """ 729 | All documents that are drafts. 730 | """ 731 | is_draft: Boolean 732 | } 733 | 734 | type SanityAssetSourceData { 735 | _key: String 736 | _type: String 737 | 738 | """ 739 | A canonical name for the source this asset is originating from 740 | """ 741 | name: String 742 | 743 | """ 744 | The unique ID for the asset within the originating source so you can programatically find back to it 745 | """ 746 | id: String 747 | 748 | """ 749 | A URL to find more information about this asset in the originating source 750 | """ 751 | url: String 752 | } 753 | 754 | input SanityAssetSourceDataFilter { 755 | _key: StringFilter 756 | _type: StringFilter 757 | name: StringFilter 758 | id: StringFilter 759 | url: StringFilter 760 | } 761 | 762 | input SanityAssetSourceDataSorting { 763 | _key: SortOrder 764 | _type: SortOrder 765 | name: SortOrder 766 | id: SortOrder 767 | url: SortOrder 768 | } 769 | 770 | type SanityFileAsset implements Document { 771 | """ 772 | Document ID 773 | """ 774 | _id: ID 775 | 776 | """ 777 | Document type 778 | """ 779 | _type: String 780 | 781 | """ 782 | Date the document was created 783 | """ 784 | _createdAt: DateTime 785 | 786 | """ 787 | Date the document was last modified 788 | """ 789 | _updatedAt: DateTime 790 | 791 | """ 792 | Current document revision 793 | """ 794 | _rev: String 795 | _key: String 796 | originalFilename: String 797 | label: String 798 | title: String 799 | description: String 800 | altText: String 801 | sha1hash: String 802 | extension: String 803 | mimeType: String 804 | size: Float 805 | assetId: String 806 | path: String 807 | url: String 808 | source: SanityAssetSourceData 809 | } 810 | 811 | input SanityFileAssetFilter { 812 | """ 813 | Apply filters on document level 814 | """ 815 | _: Sanity_DocumentFilter 816 | _id: IDFilter 817 | _type: StringFilter 818 | _createdAt: DatetimeFilter 819 | _updatedAt: DatetimeFilter 820 | _rev: StringFilter 821 | _key: StringFilter 822 | originalFilename: StringFilter 823 | label: StringFilter 824 | title: StringFilter 825 | description: StringFilter 826 | altText: StringFilter 827 | sha1hash: StringFilter 828 | extension: StringFilter 829 | mimeType: StringFilter 830 | size: FloatFilter 831 | assetId: StringFilter 832 | path: StringFilter 833 | url: StringFilter 834 | source: SanityAssetSourceDataFilter 835 | } 836 | 837 | input SanityFileAssetSorting { 838 | _id: SortOrder 839 | _type: SortOrder 840 | _createdAt: SortOrder 841 | _updatedAt: SortOrder 842 | _rev: SortOrder 843 | _key: SortOrder 844 | originalFilename: SortOrder 845 | label: SortOrder 846 | title: SortOrder 847 | description: SortOrder 848 | altText: SortOrder 849 | sha1hash: SortOrder 850 | extension: SortOrder 851 | mimeType: SortOrder 852 | size: SortOrder 853 | assetId: SortOrder 854 | path: SortOrder 855 | url: SortOrder 856 | source: SanityAssetSourceDataSorting 857 | } 858 | 859 | type SanityImageAsset implements Document { 860 | """ 861 | Document ID 862 | """ 863 | _id: ID 864 | 865 | """ 866 | Document type 867 | """ 868 | _type: String 869 | 870 | """ 871 | Date the document was created 872 | """ 873 | _createdAt: DateTime 874 | 875 | """ 876 | Date the document was last modified 877 | """ 878 | _updatedAt: DateTime 879 | 880 | """ 881 | Current document revision 882 | """ 883 | _rev: String 884 | _key: String 885 | originalFilename: String 886 | label: String 887 | title: String 888 | description: String 889 | altText: String 890 | sha1hash: String 891 | extension: String 892 | mimeType: String 893 | size: Float 894 | assetId: String 895 | uploadId: String 896 | path: String 897 | url: String 898 | metadata: SanityImageMetadata 899 | source: SanityAssetSourceData 900 | } 901 | 902 | input SanityImageAssetFilter { 903 | """ 904 | Apply filters on document level 905 | """ 906 | _: Sanity_DocumentFilter 907 | _id: IDFilter 908 | _type: StringFilter 909 | _createdAt: DatetimeFilter 910 | _updatedAt: DatetimeFilter 911 | _rev: StringFilter 912 | _key: StringFilter 913 | originalFilename: StringFilter 914 | label: StringFilter 915 | title: StringFilter 916 | description: StringFilter 917 | altText: StringFilter 918 | sha1hash: StringFilter 919 | extension: StringFilter 920 | mimeType: StringFilter 921 | size: FloatFilter 922 | assetId: StringFilter 923 | uploadId: StringFilter 924 | path: StringFilter 925 | url: StringFilter 926 | metadata: SanityImageMetadataFilter 927 | source: SanityAssetSourceDataFilter 928 | } 929 | 930 | input SanityImageAssetSorting { 931 | _id: SortOrder 932 | _type: SortOrder 933 | _createdAt: SortOrder 934 | _updatedAt: SortOrder 935 | _rev: SortOrder 936 | _key: SortOrder 937 | originalFilename: SortOrder 938 | label: SortOrder 939 | title: SortOrder 940 | description: SortOrder 941 | altText: SortOrder 942 | sha1hash: SortOrder 943 | extension: SortOrder 944 | mimeType: SortOrder 945 | size: SortOrder 946 | assetId: SortOrder 947 | uploadId: SortOrder 948 | path: SortOrder 949 | url: SortOrder 950 | metadata: SanityImageMetadataSorting 951 | source: SanityAssetSourceDataSorting 952 | } 953 | 954 | type SanityImageCrop { 955 | _key: String 956 | _type: String 957 | top: Float 958 | bottom: Float 959 | left: Float 960 | right: Float 961 | } 962 | 963 | input SanityImageCropFilter { 964 | _key: StringFilter 965 | _type: StringFilter 966 | top: FloatFilter 967 | bottom: FloatFilter 968 | left: FloatFilter 969 | right: FloatFilter 970 | } 971 | 972 | input SanityImageCropSorting { 973 | _key: SortOrder 974 | _type: SortOrder 975 | top: SortOrder 976 | bottom: SortOrder 977 | left: SortOrder 978 | right: SortOrder 979 | } 980 | 981 | type SanityImageDimensions { 982 | _key: String 983 | _type: String 984 | height: Float 985 | width: Float 986 | aspectRatio: Float 987 | } 988 | 989 | input SanityImageDimensionsFilter { 990 | _key: StringFilter 991 | _type: StringFilter 992 | height: FloatFilter 993 | width: FloatFilter 994 | aspectRatio: FloatFilter 995 | } 996 | 997 | input SanityImageDimensionsSorting { 998 | _key: SortOrder 999 | _type: SortOrder 1000 | height: SortOrder 1001 | width: SortOrder 1002 | aspectRatio: SortOrder 1003 | } 1004 | 1005 | type SanityImageHotspot { 1006 | _key: String 1007 | _type: String 1008 | x: Float 1009 | y: Float 1010 | height: Float 1011 | width: Float 1012 | } 1013 | 1014 | input SanityImageHotspotFilter { 1015 | _key: StringFilter 1016 | _type: StringFilter 1017 | x: FloatFilter 1018 | y: FloatFilter 1019 | height: FloatFilter 1020 | width: FloatFilter 1021 | } 1022 | 1023 | input SanityImageHotspotSorting { 1024 | _key: SortOrder 1025 | _type: SortOrder 1026 | x: SortOrder 1027 | y: SortOrder 1028 | height: SortOrder 1029 | width: SortOrder 1030 | } 1031 | 1032 | type SanityImageMetadata { 1033 | _key: String 1034 | _type: String 1035 | location: Geopoint 1036 | dimensions: SanityImageDimensions 1037 | palette: SanityImagePalette 1038 | lqip: String 1039 | blurHash: String 1040 | hasAlpha: Boolean 1041 | isOpaque: Boolean 1042 | } 1043 | 1044 | input SanityImageMetadataFilter { 1045 | _key: StringFilter 1046 | _type: StringFilter 1047 | location: GeopointFilter 1048 | dimensions: SanityImageDimensionsFilter 1049 | palette: SanityImagePaletteFilter 1050 | lqip: StringFilter 1051 | blurHash: StringFilter 1052 | hasAlpha: BooleanFilter 1053 | isOpaque: BooleanFilter 1054 | } 1055 | 1056 | input SanityImageMetadataSorting { 1057 | _key: SortOrder 1058 | _type: SortOrder 1059 | location: GeopointSorting 1060 | dimensions: SanityImageDimensionsSorting 1061 | palette: SanityImagePaletteSorting 1062 | lqip: SortOrder 1063 | blurHash: SortOrder 1064 | hasAlpha: SortOrder 1065 | isOpaque: SortOrder 1066 | } 1067 | 1068 | type SanityImagePalette { 1069 | _key: String 1070 | _type: String 1071 | darkMuted: SanityImagePaletteSwatch 1072 | lightVibrant: SanityImagePaletteSwatch 1073 | darkVibrant: SanityImagePaletteSwatch 1074 | vibrant: SanityImagePaletteSwatch 1075 | dominant: SanityImagePaletteSwatch 1076 | lightMuted: SanityImagePaletteSwatch 1077 | muted: SanityImagePaletteSwatch 1078 | } 1079 | 1080 | input SanityImagePaletteFilter { 1081 | _key: StringFilter 1082 | _type: StringFilter 1083 | darkMuted: SanityImagePaletteSwatchFilter 1084 | lightVibrant: SanityImagePaletteSwatchFilter 1085 | darkVibrant: SanityImagePaletteSwatchFilter 1086 | vibrant: SanityImagePaletteSwatchFilter 1087 | dominant: SanityImagePaletteSwatchFilter 1088 | lightMuted: SanityImagePaletteSwatchFilter 1089 | muted: SanityImagePaletteSwatchFilter 1090 | } 1091 | 1092 | input SanityImagePaletteSorting { 1093 | _key: SortOrder 1094 | _type: SortOrder 1095 | darkMuted: SanityImagePaletteSwatchSorting 1096 | lightVibrant: SanityImagePaletteSwatchSorting 1097 | darkVibrant: SanityImagePaletteSwatchSorting 1098 | vibrant: SanityImagePaletteSwatchSorting 1099 | dominant: SanityImagePaletteSwatchSorting 1100 | lightMuted: SanityImagePaletteSwatchSorting 1101 | muted: SanityImagePaletteSwatchSorting 1102 | } 1103 | 1104 | type SanityImagePaletteSwatch { 1105 | _key: String 1106 | _type: String 1107 | background: String 1108 | foreground: String 1109 | population: Float 1110 | title: String 1111 | } 1112 | 1113 | input SanityImagePaletteSwatchFilter { 1114 | _key: StringFilter 1115 | _type: StringFilter 1116 | background: StringFilter 1117 | foreground: StringFilter 1118 | population: FloatFilter 1119 | title: StringFilter 1120 | } 1121 | 1122 | input SanityImagePaletteSwatchSorting { 1123 | _key: SortOrder 1124 | _type: SortOrder 1125 | background: SortOrder 1126 | foreground: SortOrder 1127 | population: SortOrder 1128 | title: SortOrder 1129 | } 1130 | 1131 | type Slug { 1132 | _key: String 1133 | _type: String 1134 | current: String 1135 | source: String 1136 | } 1137 | 1138 | input SlugFilter { 1139 | _key: StringFilter 1140 | _type: StringFilter 1141 | current: StringFilter 1142 | source: StringFilter 1143 | } 1144 | 1145 | input SlugSorting { 1146 | _key: SortOrder 1147 | _type: SortOrder 1148 | current: SortOrder 1149 | source: SortOrder 1150 | } 1151 | 1152 | enum SortOrder { 1153 | """ 1154 | Sorts on the value in ascending order. 1155 | """ 1156 | ASC 1157 | 1158 | """ 1159 | Sorts on the value in descending order. 1160 | """ 1161 | DESC 1162 | } 1163 | 1164 | type Span { 1165 | _key: String 1166 | _type: String 1167 | marks: [String] 1168 | text: String 1169 | } 1170 | 1171 | input StringFilter { 1172 | """ 1173 | Checks if the value is equal to the given input. 1174 | """ 1175 | eq: String 1176 | 1177 | """ 1178 | Checks if the value is not equal to the given input. 1179 | """ 1180 | neq: String 1181 | 1182 | """ 1183 | Checks if the value matches the given word/words. 1184 | """ 1185 | matches: String 1186 | in: [String!] 1187 | nin: [String!] 1188 | 1189 | """ 1190 | Checks if the value is defined. 1191 | """ 1192 | is_defined: Boolean 1193 | } 1194 | --------------------------------------------------------------------------------