import SanityClient from '@sanity/client' ; import { CreateSchemaCustomizationArgs , GatsbyNode , Node , ParentSpanPluginArgs , PluginOptions , SetFieldsOnGraphQLNodeTypeArgs , SourceNodesArgs , } from 'gatsby' ; import { GraphQLFieldConfig } from 'gatsby/graphql' ; import gatsbyPkg from 'gatsby/package.json' ; import { uniq } from 'lodash' ; import oneline from 'oneline' ; import { fromEvent , merge , of } from 'rxjs' ; import { bufferWhen , debounceTime , filter , map , tap } from 'rxjs/operators' ; import debug from './debug' ; import { extendImageNode } from './images/extendImageNode' ; import { SanityInputNode } from './types/gatsby' ; import { SanityDocument , SanityWebhookBody } from './types/sanity' ; import { CACHE_KEYS , getCacheKey } from './util/cache' ; import { prefixId , unprefixId } from './util/documentIds' ; import downloadDocuments from './util/downloadDocuments' ; import { ERROR_CODES , ERROR_MAP , prefixId as prefixErrorId , SANITY_ERROR_CODE_MAP , SANITY_ERROR_CODE_MESSAGES , } from './util/errors' ; import { getGraphQLResolverMap } from './util/getGraphQLResolverMap' ; import { getLastBuildTime , registerBuildTime } from './util/getPluginStatus' ; import getSyncWithGatsby from './util/getSyncWithGatsby' ; import handleDeltaChanges from './util/handleDeltaChanges' ; import { handleWebhookEvent } from './util/handleWebhookEvent' ; import { defaultTypeMap , getRemoteGraphQLSchema , getTypeMapFromGraphQLSchema , TypeMap , } from './util/remoteGraphQLSchema' ; import { rewriteGraphQLSchema } from './util/rewriteGraphQLSchema' ; import validateConfig , { PluginConfig } from './util/validateConfig' ; let coreSupportsOnPluginInit ; try { const { isGatsbyNodeLifecycleSupported } = require ( `gatsby-plugin-utils` ) ; if ( isGatsbyNodeLifecycleSupported ( `onPluginInit` ) ) { coreSupportsOnPluginInit = 'stable' ; } else if ( isGatsbyNodeLifecycleSupported ( `unstable_onPluginInit` ) ) { coreSupportsOnPluginInit = 'unstable' ; } } catch ( e ) { console . error ( `Could not check if Gatsby supports onPluginInit lifecycle` ) ; } const defaultConfig = { version : '1' , overlayDrafts : false , graphqlTag : 'default' , watchModeBuffer : 150 , } ; const stateCache = { } ; const initializePlugin = async ( { reporter } , pluginOptions ? ) => { const config = { ... defaultConfig , ... pluginOptions } ; if ( Number ( gatsbyPkg . version . split ( '.' ) [ 0 ] ) < 3 ) { const unsupportedVersionMessage = oneline `
    You are using a version of Gatsby not supported by gatsby-source-sanity.
    Upgrade gatsby to >= 3.0.0 to continue.` ; reporter . panic ( { id : prefixErrorId ( ERROR_CODES . UnsupportedGatsbyVersion ) , context : { sourceMessage : unsupportedVersionMessage } , } ) ; return ; } if ( ! validateConfig ( config , reporter ) ) { throw new Error ( 'Invalid config' ) ; } try { reporter . info ( '[sanity] Fetching remote GraphQL schema' ) ; const client = getClient ( config ) ; const api = await getRemoteGraphQLSchema ( client , config ) ; reporter . info ( '[sanity] Transforming to Gatsby-compatible GraphQL SDL' ) ; const graphqlSdl = await rewriteGraphQLSchema ( api , { config , reporter } ) ; const graphqlSdlKey = getCacheKey ( config , CACHE_KEYS . GRAPHQL_SDL ) ; stateCache [ graphqlSdlKey ] = graphqlSdl ; reporter . info ( '[sanity] Stitching GraphQL schemas from SDL' ) ; const typeMap = getTypeMapFromGraphQLSchema ( api ) ; const typeMapKey = getCacheKey ( config , CACHE_KEYS . TYPE_MAP ) ; stateCache [ typeMapKey ] = typeMap ; } catch ( err ) { if ( err . isWarning ) { err . message . split ( '\n' ) . forEach ( ( line ) => reporter . warn ( line ) ) ; return ; } if ( typeof err . code === 'string' && SANITY_ERROR_CODE_MAP [ err . code ] ) { reporter . panic ( { id : prefixId ( SANITY_ERROR_CODE_MAP [ err . code ] ) , context : { sourceMessage : `[sanity] ${ SANITY_ERROR_CODE_MESSAGES [ err . code ] }` } , } ) ; } const prefix = typeof err . code === 'string' ? `[${ err . code }] ` : '' ; reporter . panic ( { id : prefixId ( ERROR_CODES . SchemaFetchError ) , context : { sourceMessage : `${ prefix }${ err . message }` } , } ) ; } } ; export const onPreInit = async ( { reporter } ) => { if ( ! coreSupportsOnPluginInit && reporter . setErrorMap ) { reporter . setErrorMap ( ERROR_MAP ) ; } } ; export const onPreBootstrap = async ( args , pluginOptions ? ) => { if ( ! coreSupportsOnPluginInit ) { await initializePlugin ( args , pluginOptions ) ; } } ; const onPluginInit = async ( args , pluginOptions ? ) => { args . reporter . setErrorMap ( ERROR_MAP ) ; await initializePlugin ( args , pluginOptions ) ; } ; if ( coreSupportsOnPluginInit === 'stable' ) { exports . onPluginInit = onPluginInit ; } else if ( coreSupportsOnPluginInit === 'unstable' ) { exports . unstable_onPluginInit = onPluginInit ; } export const createResolvers = ( args , pluginOptions ) : any => { const typeMapKey = getCacheKey ( pluginOptions , CACHE_KEYS . TYPE_MAP ) ; const typeMap = ( stateCache [ typeMapKey ] || defaultTypeMap ) as TypeMap ; args . createResolvers ( getGraphQLResolverMap ( typeMap , pluginOptions , args ) ) ; } ; export const createSchemaCustomization = ( { actions } , pluginConfig ) : any => { const { createTypes } = actions ; const graphqlSdlKey = getCacheKey ( pluginConfig , CACHE_KEYS . GRAPHQL_SDL ) ; const graphqlSdl = stateCache [ graphqlSdlKey ] ; createTypes ( graphqlSdl ) ; } ; export const sourceNodes = async ( args , pluginConfig ) => { const config = { ... defaultConfig , ... pluginConfig } ; const { dataset , overlayDrafts , watchMode } = config ; const { actions , createNodeId , createContentDigest , reporter , webhookBody } = args ; const { createNode , createParentChildLink } = actions ; const typeMapKey = getCacheKey ( pluginConfig , CACHE_KEYS . TYPE_MAP ) ; const typeMap = ( stateCache [ typeMapKey ] || defaultTypeMap ) as TypeMap ; const client = getClient ( config ) ; const url = client . getUrl ( `/data/export/${ dataset }?tag=sanity.gatsby.source-nodes` ) ; const processingOptions = { typeMap , createNodeId , createNode , createContentDigest , createParentChildLink , overlayDrafts , } ; if ( webhookBody && Object . keys ( webhookBody ) . length > 0 ) { const webhookHandled = handleWebhookEvent ( args , { client , processingOptions } ) ; if ( ! webhookHandled ) { reporter . warn ( '[sanity] Received webhook is invalid. Make sure your Sanity webhook is configured correctly.' ) ; reporter . info ( `[sanity] Webhook data: ${ JSON . stringify ( webhookBody , null , 2 ) }` ) ; } return ; } const gatsbyNodes = new Map < string , SanityInputNode | Node > ( ) ; let documents = new Map < string , SanityDocument > ( ) ; let syncWithGatsby = getSyncWithGatsby ( { documents , gatsbyNodes , args , processingOptions , typeMap , } ) ; const lastBuildTime = getLastBuildTime ( args ) ; let deltaHandled = false ; if ( lastBuildTime ) { try { const documentIds = ( await client . fetch < string [ ] > ( `*[!(_type match "system.**")]._id` ) ) . map ( unprefixId ) ; const typeMapStateKeys = Object . keys ( stateCache ) . filter ( ( key ) => key . endsWith ( 'typeMap' ) ) ; const sanityDocTypes = Array . from ( new Set ( typeMapStateKeys . reduce ( ( types , curKey ) => { const map = stateCache [ curKey ] as TypeMap ; const documentTypes = Object . keys ( map . objects ) . filter ( ( key ) => map . objects [ key ] . isDocument ) ; return [ ... types , ... documentTypes ] ; } , [ ] as string [ ] ) ) ) ; for ( const docType of sanityDocTypes ) { args . getNodesByType ( docType ) . forEach ( ( node ) => { if ( node . internal . owner === 'gatsby-source-sanity' && typeof node . _id === 'string' && documentIds . includes ( unprefixId ( node . _id ) ) ) { actions . touchNode ( node ) ; gatsbyNodes . set ( unprefixId ( node . _id ) , node ) ; documents . set ( node . _id , node as unknown as SanityDocument ) ; } } ) ; } deltaHandled = await handleDeltaChanges ( { args , lastBuildTime , client , syncWithGatsby , } ) ; if ( ! deltaHandled ) { reporter . warn ( "[sanity] Couldn't retrieve latest changes. Will fetch all documents instead." ) ; } } catch ( error ) { } } function syncAllWithGatsby ( ) { for ( const id of documents . keys ( ) ) { syncWithGatsby ( id ) ; } } function syncIdsWithGatsby ( ids ) { for ( const id of ids ) { syncWithGatsby ( id ) ; } } if ( watchMode ) { reporter . info ( '[sanity] Watch mode enabled, starting a listener' ) ; if ( pluginConfig . watchModeBuffer ) { reporter . warn ( "[sanity] watchModeBuffer isn't a supported option. The plugin will automatically apply changes when Gatsby can handle them." ) ; } const gatsbyEvents = fromEvent ( args . emitter , '*' ) ; client . listen ( '*[!(_id in path("_.**"))]' ) . pipe ( filter ( ( event ) => overlayDrafts || ! event . documentId . startsWith ( 'drafts.' ) ) , tap ( ( event ) => { if ( event . result ) { documents . set ( event . documentId , event . result ) ; } else { documents . delete ( event . documentId ) ; } } ) , map ( ( event ) => event . documentId ) , bufferWhen ( ( ) => merge ( gatsbyEvents , of ( 0 ) ) . pipe ( debounceTime ( config . watchModeBuffer ) ) ) , filter ( ( ids ) => ids . length > 0 ) , map ( ( ids ) => uniq ( ids ) ) , tap ( ( updateIds ) => debug ( 'The following documents updated and will be synced with gatsby: ' , updateIds ) ) , tap ( ( updatedIds ) => syncIdsWithGatsby ( updatedIds ) ) ) . subscribe ( ) ; } if ( ! deltaHandled ) { reporter . info ( '[sanity] Fetching export stream for dataset' ) ; documents = await downloadDocuments ( url , config . token , { includeDrafts : overlayDrafts } ) ; reporter . info ( `[sanity] Done! Exported ${ documents . size } documents.` ) ; syncWithGatsby = getSyncWithGatsby ( { documents , gatsbyNodes , args , processingOptions , typeMap , } ) ; syncAllWithGatsby ( ) ; } registerBuildTime ( args ) ; } ; export const setFieldsOnGraphQLNodeType = async ( context , pluginConfig ) => { const { type } = context ; let fields = { } ; if ( type . name === 'SanityImageAsset' ) { fields = { ... fields , ... extendImageNode ( pluginConfig ) } ; } return fields ; } ; function getClient ( config ) { const { projectId , dataset , token } = config ; return new SanityClient ( { projectId , dataset , token , apiVersion : '1' , useCdn : false , } ) ; }