import { GraphQLSchema } from "graphql" ; import { GraphQLAbstractType , GraphQLField , GraphQLInterfaceType , GraphQLObjectType } from "graphql/type/definition" ; import { IGatsbyNodeConfig , IGatsbyNodeDefinition , ISourcingConfig } from "gatsby-graphql-source-toolkit/dist/types" ; import { NodePluginArgs , Reporter } from "gatsby" ; import { createRemoteFileNode } from "gatsby-source-filesystem" ; type SourcePluginOptions = { craftGqlUrl : string ; craftGqlToken : string ; concurrency : number ; debugDir : string ; fragmentsDir : string ; typePrefix : string ; looseInterfaces : boolean ; sourcingParams : { [ key ] : { [ key ] : string ; } ; } ; enabledSites : string | [ string ] | null ; } ; type ModifiedNodeInfo = { nodeId : number ; nodeType : string ; siteId : number ; } ; type WebhookBody = { operation : string ; typeName : string ; id : number ; siteId : number ; token ? : string ; } ; const fs = require ( "fs-extra" ) ; const fetch = require ( "node-fetch" ) ; const path = require ( "path" ) ; const { print } = require ( "gatsby/graphql" ) ; const { sourceAllNodes , sourceNodeChanges , createSchemaCustomization , generateDefaultFragments , compileNodeQueries , buildNodeDefinitions , wrapQueryExecutorWithQueue , loadSchema , } = require ( "gatsby-graphql-source-toolkit" ) ; const { isInterfaceType , isListType } = require ( "graphql" ) ; const loadedPluginOptions = { craftGqlToken : process . env . CRAFTGQL_TOKEN + "" , craftGqlUrl : process . env . CRAFTGQL_URL + "" , concurrency : 10 , debugDir : __dirname + "/.cache/craft-graphql-documents" , fragmentsDir : __dirname + "/.cache/craft-fragments" , typePrefix : "Craft_" , looseInterfaces : false , sourcingParams : { } , enabledSites : null } ; const internalFragmentDir = __dirname + "/.cache/internal-craft-fragments" ; const mandatoryFragments = { ensureRemoteId : 'fragment RequiredEntryFields on EntryInterface { id }' } ; let schema ; let gatsbyNodeTypes ; let sourcingConfig ; let previewToken ; let craftInterfaces = [ ] ; let craftTypesByInterface = { } ; let craftFieldsByInterface = { } ; let craftPrimarySiteId = '' ; let craftEnabledSites = '' ; /**
 * Fetch the schema
 */ async function getSchema ( ) { if ( ! schema ) { schema = await loadSchema ( execute ) ; } return schema ; } /**
 * Return a list of all possible Gatsby node types
 */ async function getGatsbyNodeTypes ( ) { if ( gatsbyNodeTypes ) { return gatsbyNodeTypes ; } const schema = await getSchema ( ) ; const queries = schema . getQueryType ( ) ?. getFields ( ) ; if ( ! queries ) { return ( [ ] ) ; } if ( ! queries . sourceNodeInformation ) { return ( [ ] ) ; } const queryResponse = await execute ( { operationName : 'sourceNodeData' , query : 'query sourceNodeData { sourceNodeInformation { node list filterArgument filterTypeExpression targetInterface } primarySiteId }' , variables : { } , additionalHeaders : { "X-Craft-Gql-Cache" : "no-cache" } } ) ; if ( ! ( queryResponse . data && queryResponse . data . sourceNodeInformation ) ) { return ( [ ] ) ; } craftPrimarySiteId = queryResponse . data . primarySiteId ; const sourceNodeInformation = queryResponse . data . sourceNodeInformation ; const queryMap = { } ; for ( let nodeInformation of sourceNodeInformation ) { queryMap [ nodeInformation . targetInterface ] = { list : nodeInformation . list , node : nodeInformation . node } ; if ( nodeInformation . filterArgument ) { queryMap [ nodeInformation . targetInterface ] . filterArgument = nodeInformation . filterArgument ; } if ( nodeInformation . filterTypeExpression ) { queryMap [ nodeInformation . targetInterface ] . filterTypeExpression = nodeInformation . filterTypeExpression ; } craftInterfaces . push ( nodeInformation . targetInterface ) ; } param string param callable const extractNodesFromInterface = ( ifaceName , queryListBuilder ) : IGatsbyNodeConfig [ ] => { const iface = schema . getType ( ifaceName ) as GraphQLInterfaceType ; if ( ! iface ) { return [ ] ; } for ( let field of Object . values ( iface . getFields ( ) ) ) { if ( craftFieldsByInterface [ ifaceName ] ) { craftFieldsByInterface [ ifaceName ] . push ( field ) ; } else { craftFieldsByInterface [ ifaceName ] = [ field ] ; } } const canBeDraft = ( input ) : boolean => { return typeof input === 'object' && input !== null && '_fields' in input && 'sourceId' in ( input as GraphQLObjectType ) . getFields ( ) ; } ; return schema . getPossibleTypes ( iface ) . map ( type => { if ( craftTypesByInterface [ ifaceName ] ) { craftTypesByInterface [ ifaceName ] . push ( type ) ; } else { craftTypesByInterface [ ifaceName ] = [ type ] ; } return ( { remoteTypeName : type . name , queries : queryListBuilder ( type . name , canBeDraft ( type ) ) , nodeQueryVariables : id => { const idValue = id . sourceId ?? id . id ; return { id : idValue , siteId : id . siteId } ; } } ) ; } ) ; } ; param string const fragmentHelper = ( typeName , canBeDraft ) : { fragmentName : string ; fragment : string ; } => { const fragmentName = '_Craft' + typeName + 'ID_' ; const idProperty = canBeDraft ? 'sourceId' : 'id' ; return { fragmentName : fragmentName , fragment : `
            fragment ${ fragmentName } on ${ typeName } {
                __typename
                ${ idProperty }
                siteId
            }
            ` } ; } ; gatsbyNodeTypes = [ ] ; if ( loadedPluginOptions . enabledSites ) { if ( typeof loadedPluginOptions . enabledSites == "object" ) { craftEnabledSites = `["${ loadedPluginOptions . enabledSites . join ( '", "' ) }"]` ; } else { craftEnabledSites = `"${ loadedPluginOptions . enabledSites }"` ; } } else { craftEnabledSites = `"${ craftPrimarySiteId }"` ; } for ( let [ interfaceName , sourceNodeInformation ] of Object . entries ( queryMap ) ) { gatsbyNodeTypes . push ( ... extractNodesFromInterface ( interfaceName , ( typeName , canBeDraft ) => { let queries = '' ; let fragmentInfo = fragmentHelper ( typeName , canBeDraft ) ; queries = fragmentInfo . fragment ; if ( sourceNodeInformation . node ) { queries += `query NODE_${ typeName } { ${ sourceNodeInformation . node }(id: $id siteId: $siteId status: null) { ... ${ fragmentInfo . fragmentName }  } }
                ` ; } let typeFilter = '' ; if ( sourceNodeInformation . filterArgument ) { let regexp = new RegExp ( sourceNodeInformation . filterTypeExpression as string ) ; const matches = typeName . match ( regexp ) ; if ( matches && matches [ 1 ] ) { typeFilter = sourceNodeInformation . filterArgument + ': "' + matches [ 1 ] + '"' ; } } let configuredParameters = { } ; if ( interfaceName in loadedPluginOptions . sourcingParams ) { configuredParameters = Object . assign ( configuredParameters , loadedPluginOptions . sourcingParams [ interfaceName ] ) ; } if ( typeName in loadedPluginOptions . sourcingParams ) { configuredParameters = Object . assign ( configuredParameters , loadedPluginOptions . sourcingParams [ typeName ] ) ; } let configuredParameterString = '' ; for ( const [ key , value ] of Object . entries ( configuredParameters ) ) { configuredParameterString += `${ key }: ${ value } ` ; } queries += `query LIST_${ typeName } { ${ sourceNodeInformation . list }(${ typeFilter } limit: $limit, offset: $offset site: ${ craftEnabledSites } ${ configuredParameterString }) { ... ${ fragmentInfo . fragmentName } } }
            ` ; return queries ; } ) ) ; } return ( gatsbyNodeTypes ) ; } /**
 * Write default fragments to the disk.
 */ async function writeDefaultFragments ( ) { const defaultFragments = generateDefaultFragments ( { schema : await getSchema ( ) , gatsbyNodeTypes : await getGatsbyNodeTypes ( ) , } ) ; await fs . ensureDir ( internalFragmentDir ) ; for ( const [ remoteTypeName , fragment ] of defaultFragments ) { const filePath = path . join ( internalFragmentDir , `${ remoteTypeName }.graphql` ) ; if ( ! fs . existsSync ( filePath ) ) { await fs . writeFile ( filePath , fragment ) ; } } } async function addExtraFragments ( reporter ) { const fragmentDir = loadedPluginOptions . fragmentsDir ; const fragments = await fs . readdir ( fragmentDir ) ; for ( let [ fragmentName , fragmentBody ] of Object . entries ( mandatoryFragments ) ) { fragmentName += '.graphql' ; const filePath = path . join ( internalFragmentDir , fragmentName ) ; fs . writeFile ( filePath , fragmentBody ) ; } reporter . info ( "Found " + fragments . length + " additional fragments" ) ; for ( const fragmentFile of fragments ) { const extraFile = path . join ( fragmentDir , fragmentFile ) ; const existingFile = path . join ( internalFragmentDir , fragmentFile ) ; const stats = fs . statSync ( extraFile ) ; const fileSizeInBytes = stats [ "size" ] ; if ( fs . existsSync ( existingFile ) ) { reporter . info ( "Overwriting the " + fragmentFile + " fragment" ) ; } else { reporter . info ( "Adding " + fragmentFile + " to additional fragments" ) ; } fs . copyFileSync ( extraFile , existingFile ) ; } } /**
 * Collect fragments from the disk.
 */ async function collectFragments ( ) { const customFragments = [ ] ; for ( const fileName of await fs . readdir ( internalFragmentDir ) ) { if ( /.graphql$/ . test ( fileName ) ) { const filePath = path . join ( internalFragmentDir , fileName ) ; const fragment = await fs . readFile ( filePath ) ; customFragments . push ( fragment . toString ( ) ) ; } } return customFragments ; } param nodeDocs async function writeCompiledQueries ( nodeDocs ) { for ( const [ remoteTypeName , document ] of nodeDocs ) { await fs . writeFile ( loadedPluginOptions . debugDir + `/${ remoteTypeName }.graphql` , print ( document ) ) ; } } param operation async function execute ( operation ) { let { operationName , query , variables = { } , additionalHeaders = { } } = operation ; const headers = { "Content-Type" : "application/json" , Authorization : `Bearer ${ loadedPluginOptions . craftGqlToken }` , ... additionalHeaders } ; if ( previewToken ) { headers [ 'X-Craft-Token' ] = previewToken ; } const res = await fetch ( loadedPluginOptions . craftGqlUrl , { method : "POST" , body : JSON . stringify ( { query , variables , operationName } ) , headers } ) ; previewToken = null ; return await res . json ( ) ; } exports . onPreBootstrap = async ( gatsbyApi , pluginOptions ) => { loadedPluginOptions . craftGqlUrl = pluginOptions . craftGqlUrl ?? loadedPluginOptions . craftGqlUrl ; loadedPluginOptions . craftGqlToken = pluginOptions . craftGqlToken ?? loadedPluginOptions . craftGqlToken ; loadedPluginOptions . concurrency = pluginOptions . concurrency ?? loadedPluginOptions . concurrency ; loadedPluginOptions . debugDir = pluginOptions . debugDir ?? loadedPluginOptions . debugDir ; loadedPluginOptions . fragmentsDir = pluginOptions . fragmentsDir ?? loadedPluginOptions . fragmentsDir ; loadedPluginOptions . typePrefix = pluginOptions . typePrefix ?? loadedPluginOptions . typePrefix ; loadedPluginOptions . looseInterfaces = pluginOptions . looseInterfaces ?? loadedPluginOptions . looseInterfaces ; loadedPluginOptions . sourcingParams = pluginOptions . sourcingParams ?? loadedPluginOptions . sourcingParams ; loadedPluginOptions . enabledSites = pluginOptions . enabledSites ?? loadedPluginOptions . enabledSites ; await fs . ensureDir ( loadedPluginOptions . debugDir ) ; await fs . ensureDir ( loadedPluginOptions . fragmentsDir ) ; await ensureFragmentsExist ( gatsbyApi . reporter ) ; } ; exports . createSchemaCustomization = async ( gatsbyApi ) => { const config = await getSourcingConfig ( gatsbyApi ) ; const { createTypes } = gatsbyApi . actions ; let typeDefs = '' ; for ( let craftInterface of craftInterfaces ) { let extraFields = { } ; let extraFieldsAsString = '' ; let redefineTypes = '' ; const extractFieldType = ( field , onlyNullable ) : string | false => { const fieldName = field . name ; const skippedTypes = [ 'id' , 'parent' , 'children' , 'next' , 'prev' ] ; if ( skippedTypes . includes ( fieldName ) || fieldName . charAt ( 0 ) === '_' ) { return false ; } let fieldType = field . type . toString ( ) ; if ( onlyNullable && fieldType . slice ( - 1 ) == '!' ) { return false ; } for ( let fieldArgument of field . args ) { if ( fieldArgument . type . toString ( ) . slice ( - 1 ) == '!' ) { return false ; } } fieldType = fieldType . replace ( /DateTime/ , 'JSON' ) ; if ( fieldType . match ( /(Int|Float|String|Boolean|ID|JSON)(\]|!\]|$)/ ) ) { return fieldType ; } return fieldType . replace ( /^([^a-z]+)?([a-z_]+)([^a-z]+)?$/i , '$1' + loadedPluginOptions . typePrefix + '$2$3' ) ; } ; if ( craftTypesByInterface [ craftInterface ] ) { if ( loadedPluginOptions . looseInterfaces ) { for ( let gqlType of craftTypesByInterface [ craftInterface ] ) { for ( let field of Object . values ( gqlType . getFields ( ) ) ) { let extractedType = extractFieldType ( field , true ) ; if ( extractedType ) { extraFields [ field . name ] = extractedType ; } } } } else if ( craftFieldsByInterface [ craftInterface ] ) { for ( let field of Object . values ( craftFieldsByInterface [ craftInterface ] ) ) { let extractedType = extractFieldType ( field , false ) ; if ( extractedType ) { extraFields [ field . name ] = extractedType ; } } } for ( let [ fieldName , fieldType ] of Object . entries ( extraFields ) ) { extraFieldsAsString += `${ fieldName }: ${ fieldType }
            ` ; } if ( loadedPluginOptions . looseInterfaces ) { for ( let gqlType of craftTypesByInterface [ craftInterface ] ) { redefineTypes += `type ${ loadedPluginOptions . typePrefix }${ gqlType . name } {
                        id: ID!
                        ${ extraFieldsAsString }
                    }` ; } } } typeDefs += `
            interface ${ loadedPluginOptions . typePrefix }${ craftInterface } implements Node { 
                id: ID!
                ${ extraFieldsAsString }
            }
            
            ${ redefineTypes }
        ` ; } createTypes ( typeDefs ) ; await createSchemaCustomization ( config ) ; } ; exports . createResolvers = async ( { createResolvers , intermediateSchema , actions , cache , createNodeId , store , reporter } ) => { const { createNode } = actions ; const ifaceName = loadedPluginOptions . typePrefix + 'AssetInterface' ; const iface = intermediateSchema . getType ( ifaceName ) as GraphQLInterfaceType ; if ( iface ) { const possibleTypes = intermediateSchema . getPossibleTypes ( iface ) ; const resolvers = { } ; for ( const assetType of possibleTypes ) { resolvers [ assetType ] = { localFile : { type : `File` , async resolve ( source ) { if ( source . url ) { return await createRemoteFileNode ( { url : encodeURI ( source . url ) , store , cache , createNode , createNodeId , reporter } ) ; } } , } , } ; } createResolvers ( resolvers ) ; } } ; exports . sourceNodes = async ( gatsbyApi ) => { const { cache , reporter , webhookBody } = gatsbyApi ; const config = await getSourcingConfig ( gatsbyApi ) ; if ( webhookBody && typeof webhookBody == "object" && Object . keys ( webhookBody ) . length ) { reporter . info ( "Processing webhook." ) ; const nodeEvent = ( webhookBody ) => { const { operation , typeName , id , siteId } = webhookBody ; let eventName = '' ; switch ( operation ) { case 'delete' : eventName = 'DELETE' ; break ; case 'update' : eventName = 'UPDATE' ; break ; } previewToken = webhookBody . token ?? null ; return { eventName , remoteTypeName : typeName , remoteId : { id , __typename : typeName , siteId } , } ; } ; await sourceNodeChanges ( config , { nodeEvents : [ nodeEvent ( webhookBody as WebhookBody ) ] , } ) ; return ; } reporter . info ( "Checking Craft config version." ) ; const { data } = await execute ( { operationName : 'craftState' , query : 'query craftState { configVersion lastUpdateTime }' , variables : { } , additionalHeaders : { "X-Craft-Gql-Cache" : "no-cache" } } ) ; const remoteConfigVersion = data . configVersion ; const remoteContentUpdateTime = data . lastUpdateTime ; const localConfigVersion = ( await cache . get ( `CRAFT_CONFIG_VERSION` ) ) || '' ; const localContentUpdateTime = ( await cache . get ( `CRAFT_LAST_CONTENT_UPDATE` ) ) || '' ; if ( remoteConfigVersion !== localConfigVersion || ! localContentUpdateTime ) { reporter . info ( "Cached content is unavailable or outdated, sourcing _all_ nodes." ) ; await sourceAllNodes ( config ) ; } else { reporter . info ( `Craft config version has not changed since last sourcing. Checking for content changes since "${ localContentUpdateTime }".` ) ; const { data } = await execute ( { operationName : 'nodeChanges' , query : `query nodeChanges {  
                nodesUpdatedSince (since: "${ localContentUpdateTime }" site: ${ craftEnabledSites }) { nodeId nodeType siteId}
                nodesDeletedSince (since: "${ localContentUpdateTime }") { nodeId nodeType siteId}
            }` , variables : { } , additionalHeaders : { "X-Craft-Gql-Cache" : "no-cache" } } ) ; const updatedNodes = data . nodesUpdatedSince as ModifiedNodeInfo [ ] ; const deletedNodes = data . nodesDeletedSince as ModifiedNodeInfo [ ] ; const nodeEvents = [ ... updatedNodes . map ( entry => { return { eventName : 'UPDATE' , remoteTypeName : entry . nodeType , remoteId : { __typename : entry . nodeType , id : entry . nodeId , siteId : entry . siteId } } ; } ) , ... deletedNodes . map ( entry => { return { eventName : 'DELETE' , remoteTypeName : entry . nodeType , remoteId : { __typename : entry . nodeType , id : entry . nodeId , siteId : entry . siteId } } ; } ) ] ; if ( nodeEvents . length ) { reporter . info ( "Sourcing changes for " + nodeEvents . length + " nodes." ) ; } else { reporter . info ( "No content changes found." ) ; } await sourceNodeChanges ( config , { nodeEvents } ) ; } await cache . set ( `CRAFT_CONFIG_VERSION` , remoteConfigVersion ) ; await cache . set ( `CRAFT_LAST_CONTENT_UPDATE` , remoteContentUpdateTime ) ; } ; async function getSourcingConfig ( gatsbyApi ) { if ( sourcingConfig ) { return sourcingConfig ; } const schema = await getSchema ( ) ; const gatsbyNodeTypes = await getGatsbyNodeTypes ( ) ; const documents = await compileNodeQueries ( { schema , gatsbyNodeTypes , customFragments : await collectFragments ( ) , } ) ; await writeCompiledQueries ( documents ) ; return ( sourcingConfig = { gatsbyApi , schema , gatsbyNodeDefs : buildNodeDefinitions ( { gatsbyNodeTypes , documents } ) , gatsbyTypePrefix : loadedPluginOptions . typePrefix , execute : wrapQueryExecutorWithQueue ( execute , { concurrency : loadedPluginOptions . concurrency } ) , verbose : true , } ) ; } async function ensureFragmentsExist ( reporter ) { reporter . info ( "Clearing previous fragments." ) ; await fs . remove ( internalFragmentDir , { recursive : true } ) ; reporter . info ( "Writing default fragments." ) ; await writeDefaultFragments ( ) ; await addExtraFragments ( reporter ) ; }