├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── renovate.json ├── src ├── RemoteSchemaFactory.ts ├── SchemaCache.ts └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .idea 4 | .DS_Store 5 | .envrc 6 | *.log 7 | .graphcoolrc 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '9' 4 | - '8' 5 | - '6' 6 | 7 | after_success: 8 | - npm i -g semantic-release 9 | - semantic-release 10 | 11 | branches: 12 | except: 13 | - /^v\d+\.\d+\.\d+$/ 14 | 15 | notifications: 16 | email: false 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Graphcool, Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-schema-cache 2 | 3 | [![Build Status](https://travis-ci.org/graphcool/graphql-schema-cache.svg?branch=master)](https://travis-ci.org/graphcool/graphql-schema-cache) [![npm version](https://badge.fury.io/js/graphql-schema-cache.svg)](https://badge.fury.io/js/graphql-schema-cache) 4 | 5 | GraphQL schema caching for high-performance schema delegation (schema stitching) 6 | 7 | ## Install 8 | 9 | ```sh 10 | yarn add graphql-schema-cache 11 | ``` 12 | 13 | ## Usage 14 | 15 | See implementation of 16 | [graphcool-binding](https://github.com/graphcool/graphcool-binding). 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-schema-cache", 3 | "version": "0.0.0-semantic-release", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "typescript": { 7 | "definition": "dist/index.d.ts" 8 | }, 9 | "files": [ 10 | "dist" 11 | ], 12 | "repository": "git@github.com:graphcool/graphql-remote.git", 13 | "author": "Tim Suchanek , Johannes Schickling ", 14 | "license": "MIT", 15 | "scripts": { 16 | "build": "tsc -d", 17 | "prepare": "yarn build", 18 | "test": "echo No tests yet" 19 | }, 20 | "peerDependencies": { 21 | "graphql": "^0.12.0", 22 | "graphql-tools": "^2.4.0" 23 | }, 24 | "dependencies": { 25 | "apollo-link": "^1.0.7", 26 | "graphql": "^0.12.3", 27 | "graphql-tools": "^2.14.0" 28 | }, 29 | "devDependencies": { 30 | "@types/graphql": "0.11.7", 31 | "@types/node": "8.5.2", 32 | "typescript": "2.6.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/RemoteSchemaFactory.ts: -------------------------------------------------------------------------------- 1 | // Contains copied code from https://github.com/apollographql/graphql-tools 2 | 3 | // The MIT License (MIT) 4 | 5 | // Copyright (c) 2015 - 2017 Meteor Development Group, Inc. 6 | 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | import { 26 | GraphQLObjectType, 27 | GraphQLFieldResolver, 28 | GraphQLSchema, 29 | GraphQLInterfaceType, 30 | GraphQLUnionType, 31 | GraphQLID, 32 | GraphQLString, 33 | GraphQLFloat, 34 | GraphQLBoolean, 35 | GraphQLInt, 36 | GraphQLScalarType, 37 | ExecutionResult, 38 | print, 39 | printSchema, 40 | Kind, 41 | ValueNode, 42 | } from 'graphql' 43 | import { ApolloLink } from 'apollo-link' 44 | import { makeExecutableSchema } from 'graphql-tools' 45 | import linkToFetcher from 'graphql-tools/dist/stitching/linkToFetcher' 46 | import defaultMergedResolver from 'graphql-tools/dist/stitching/defaultMergedResolver' 47 | import { checkResultAndHandleErrors } from 'graphql-tools/dist/stitching/errors' 48 | import { IResolverObject, IResolvers } from 'graphql-tools/dist/Interfaces' 49 | import resolveFromParentTypename from 'graphql-tools/dist/stitching/resolveFromParentTypename' 50 | import isEmptyObject from 'graphql-tools/dist/isEmptyObject' 51 | 52 | export type Fetcher = (operation: FetcherOperation) => Promise 53 | 54 | export type FetcherOperation = { 55 | query: string 56 | operationName?: string 57 | variables?: { [key: string]: any } 58 | context?: { [key: string]: any } 59 | } 60 | 61 | /** 62 | * The purpose of the class is to be able to "switch out" links/fetchers 63 | * without the need to re-run `makeExecutableSchema` which is expensive to run 64 | */ 65 | export class ExecutableSchemaFactory { 66 | private fetcher: Fetcher 67 | private introspectionSchema: GraphQLSchema 68 | private executableSchema: GraphQLSchema 69 | 70 | constructor(introspectionSchema: GraphQLSchema, link: ApolloLink) { 71 | this.introspectionSchema = introspectionSchema 72 | this.fetcher = linkToFetcher(link) 73 | this.init() 74 | } 75 | 76 | getSchema(): GraphQLSchema { 77 | return this.executableSchema 78 | } 79 | 80 | setLink(link: ApolloLink): void { 81 | this.fetcher = linkToFetcher(link) 82 | } 83 | 84 | private init(): void { 85 | const queryType = this.introspectionSchema.getQueryType() 86 | const queries = queryType.getFields() 87 | const queryResolvers: IResolverObject = {} 88 | Object.keys(queries).forEach(key => { 89 | // the following line was modified 90 | queryResolvers[key] = this.createResolver() 91 | }) 92 | let mutationResolvers: IResolverObject = {} 93 | const mutationType = this.introspectionSchema.getMutationType() 94 | if (mutationType) { 95 | const mutations = mutationType.getFields() 96 | Object.keys(mutations).forEach(key => { 97 | // the following line was modified 98 | mutationResolvers[key] = this.createResolver() 99 | }) 100 | } 101 | 102 | const resolvers: IResolvers = { [queryType.name]: queryResolvers } 103 | 104 | if (!isEmptyObject(mutationResolvers)) { 105 | resolvers[mutationType.name] = mutationResolvers 106 | } 107 | 108 | const typeMap = this.introspectionSchema.getTypeMap() 109 | const types = Object.keys(typeMap).map(name => typeMap[name]) 110 | for (const type of types) { 111 | if ( 112 | type instanceof GraphQLInterfaceType || 113 | type instanceof GraphQLUnionType 114 | ) { 115 | resolvers[type.name] = { 116 | __resolveType(parent, context, info) { 117 | return resolveFromParentTypename(parent, info.introspectionSchema) 118 | }, 119 | } 120 | } else if (type instanceof GraphQLScalarType) { 121 | if ( 122 | !( 123 | type === GraphQLID || 124 | type === GraphQLString || 125 | type === GraphQLFloat || 126 | type === GraphQLBoolean || 127 | type === GraphQLInt 128 | ) 129 | ) { 130 | resolvers[type.name] = createPassThroughScalar(type) 131 | } 132 | } else if ( 133 | type instanceof GraphQLObjectType && 134 | type.name.slice(0, 2) !== '__' && 135 | type !== queryType && 136 | type !== mutationType 137 | ) { 138 | const resolver = {} 139 | Object.keys(type.getFields()).forEach(field => { 140 | resolver[field] = defaultMergedResolver 141 | }) 142 | resolvers[type.name] = resolver 143 | } 144 | } 145 | 146 | const typeDefs = printSchema(this.introspectionSchema) 147 | 148 | this.executableSchema = makeExecutableSchema({ 149 | typeDefs, 150 | resolvers, 151 | }) 152 | } 153 | 154 | private createResolver(): GraphQLFieldResolver { 155 | return async (root, args, context, info) => { 156 | const fragments = Object.keys(info.fragments).map( 157 | fragment => info.fragments[fragment], 158 | ) 159 | const document = { 160 | kind: Kind.DOCUMENT, 161 | definitions: [info.operation, ...fragments], 162 | } 163 | const result = await this.fetcher({ 164 | query: print(document), 165 | variables: info.variableValues, 166 | context: { graphqlContext: context }, 167 | }) 168 | return checkResultAndHandleErrors(result, info) 169 | } 170 | } 171 | } 172 | 173 | function createPassThroughScalar({ 174 | name, 175 | description, 176 | }: { 177 | name: string 178 | description: string 179 | }): GraphQLScalarType { 180 | return new GraphQLScalarType({ 181 | name: name, 182 | description: description, 183 | serialize(value) { 184 | return value 185 | }, 186 | parseValue(value) { 187 | return value 188 | }, 189 | parseLiteral(ast) { 190 | return parseLiteral(ast) 191 | }, 192 | }) 193 | } 194 | 195 | function parseLiteral(ast: ValueNode): any { 196 | switch (ast.kind) { 197 | case Kind.STRING: 198 | case Kind.BOOLEAN: { 199 | return ast.value 200 | } 201 | case Kind.INT: 202 | case Kind.FLOAT: { 203 | return parseFloat(ast.value) 204 | } 205 | case Kind.OBJECT: { 206 | const value = Object.create(null) 207 | ast.fields.forEach(field => { 208 | value[field.name.value] = parseLiteral(field.value) 209 | }) 210 | 211 | return value 212 | } 213 | case Kind.LIST: { 214 | return ast.values.map(parseLiteral) 215 | } 216 | default: 217 | return null 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/SchemaCache.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql' 2 | import { ApolloLink } from 'apollo-link' 3 | import { makeExecutableSchema } from 'graphql-tools' 4 | import { ExecutableSchemaFactory } from './RemoteSchemaFactory' 5 | 6 | export interface SchemaCacheOptions { 7 | link: ApolloLink 8 | typeDefs: string 9 | key: string 10 | } 11 | 12 | export interface CacheElement { 13 | introspectionSchema: GraphQLSchema 14 | factory: ExecutableSchemaFactory 15 | } 16 | 17 | export class SchemaCache { 18 | cache: { [key: string]: CacheElement } = {} 19 | 20 | makeExecutableSchema({ 21 | link, 22 | typeDefs, 23 | key, 24 | }: SchemaCacheOptions): GraphQLSchema { 25 | if (this.cache[key]) { 26 | this.cache[key].factory.setLink(link) 27 | } else { 28 | const introspectionSchema = makeExecutableSchema({ typeDefs }) 29 | const factory = new ExecutableSchemaFactory(introspectionSchema, link) 30 | 31 | this.cache[key] = { introspectionSchema, factory } 32 | } 33 | 34 | return this.cache[key].factory.getSchema() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { SchemaCache } from './SchemaCache' 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "noUnusedLocals": true, 8 | "rootDir": "./src", 9 | "outDir": "./dist", 10 | "lib": ["esnext"] 11 | }, 12 | "exclude": ["node_modules", "dist", "example"] 13 | } 14 | --------------------------------------------------------------------------------