/**
Copyright 2021 Forestry.io Holdings, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/ import _ from 'lodash' ; import { graphql , buildASTSchema , getNamedType , print , OperationDefinitionNode , FieldNode , visit , FragmentDefinitionNode , Visitor , ASTKindToNode , ASTNode , GraphQLFieldMap , GraphQLError , parse , TypeInfo , visitWithTypeInfo , GraphQLUnionType , } from 'graphql' ; import { createSchema } from './schema' ; import { createResolver } from './resolver' ; import { assertShape } from './util' ; import type { GraphQLResolveInfo } from 'graphql' ; import type { Database } from './database' ; import type { TinaCloudCollection } from './types' ; import { Path } from 'graphql/jsutils/Path' ; import { NAMER } from './ast-builder' ; type VisitorType = Visitor < ASTKindToNode , ASTNode > ; type Frag = { name : string ; node : FragmentDefinitionNode ; subFrags : string [ ] ; } ; type ReferenceQuery = { path : ( string | number ) [ ] ; dataPath : ( string | number ) [ ] ; queryString : string ; } ; type PreparedMutation = { path : ( string | number ) [ ] ; string : string ; includeCollection : boolean ; } ; export const resolve = async ( { query , variables , database , silenceErrors , } ) => { try { const graphQLSchemaAst = await database . getGraphQLSchema ( ) ; const graphQLSchema = buildASTSchema ( graphQLSchemaAst ) ; const config = await database . getTinaSchema ( ) ; const tinaSchema = await createSchema ( { schema : config } ) ; const resolver = await createResolver ( { database , tinaSchema } ) ; const paths = [ ] ; const mutationPaths = [ ] ; const ast = parse ( query ) ; const typeInfo = new TypeInfo ( graphQLSchema ) ; const referencePathVisitor = ( ) : VisitorType => { return { leave : { Field ( node , key , parent , path , ancestors ) { const type = typeInfo . getType ( ) ; if ( type ) { const realType = getNamedType ( type ) ; if ( realType instanceof GraphQLUnionType ) { const hasNodeInterface = ! ! realType . getTypes ( ) . find ( ( objectType ) => objectType . getInterfaces ( ) . find ( ( intfc ) => intfc . name === 'Node' ) ) ; if ( hasNodeInterface ) { const p = [ ] ; ancestors . forEach ( ( item , index ) => { const activePath = path [ index ] ; const result = item [ activePath ] ; if ( result ?. name ?. value ) { if ( result . kind === 'Field' ) { p . push ( result . name . value ) ; } } } ) ; const referenceQuery = buildReferenceQuery ( node , [ ... p , node . name . value , ] ) ; if ( referenceQuery ) { paths . push ( referenceQuery ) ; } } } } } , } , } ; } ; visit ( ast , visitWithTypeInfo ( typeInfo , referencePathVisitor ( ) ) ) ; const res = await graphql ( { schema : graphQLSchema , source : query , variableValues : variables , contextValue : { database , } , typeResolver : async ( source , _args , info ) => { if ( source . __typename ) return source . __typename ; const namedType = getNamedType ( info . returnType ) . toString ( ) ; const lookup = await database . getLookup ( namedType ) ; if ( lookup . resolveType === 'unionData' ) { return lookup . typeMap [ source . _template ] ; } else { throw new Error ( `Unable to find lookup key for ${ namedType }` ) ; } } , fieldResolver : async ( source = { } , _args = { } , _context , info ) => { const args = JSON . parse ( JSON . stringify ( _args ) ) ; const returnType = getNamedType ( info . returnType ) . toString ( ) ; const lookup = await database . getLookup ( returnType ) ; const isMutation = info . parentType . toString ( ) === 'Mutation' ; const value = source [ info . fieldName ] ; /**
                 * `getCollection`
                 */ if ( returnType === 'Collection' ) { if ( value ) { return value ; } if ( info . fieldName === 'getCollections' ) { return tinaSchema . getCollections ( ) . map ( ( collection ) => { return resolver . resolveCollection ( collection . name ) ; } ) ; } return resolver . resolveCollection ( args . collection ) ; } /**
                 * `getDocumentFields`
                 */ if ( info . fieldName === 'getDocumentFields' ) { return resolver . getDocumentFields ( ) ; } if ( ! lookup ) { return value ; } const isCreation = lookup [ info . fieldName ] === 'create' ; /**
                 * From here, we need more information on how to resolve this, aided
                 * by the lookup value for the given return type, we can enrich the request
                 * with more contextual information that we gathered at build-time.
                 */ switch ( lookup . resolveType ) { case 'nodeDocument' : assertShape < { id : string ; } > ( args , ( yup ) => yup . object ( { id : yup . string ( ) . required ( ) } ) ) ; return resolver . getDocument ( args . id ) ; case 'multiCollectionDocument' : if ( typeof value === 'string' ) { /**
                             * This is a reference value (`director: /path/to/george.md`)
                             */ return resolver . getDocument ( value ) ; } if ( args && args . collection && info . fieldName === 'addPendingDocument' ) { /**
                             * `addPendingDocument`
                             * FIXME: this should probably be it's own lookup
                             */ return resolver . resolveDocument ( { args : { ... args , params : { } } , collection : args . collection , isMutation , isCreation : true , isAddPendingDocument : true , } ) ; } if ( [ 'getDocument' , 'createDocument' , 'updateDocument' ] . includes ( info . fieldName ) ) { /**
                             * `getDocument`/`createDocument`/`updateDocument`
                             */ const result = await resolver . resolveDocument ( { args , collection : args . collection , isMutation , isCreation , isAddPendingDocument : false , isCollectionSpecific : false , } ) ; if ( ! isMutation ) { const mutationPath = buildMutationPath ( info , { relativePath : result . sys . relativePath , } ) ; if ( mutationPath ) { mutationPaths . push ( mutationPath ) ; } } return result ; } return value ; case 'multiCollectionDocumentList' : if ( Array . isArray ( value ) ) { return { totalCount : value . length , edges : value . map ( ( document ) => { return { node : document } ; } ) , } ; } else { throw new Error ( `Expected an array for result of ${ info . fieldName } at ${ info . path }` ) ; } case 'collectionDocument' : if ( value ) { return value ; } const result = value || ( await resolver . resolveDocument ( { args , collection : lookup . collection , isMutation , isCreation , isAddPendingDocument : false , isCollectionSpecific : true , } ) ) ; if ( ! isMutation ) { const mutationPath = buildMutationPath ( info , { collection : tinaSchema . getCollection ( lookup . collection ) , relativePath : result . sys . relativePath , } ) ; if ( mutationPath ) { mutationPaths . push ( mutationPath ) ; } } return result ; case 'collectionDocumentList' : return resolver . resolveCollectionConnection ( { args , lookup } ) ; case 'unionData' : return value ; default : console . error ( lookup ) ; throw new Error ( `Unexpected resolve type` ) ; } } , } ) ; /**
         * This code will benefit from supporting nested forms. Right now, the way it works
         * is whenever we come across a reference field, we store some info about it, and when
         * it's `value` changes in a tina form, we use this info to refetch the data (since the
         * reference has changed, the data needs to change too). It's an imperative approach
         * which doesn't fit well with the highly-dynamic nature of the GraphQL system.
         *
         * ```js
         *  {
         *    // the location of this node's form
         *    path: [ 'data', 'getPostsDocument', 'form' ],
         *    // the path to the data node of the reference
         *    dataPath: [ 'getPostsDocument', 'data', 'author' ],
         *    // ths part of the query we're referencing
         *    queryString: 'query GetNode($id: String!) {\n' +
         *      '  node(id: $id) {\n' +
         *      '    ... on AuthorsDocument {\n' +
         *      '      data {\n' +
         *      '        name\n' +
         *      '        avatar\n' +
         *      '      }\n' +
         *      '    }\n' +
         *      '  }\n' +
         *      '}'
         *  }
         * ```
         * But with support for nested forms we don't need any of this, each reference can just
         * build itself async and it will likely be far simpler from the backend.
         */ paths . forEach ( ( p ) => { const item = _ . get ( res , p . path . slice ( 0 , 3 ) ) ; if ( item ) { item . paths = [ ... ( item . paths || [ ] ) , p ] ; } } ) ; mutationPaths . forEach ( ( mutationPath ) => { const item = _ . get ( res , mutationPath . path ) ; if ( item ) { item . mutationInfo = mutationPath ; } } ) ; if ( res . errors ) { if ( ! silenceErrors ) { console . error ( res . errors ) ; } } return res ; } catch ( e ) { if ( ! silenceErrors ) { console . error ( e ) ; } if ( e instanceof GraphQLError ) { return { errors : [ e ] , } ; } else { throw e ; } } } ; const buildPath = ( path , accum ) => { if ( path . prev ) { buildPath ( path . prev , accum ) ; } accum . push ( path . key ) ; return accum ; } ; const buildReferenceQuery = ( fieldNode , path ) : ReferenceQuery | undefined => { if ( fieldNode ) { const p = path . map ( ( item ) => ( item === 'data' ? 'form' : item ) ) ; const dataPath = path ; const newNode = { ... fieldNode , name : { kind : 'Name' , value : 'node' } , arguments : [ { kind : 'Argument' , name : { kind : 'Name' , value : 'id' , } , value : { kind : 'Variable' , name : { kind : 'Name' , value : 'id' , } , } , } , ] , } ; const q = { kind : 'OperationDefinition' , operation : 'query' , name : { value : 'GetNode' , kind : 'Name' , } , variableDefinitions : [ { kind : 'VariableDefinition' , variable : { kind : 'Variable' , name : { kind : 'Name' , value : 'id' , } , } , type : { kind : 'NonNullType' , type : { kind : 'NamedType' , name : { kind : 'Name' , value : 'String' , } , } , } , } , ] , selectionSet : { kind : 'SelectionSet' , selections : [ newNode ] , } , } ; const queryString = print ( q ) ; return { path : [ 'data' , ... p . slice ( 0 , - 1 ) ] , dataPath , queryString : queryString , } ; } } ; const buildMutationPath = ( info , { collection , relativePath , } ) => { const queryNode = info . fieldNodes . find ( ( fn ) => fn . name . value === info . fieldName ) ; if ( ! queryNode ) { throw new Error ( `exptected to find field node for ${ info . fieldName }` ) ; } const mutationName = collection ? NAMER . updateName ( [ collection . name ] ) : 'updateDocument' ; const mutations = JSON . parse ( JSON . stringify ( info . schema . getMutationType ( ) ?. getFields ( ) ) ) as GraphQLFieldMap < any , any > ; const mutation = mutations [ mutationName ] ; if ( ! mutation ) { throw new Error ( `exptected to find mutation for ${ mutationName }` ) ; } const mutationNode = mutations [ mutationName ] . astNode ; const newNode = { ... queryNode , name : { kind : 'Name' , value : mutation . name } , arguments : mutationNode ?. arguments ?. map ( ( argument ) => { if ( argument . name . value === 'relativePath' ) { return { kind : 'Argument' , name : { kind : 'Name' , value : argument . name . value , } , value : { kind : 'StringValue' , value : relativePath , } , } ; } return { kind : 'Argument' , name : { kind : 'Name' , value : argument . name . value , } , value : { kind : 'Variable' , name : { kind : 'Name' , value : argument . name . value , } , } , } ; } ) , } ; const paramArgs = mutationNode ?. arguments ?. find ( ( arg ) => arg . name . value === 'params' ) ; if ( ! paramArgs ) { throw new Error ( `Expected to find argument named params for mutation ${ mutationName }` ) ; } const q = { kind : 'OperationDefinition' , operation : 'mutation' , name : { value : 'UpdateDocument' , kind : 'Name' , } , variableDefinitions : [ { kind : 'VariableDefinition' , variable : { kind : 'Variable' , name : { kind : 'Name' , value : 'params' , } , } , type : paramArgs ?. type , } , ] , selectionSet : { kind : 'SelectionSet' , selections : [ newNode ] , } , } ; const mutationString = addFragmentsToQuery ( info , newNode , q ) ; return { path : [ 'data' , ... buildPath ( info . path , [ ] ) , 'form' ] , string : mutationString , includeCollection : collection ? false : true , includeTemplate : collection ? ! ! collection . templates : false , } ; } ; function addFragmentsToQuery ( info , fieldNode , q ) { const fragmentSpreadVisitor = ( frag ) : VisitorType => { return { leave : { FragmentSpread ( node ) { frag . subFrags . push ( node . name . value ) ; } , } , } ; } ; const frags = [ ] ; Object . entries ( info . fragments ) . map ( ( [ fragmentName , fragmentDefinition ] ) => { const frag = { name : fragmentName , node : fragmentDefinition , subFrags : [ ] , } ; frags . push ( frag ) ; visit ( fragmentDefinition , fragmentSpreadVisitor ( frag ) ) ; } ) ; const n = { query : print ( fieldNode ) , fragments : [ ] , } ; const visitor = { leave : { FragmentSpread ( node ) { n . fragments . push ( node . name . value ) ; } , } , } ; visit ( fieldNode , visitor ) ; const getFrags = ( fragNames , accum ) => { fragNames . forEach ( ( fragName ) => { const frag = frags . find ( ( f ) => f . name === fragName ) ; if ( ! frag ) { throw new Error ( `Unable to find fragment ${ fragName }` ) ; } accum . push ( frag . node ) ; if ( frag . subFrags ) { getFrags ( frag . subFrags , accum ) ; } } ) ; return accum ; } ; const fragss = getFrags ( n . fragments , [ ] ) ; const queryString = `${ fragss . map ( ( f ) => print ( f ) ) . join ( '\n' ) }\n${ print ( q ) }` ; return queryString ; }