diff --git a/packages/gatsby/src/bootstrap/__tests__/graphql-runner.js b/packages/gatsby/src/bootstrap/__tests__/graphql-runner.js index 48caf00e239d1..e9dba17611223 100644 --- a/packages/gatsby/src/bootstrap/__tests__/graphql-runner.js +++ b/packages/gatsby/src/bootstrap/__tests__/graphql-runner.js @@ -1,7 +1,12 @@ jest.mock(`graphql`) const createGraphqlRunner = require(`../graphql-runner`) -const { graphql } = require(`graphql`) +const { execute, validate, parse } = require(`graphql`) + +parse.mockImplementation(() => { + return {} +}) +validate.mockImplementation(() => []) const createStore = (schema = {}) => { return { @@ -33,9 +38,9 @@ describe(`grapqhl-runner`, () => { gatsby: `is awesome`, }, } - graphql.mockImplementation(() => Promise.resolve(expectation)) + execute.mockImplementation(() => Promise.resolve(expectation)) - const result = await graphqlRunner({}, {}) + const result = await graphqlRunner(``, {}) expect(reporter.panicOnBuild).not.toHaveBeenCalled() expect(result).toBe(expectation) }) @@ -51,9 +56,9 @@ describe(`grapqhl-runner`, () => { }, ], } - graphql.mockImplementation(() => Promise.resolve(expectation)) + execute.mockImplementation(() => Promise.resolve(expectation)) - const result = await graphqlRunner({}, {}) + const result = await graphqlRunner(``, {}) expect(reporter.panicOnBuild).not.toHaveBeenCalled() expect(result).toBe(expectation) }) @@ -68,13 +73,13 @@ describe(`grapqhl-runner`, () => { message: `Cannot query field boyhowdy on RootQueryType`, } - graphql.mockImplementation(() => + execute.mockImplementation(() => Promise.resolve({ errors: [errorObject], }) ) - await graphqlRunner({}, {}) + await graphqlRunner(``, {}) expect(reporter.panicOnBuild).toHaveBeenCalled() expect(reporter.panicOnBuild).toMatchSnapshot() }) diff --git a/packages/gatsby/src/query/graphql-runner.js b/packages/gatsby/src/query/graphql-runner.js index cc1f55741b289..d46b8c383c7bb 100644 --- a/packages/gatsby/src/query/graphql-runner.js +++ b/packages/gatsby/src/query/graphql-runner.js @@ -1,4 +1,5 @@ -const { graphql } = require(`graphql`) +const { parse, validate, execute } = require(`graphql`) +const { debounce } = require(`lodash`) const withResolverContext = require(`../schema/context`) const { LocalNodeModel } = require(`../schema/node-model`) @@ -16,24 +17,68 @@ class GraphQLRunner { schemaComposer: schemaCustomization.composer, createPageDependency, }) + this.schema = schema + this.parseCache = new Map() + this.validDocuments = new WeakSet() + this.scheduleClearCache = debounce(this.clearCache.bind(this), 5000) + } + + clearCache() { + this.parseCache.clear() + this.validDocuments = new WeakSet() + } + + parse(query) { + if (!this.parseCache.has(query)) { + this.parseCache.set(query, parse(query)) + } + return this.parseCache.get(query) + } + + validate(schema, document) { + if (!this.validDocuments.has(document)) { + const errors = validate(schema, document) + if (!errors.length) { + this.validDocuments.add(document) + } + return errors + } + return [] } query(query, context) { const { schema, schemaCustomization } = this.store.getState() - return graphql( - schema, - query, - context, - withResolverContext({ - schema, - schemaComposer: schemaCustomization.composer, - context, - customContext: schemaCustomization.context, - nodeModel: this.nodeModel, - }), - context - ) + if (this.schema !== schema) { + this.schema = schema + this.clearCache() + } + + const document = this.parse(query) + const errors = this.validate(schema, document) + + const result = + errors.length > 0 + ? { errors } + : execute({ + schema, + document, + rootValue: context, + contextValue: withResolverContext({ + schema, + schemaComposer: schemaCustomization.composer, + context, + customContext: schemaCustomization.context, + nodeModel: this.nodeModel, + }), + variableValues: context, + }) + + // Queries are usually executed in batch. But after the batch is finished + // cache just wastes memory without much benefits. + // TODO: consider a better strategy for cache purging/invalidation + this.scheduleClearCache() + return result } }