diff --git a/.changeset/good-spies-share.md b/.changeset/good-spies-share.md new file mode 100644 index 00000000000..0195588b248 --- /dev/null +++ b/.changeset/good-spies-share.md @@ -0,0 +1,45 @@ +--- +'@apollo/server': minor +--- + +# Added ability to use a custom executor such as graphql-jit + +Apollo Server uses graphql-js behind the scenes when executing graphql queries. +If you would like to use a custom executor, such as +[graphql-jit](https://github.com/zalando-incubator/graphql-jit/tree/main), +you can swap in a `customExecutor`. This can result in This can result in improved performance. + +Note that this cannot be used when using the `gateway` option. + +```ts +import { compileQuery, isCompiledQuery } from 'graphql-jit'; +import { lru } from 'tiny-lru'; + +const executor = (cacheSize = 2014, compilerOpts = {}) => { + const cache = lru(cacheSize); + return async ({ contextValue, document, operationName, request, queryHash, schema }) => { + const prefix = operationName || 'NotParametrized'; + const cacheKey = `${prefix}-${queryHash}`; + let compiledQuery = cache.get(cacheKey); + if (!compiledQuery) { + const compilationResult = compileQuery(schema, document, operationName || undefined, compilerOpts); + if (isCompiledQuery(compilationResult)) { + compiledQuery = compilationResult; + cache.set(cacheKey, compiledQuery); + } else { + // ...is ExecutionResult + return compilationResult; + } + } + + return compiledQuery.query(undefined, contextValue, request.variables || {}); + }; +}; + +const schema = buildSubgraphSchema([{ typeDefs, resolvers }]); + +const server = new ApolloServer({ + schema, + customExecutor: executor(), +}); +``` diff --git a/docs/source/api/apollo-server.mdx b/docs/source/api/apollo-server.mdx index e6585ccdf8e..60ff065af5b 100644 --- a/docs/source/api/apollo-server.mdx +++ b/docs/source/api/apollo-server.mdx @@ -421,6 +421,27 @@ You can also manually call `stop()` in other contexts. Note that `stop()` is asy + + + + +###### `customExecutor` + +`Object` + + + + +Apollo Server uses graphql-js behind the scenes when executing graphql queries. +If you would like to use a [custom executor](/apollo-server/performance/custom-executor), such as +[graphql-jit](https://github.com/zalando-incubator/graphql-jit/tree/main), +you can swap in a `customExecutor`. + +Note that this cannot be used when using the `gateway` option. + + + + diff --git a/docs/source/config.json b/docs/source/config.json index 8e4be099ff7..fec723ea7fb 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -45,7 +45,8 @@ "Caching": "/performance/caching", "Cache Backends": "/performance/cache-backends", "Response Cache Eviction": "/performance/response-cache-eviction", - "Automatic Persisted Queries": "/performance/apq" + "Automatic Persisted Queries": "/performance/apq", + "Using a Custom Executor": "/performance/custom-executor" }, "Security": { "Authentication and Authorization": "/security/authentication", diff --git a/docs/source/performance/custom-executor.mdx b/docs/source/performance/custom-executor.mdx new file mode 100644 index 00000000000..389c9470104 --- /dev/null +++ b/docs/source/performance/custom-executor.mdx @@ -0,0 +1,46 @@ +--- +title: Using a Custom Executor +description: How to configure Apollo Server to use a custom executor +--- + +Apollo Server uses graphql-js behind the scenes when executing graphql queries. +If you would like to use a custom executor, such as +[graphql-jit](https://github.com/zalando-incubator/graphql-jit/tree/main), +you can swap in a `customExecutor`. This can result in improved performance. + +Note that this cannot be used when using the `gateway` option. + +## Example using graphql-jit + +```ts +import { compileQuery, isCompiledQuery } from 'graphql-jit'; +import { lru } from 'tiny-lru'; + +const executor = (cacheSize = 2014, compilerOpts = {}) => { + const cache = lru(cacheSize); + return async ({ contextValue, document, operationName, request, queryHash, schema }) => { + const prefix = operationName || 'NotParametrized'; + const cacheKey = `${prefix}-${queryHash}`; + let compiledQuery = cache.get(cacheKey); + if (!compiledQuery) { + const compilationResult = compileQuery(schema, document, operationName || undefined, compilerOpts); + if (isCompiledQuery(compilationResult)) { + compiledQuery = compilationResult; + cache.set(cacheKey, compiledQuery); + } else { + // ...is ExecutionResult + return compilationResult; + } + } + + return compiledQuery.query(undefined, contextValue, request.variables || {}); + }; +}; + +const schema = buildSubgraphSchema([{ typeDefs, resolvers }]); + +const server = new ApolloServer({ + schema, + customExecutor: executor(), +}); +``` diff --git a/package-lock.json b/package-lock.json index d9cbb4c6c2b..46db264833c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "eslint-plugin-import": "2.31.0", "express": "4.21.1", "graphql": "16.9.0", + "graphql-jit": "^0.8.7", "graphql-subscriptions": "2.0.0", "graphql-tag": "2.12.6", "jest": "29.7.0", @@ -69,6 +70,7 @@ "rollup": "3.29.5", "supertest": "7.0.0", "test-listen": "1.1.0", + "tiny-lru": "^11.2.11", "ts-jest": "29.2.5", "typescript": "5.4.5" }, @@ -2532,6 +2534,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, "node_modules/@graphql-codegen/cli": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-3.3.1.tgz", @@ -5114,6 +5125,51 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/ansi-colors": { "version": "4.1.3", "dev": true, @@ -8016,6 +8072,49 @@ "version": "2.1.0", "license": "MIT" }, + "node_modules/fast-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", + "dev": true, + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, + "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/fast-levenshtein": { "version": "2.0.6", "dev": true, @@ -8035,6 +8134,12 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "dev": true + }, "node_modules/fast-url-parser": { "version": "1.1.3", "dev": true, @@ -8397,6 +8502,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensequence": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/gensequence/-/gensequence-5.0.2.tgz", @@ -8680,6 +8794,32 @@ "graphql": ">=0.11 <=16" } }, + "node_modules/graphql-jit": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/graphql-jit/-/graphql-jit-0.8.7.tgz", + "integrity": "sha512-KGzCrsxQPfEiXOUIJCexWKiWF6ycjO89kAO6SdO8OWRGwYXbG0hsLuTnbFfMq0gj7d7/ib/Gh7jtst7FHZEEjw==", + "dev": true, + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "fast-json-stringify": "^5.16.1", + "generate-function": "^2.3.1", + "lodash.memoize": "^4.1.2", + "lodash.merge": "4.6.2", + "lodash.mergewith": "4.6.2" + }, + "peerDependencies": { + "graphql": ">=15" + } + }, + "node_modules/graphql-jit/node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "dev": true, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/graphql-request": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-5.1.0.tgz", @@ -9434,6 +9574,12 @@ "node": ">=8" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "dev": true + }, "node_modules/is-reference": { "version": "1.2.1", "dev": true, @@ -10407,6 +10553,15 @@ "version": "2.3.1", "license": "MIT" }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "dev": true, @@ -10686,6 +10841,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -12222,6 +12383,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "dev": true, @@ -13307,6 +13477,15 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-lru": { + "version": "11.2.11", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.2.11.tgz", + "integrity": "sha512-27BIW0dIWTYYoWNnqSmoNMKe5WIbkXsc0xaCQHd3/3xT2XMuMJrzHdrO9QBFR14emBz1Bu0dOAs2sCBBrvgPQA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/title-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", @@ -16388,6 +16567,15 @@ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true }, + "@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, "@graphql-codegen/cli": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-3.3.1.tgz", @@ -18501,6 +18689,41 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "ansi-colors": { "version": "4.1.3", "dev": true @@ -20574,6 +20797,49 @@ "fast-json-stable-stringify": { "version": "2.1.0" }, + "fast-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", + "dev": true, + "requires": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "dependencies": { + "fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + } + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "fast-levenshtein": { "version": "2.0.6", "dev": true @@ -20592,6 +20858,12 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "dev": true + }, "fast-url-parser": { "version": "1.1.3", "dev": true, @@ -20868,6 +21140,15 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, "gensequence": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/gensequence/-/gensequence-5.0.2.tgz", @@ -21045,6 +21326,29 @@ "integrity": "sha512-4Jor+LRbA7SfSaw7dfDUs2UBzvWg3cKrykfHRgKsOIvQaLuf+QOcG2t3Mx5N9GzSNJcuqMqJWz0ta5+BryEmXg==", "requires": {} }, + "graphql-jit": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/graphql-jit/-/graphql-jit-0.8.7.tgz", + "integrity": "sha512-KGzCrsxQPfEiXOUIJCexWKiWF6ycjO89kAO6SdO8OWRGwYXbG0hsLuTnbFfMq0gj7d7/ib/Gh7jtst7FHZEEjw==", + "dev": true, + "requires": { + "@graphql-typed-document-node/core": "^3.2.0", + "fast-json-stringify": "^5.16.1", + "generate-function": "^2.3.1", + "lodash.memoize": "^4.1.2", + "lodash.merge": "4.6.2", + "lodash.mergewith": "4.6.2" + }, + "dependencies": { + "@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "dev": true, + "requires": {} + } + } + }, "graphql-request": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-5.1.0.tgz", @@ -21535,6 +21839,12 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "dev": true + }, "is-reference": { "version": "1.2.1", "dev": true, @@ -22237,6 +22547,15 @@ "json-parse-even-better-errors": { "version": "2.3.1" }, + "json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, "json-schema-traverse": { "version": "0.4.1", "dev": true @@ -22440,6 +22759,12 @@ "version": "4.6.2", "dev": true }, + "lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -23490,6 +23815,12 @@ "require-directory": { "version": "2.1.1" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "dev": true @@ -24269,6 +24600,12 @@ "version": "2.3.8", "dev": true }, + "tiny-lru": { + "version": "11.2.11", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.2.11.tgz", + "integrity": "sha512-27BIW0dIWTYYoWNnqSmoNMKe5WIbkXsc0xaCQHd3/3xT2XMuMJrzHdrO9QBFR14emBz1Bu0dOAs2sCBBrvgPQA==", + "dev": true + }, "title-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", diff --git a/package.json b/package.json index 1833f15646c..fdd9d8057c6 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "eslint-plugin-import": "2.31.0", "express": "4.21.1", "graphql": "16.9.0", + "graphql-jit": "^0.8.7", "graphql-subscriptions": "2.0.0", "graphql-tag": "2.12.6", "jest": "29.7.0", @@ -97,6 +98,7 @@ "rollup": "3.29.5", "supertest": "7.0.0", "test-listen": "1.1.0", + "tiny-lru": "^11.2.11", "ts-jest": "29.2.5", "typescript": "5.4.5" }, diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 4a9c8d97c9f..f7d6e9c1a9a 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -74,6 +74,7 @@ import { UnreachableCaseError } from './utils/UnreachableCaseError.js'; import { computeCoreSchemaHash } from './utils/computeCoreSchemaHash.js'; import { isDefined } from './utils/isDefined.js'; import { SchemaManager } from './utils/schemaManager.js'; +import type { GraphQLExecutor } from './externalTypes/requestPipeline.js'; const NoIntrospection: ValidationRule = (context: ValidationContext) => ({ Field(node) { @@ -184,6 +185,7 @@ export interface ApolloServerInternals { stringifyResult: ( value: FormattedExecutionResult, ) => string | Promise; + customExecutor?: GraphQLExecutor; } function defaultLogger(): Logger { @@ -241,6 +243,13 @@ export class ApolloServer { ); } + if (config.gateway && config.customExecutor) { + throw new Error( + 'You cannot specify both config.gateway and config.customExecutor' + + 'because they are mutually exclusive.', + ); + } + const state: ServerState = config.gateway ? // ApolloServer has been initialized but we have not yet tried to load the // schema from the gateway. That will wait until `start()` or @@ -330,6 +339,7 @@ export class ApolloServer { stopOnTerminationSignals: config.stopOnTerminationSignals, gatewayExecutor: null, // set by _start + customExecutor: config.customExecutor, csrfPreventionRequestHeaders: config.csrfPrevention === true || config.csrfPrevention === undefined diff --git a/packages/server/src/__tests__/ApolloServer.test.ts b/packages/server/src/__tests__/ApolloServer.test.ts index 460a7d5cc76..9e97d851857 100644 --- a/packages/server/src/__tests__/ApolloServer.test.ts +++ b/packages/server/src/__tests__/ApolloServer.test.ts @@ -17,6 +17,9 @@ import type { GraphQLResponseBody } from '../externalTypes/graphql'; import { ApolloServerPluginCacheControlDisabled } from '../plugin/disabled/index.js'; import { ApolloServerPluginUsageReporting } from '../plugin/usageReporting/index.js'; import { mockLogger } from './mockLogger.js'; +import { GraphQLExecutor } from '../externalTypes/requestPipeline'; +import { compileQuery, isCompiledQuery } from 'graphql-jit'; +import { lru } from 'tiny-lru'; const typeDefs = gql` type Query { @@ -109,6 +112,27 @@ describe('ApolloServer construction', () => { }).toThrow(); }); + it('throws when both a Gateway and a customExecutor are provided', () => { + const schema = makeExecutableSchema({ typeDefs, resolvers }); + const gateway: GatewayInterface = { + async load() { + return { schema, executor: null }; + }, + async stop() {}, + onSchemaLoadOrUpdate() { + return () => {}; + }, + }; + + const customExecutor: GraphQLExecutor = async () => { + return {}; + }; + + expect(() => new ApolloServer({ gateway, customExecutor })).toThrowError( + 'You cannot specify both config.gateway and config.customExecutor', + ); + }); + it('TypeScript enforces schema-related option combinations', async () => { const schema = makeExecutableSchema({ typeDefs, resolvers }); const gateway: GatewayInterface = { @@ -509,6 +533,60 @@ describe('ApolloServer executeOperation', () => { await server.stop(); }); + it('is able to use a custom executor', async () => { + const executor = ( + schema: GraphQLSchema, + cacheSize = 2014, + compilerOpts = {}, + ) => { + const cache = lru(cacheSize); + return async ({ + contextValue, + document, + operationName, + request, + queryHash, + }: any) => { + const prefix = operationName || 'NotParametrized'; + const cacheKey = `${prefix}-${queryHash}`; + let compiledQuery = cache.get(cacheKey); + if (!compiledQuery) { + const compilationResult = compileQuery( + schema, + document, + operationName || undefined, + compilerOpts, + ); + if (isCompiledQuery(compilationResult)) { + compiledQuery = compilationResult; + cache.set(cacheKey, compiledQuery); + } else { + // ...is ExecutionResult + return compilationResult; + } + } + + return compiledQuery.query( + undefined, + contextValue, + request.variables || {}, + ); + }; + }; + const schema = makeExecutableSchema({ typeDefs, resolvers }); + const server = new ApolloServer({ + schema, + customExecutor: executor(schema), + }); + await server.start(); + + const { body } = await server.executeOperation({ query: '{ hello }' }); + const result = singleResult(body); + expect(result.errors).toBeUndefined(); + expect(result.data?.hello).toBe('world'); + await server.stop(); + }); + it('parse errors', async () => { const server = new ApolloServer({ typeDefs, diff --git a/packages/server/src/externalTypes/constructor.ts b/packages/server/src/externalTypes/constructor.ts index 6d6f354f655..e1bf4cb140b 100644 --- a/packages/server/src/externalTypes/constructor.ts +++ b/packages/server/src/externalTypes/constructor.ts @@ -20,6 +20,7 @@ import type { GatewayInterface } from '@apollo/server-gateway-interface'; import type { ApolloServerPlugin } from './plugins.js'; import type { BaseContext } from './index.js'; import type { GraphQLExperimentalIncrementalExecutionResults } from '../incrementalDeliveryPolyfill.js'; +import type { GraphQLExecutor } from './requestPipeline.js'; export type DocumentStore = KeyValueCache; @@ -116,6 +117,8 @@ interface ApolloServerOptionsBase { // consider undoing the workaround). status400ForVariableCoercionErrors?: boolean; + customExecutor?: GraphQLExecutor; + // For testing only. __testing_incrementalExecutionResults?: GraphQLExperimentalIncrementalExecutionResults; } diff --git a/packages/server/src/externalTypes/requestPipeline.ts b/packages/server/src/externalTypes/requestPipeline.ts index 75445cc7de2..175e1ce1a6d 100644 --- a/packages/server/src/externalTypes/requestPipeline.ts +++ b/packages/server/src/externalTypes/requestPipeline.ts @@ -14,6 +14,7 @@ import type { Logger } from '@apollo/utils.logger'; import type { KeyValueCache } from '@apollo/utils.keyvaluecache'; import type { DocumentNode, + ExecutionResult, GraphQLError, GraphQLSchema, OperationDefinitionNode, @@ -121,3 +122,9 @@ export type GraphQLRequestContextDidEncounterSubsequentErrors< export type GraphQLRequestContextWillSendSubsequentPayload< TContext extends BaseContext, > = GraphQLRequestContextWillSendResponse; + +export type GraphQLExecutor< + TContext extends BaseContext = Record, +> = ( + requestContext: GraphQLRequestContextExecutionDidStart, +) => Promise; diff --git a/packages/server/src/requestPipeline.ts b/packages/server/src/requestPipeline.ts index b9b4b5542dc..9eb55cff84a 100644 --- a/packages/server/src/requestPipeline.ts +++ b/packages/server/src/requestPipeline.ts @@ -539,6 +539,9 @@ export async function processGraphQLRequest( makeGatewayGraphQLRequestContext(requestContext, server, internals), ); return { singleResult: result }; + } else if (internals.customExecutor) { + const result = await internals.customExecutor(requestContext); + return { singleResult: result }; } else { const resultOrResults = await executeIncrementally({ schema: schemaDerivedData.schema,