import * as deepmerge from 'deepmerge' ; import * as fs from 'fs' ; import * as Glob from 'glob' ; import { buildASTSchema , DocumentNode , GraphQLSchema , ObjectTypeDefinitionNode , parse , SchemaDefinitionNode , } from 'graphql' ; import { addResolveFunctionsToSchema } from 'graphql-tools' ; import * as Kind from 'graphql/language/kinds' ; export class GraphQLLoaderError extends Error { public static zeroMatchError ( glob ) { return new GraphQLLoaderError ( `The glob pattern "${ glob }" has zero matches` ) ; } constructor ( message ) { super ( message ) ; this . name = 'GraphQLLoaderError' ; } } export interface ISchemaCallback { ( err , schema ) ; } export interface ILoadSchemaFunc { ( pattern , callback ? ) : Promise < GraphQLSchema > ; sync ? : ( pattern : string ) => GraphQLSchema ; } export interface IGraphQLModule { document : DocumentNode ; resolvers ? : any ; } export type IGraphQLModuleFunction = ( ) => IGraphQLModule | Promise < IGraphQLModule > ; param pattern param callback export const loadSchema = ( pattern , callback ? ) : Promise < GraphQLSchema > => { return new Promise ( ( resolve , reject ) => { loadDocument ( pattern ) . then ( buildASTSchema ) . then ( ( schema ) => callback ? callback ( null , schema ) : resolve ( schema ) ) . catch ( ( err ) => callback ? callback ( err , null ) : reject ( err ) ) ; } ) ; } ; param pattern export const loadDocument = ( pattern ) : Promise < DocumentNode > => getGlob ( pattern ) . then ( combineFiles ) . then ( parse ) ; param docs export const combineDocuments = ( docs ) : GraphQLSchema => buildASTSchema ( concatAST ( docs ) ) ; param modules export const executableSchemaFromModules = ( modules ) : Promise < GraphQLSchema > => { const promises = convertModulesToPromises ( modules ) ; return Promise . all ( promises ) . then ( ( gqlModules ) => { const schema = combineDocuments ( gqlModules . map ( ( mod ) => mod . document ) ) ; const resolvers = gqlModules . reduce ( ( prev , curr ) => deepmerge ( prev , curr . resolvers || { } ) , { } ) ; addResolveFunctionsToSchema ( schema , resolvers ) ; return schema ; } ) . catch ( ( e ) => { throw e ; } ) ; } ; const convertModulesToPromises = ( modules ) : Array < Promise < IGraphQLModule > > => { return ( modules as IGraphQLModuleFunction [ ] ) . map ( ( mod ) => { const result = typeof mod === 'function' ? mod ( ) : mod ; if ( ( result as IGraphQLModule ) . document ) { return Promise . resolve ( result ) ; } else { return result as Promise < IGraphQLModule > ; } } ) ; } ; const filterDups = ( dups , doc , index , orig ) => { const hasDups = orig . filter ( ( _ ) => _ . name . value === doc . name . value ) . length > 1 ; const docInDups = dups . find ( ( _ ) => _ . name . value === doc . name . value ) ; return hasDups && ! docInDups ? dups . concat ( doc ) : dups ; } ; const mergeFields = ( allDefs ) => ( dup ) => { allDefs . forEach ( ( def ) => { if ( def . name && def . name . value === dup . name . value && def !== dup ) { dup . fields = dup . fields . concat ( def . fields ) ; } } ) ; return dup ; } ; const mergeOperations = ( allDefs ) => ( dup ) => { allDefs . forEach ( ( def ) => { if ( def . kind === Kind . SCHEMA_DEFINITION && def !== dup ) { const findTypes = ( opType ) => ! dup . operationTypes . find ( ( dupType ) => dupType . operation === opType . operation ) ; const deduped = def . operationTypes . filter ( findTypes ) ; dup . operationTypes = dup . operationTypes . concat ( deduped ) ; } } ) ; return dup ; } ; const isKind = ( kind ) => ( def ) => def . kind === kind ; const filterSchemaAndDups = ( dups ) => ( def ) => { return def . kind !== Kind . SCHEMA_DEFINITION && ! dups . find ( ( _ ) => _ . name . value === def . name . value ) ; } ; const concatAST = ( documents ) : DocumentNode => { const allDefs = documents . reduce ( ( defs , doc ) => defs . concat ( doc . definitions ) , [ ] ) ; const dups = allDefs . filter ( isKind ( Kind . OBJECT_TYPE_DEFINITION ) ) . reduce ( filterDups , [ ] ) . map ( mergeFields ( allDefs ) ) ; const schemas = allDefs . filter ( isKind ( Kind . SCHEMA_DEFINITION ) ) . slice ( 0 , 1 ) . map ( mergeOperations ( allDefs ) ) ; const definitions = allDefs . filter ( filterSchemaAndDups ( dups ) ) . concat ( schemas , dups ) ; return { definitions , kind : 'Document' , } ; } ; const combineFiles = ( fileNames ) : Promise < string > => { const promises = fileNames . map ( readFile ) ; return Promise . all ( promises ) . then ( ( fileContents ) => fileContents . join ( ) ) . catch ( ( err ) => { throw err ; } ) ; } ; const getGlob = ( pattern ) : Promise < string [ ] > => { return new Promise ( ( resolve , reject ) => { Glob ( pattern , ( err , files ) => { if ( files . length === 0 ) { reject ( GraphQLLoaderError . zeroMatchError ( pattern ) ) ; } else { resolve ( files ) ; } } ) ; } ) ; } ; const readFile = ( fileName ) : Promise < string > => { return new Promise ( ( resolve , reject ) => { fs . readFile ( fileName , 'utf8' , ( err , data ) => { if ( err ) { reject ( err ) ; } else { resolve ( data ) ; } } ) ; } ) ; } ; loadSchema . sync = ( pattern ) : GraphQLSchema => { const fileNames = getGlobSync ( pattern ) ; const schema = makeSchemaSync ( fileNames ) ; return buildASTSchema ( parse ( schema ) ) ; } ; const getGlobSync = ( pattern ) => { const fileNames = Glob . sync ( pattern ) ; if ( fileNames . length === 0 ) { throw GraphQLLoaderError . zeroMatchError ( pattern ) ; } else { return fileNames ; } } ; const makeSchemaSync = ( fileNames ) => { return fileNames . map ( readFileSync ) . join ( ) ; } ; const readFileSync = ( fileName ) : string => { return fs . readFileSync ( fileName , 'utf8' ) ; } ;