Repository: apollographql/apollo-server
Pull request #7634 (2023-07-08 14:53:16)

Issue 4359 pretty print response
This is a small Pull Request that addresses Issue #4359.

It adds a new function `stringifyResults(FormattedExecutionResult) => string` to the ApolloServerOptionsBase and ApolloServerInternals. This allows consumers to pass in a function that manipulates the FormattedExecutionResult into a string of their preferred format and liking. This function is used within the `runHttpQuery` function. If the stringifyResult attribute is present, we will pass the FormattedExecutionResult to the supplied function, and its returned value will be the string portion of the HTTPGraphQLResponse.body. If the stringifyResult function is *not* provided, we fall back to the default behavior of `prettyJSONStringify(orderExecutionResultFields(graphQLResponse.body.singleResult)`.


apollo-cla: @dfperry5: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy request for *apollo-server-docs* pending review.
[Visit the deploys page to approve it](https://app.netlify.com/sites/apollo-server-docs/deploys)

|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 271a07b780bc93dc78567d6c94bc72a562a3c1f0 |
codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/apollographql/apollo-server/pr/7634/builds/396206) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 271a07b780bc93dc78567d6c94bc72a562a3c1f0:

|Sandbox| Source |
|--|--|
|[Apollo Server Typescript](https://codesandbox.io/s/apollo-server-typescript-56sx9d)| Configuration |
|[Apollo Server](https://codesandbox.io/s/apollo-server-s4mkls)| Configuration |

dfperry5: Apologies for all the clean up commits - but, I do have a question:

Should I be running `npm run changeset-publish`? I couldn't find a reference to it in the contributing.md file.
trevor-scheer: No worries on the commits, they'll all be squashed into one when we land the PR.

Patch:
diff --git a/.changeset/pink-jobs-accept.md b/.changeset/pink-jobs-accept.md
new file mode 100644
index 000000000..28fdaf855
--- /dev/null
+++ b/.changeset/pink-jobs-accept.md
@@ -0,0 +1,18 @@
+---
+'@apollo/server-integration-testsuite': minor
+'@apollo/server': minor
+---
+
+Updating the ApolloServer constructor to take in a stringifyResult function that will allow a consumer to pass in a function that formats the result of an http query.
+
+Usage:
+```ts
+const server = new ApolloServer({
+  typeDefs,
+  resolvers,
+  stringifyResult: (value: FormattedExecutionResult) => {
+    return JSON.stringify(value, null, 2);
+  },
+});
+```
+
diff --git a/.changeset/tough-gorillas-visit.md b/.changeset/tough-gorillas-visit.md
new file mode 100644
index 000000000..7105d60d3
--- /dev/null
+++ b/.changeset/tough-gorillas-visit.md
@@ -0,0 +1,5 @@
+---
+'@apollo/server-integration-testsuite': patch
+---
+
+Update graphql-http dependency
diff --git a/package-lock.json b/package-lock.json
index 10d986662..7a7d9edd0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -65,7 +65,7 @@
         "lodash.sumby": "4.6.0",
         "nock": "13.3.1",
         "node-fetch": "2.6.12",
-        "prettier": "2.8.8",
+        "prettier": "3.0.0",
         "qs-middleware": "1.0.3",
         "requisition": "1.7.0",
         "rollup": "3.26.2",
@@ -1605,6 +1605,21 @@
         "semver": "^7.5.3"
       }
     },
+    "node_modules/@changesets/apply-release-plan/node_modules/prettier": {
+      "version": "2.8.8",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+      "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin-prettier.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
     "node_modules/@changesets/assemble-release-plan": {
       "version": "5.2.4",
       "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-5.2.4.tgz",
@@ -2132,6 +2147,21 @@
         "prettier": "^2.7.1"
       }
     },
+    "node_modules/@changesets/write/node_modules/prettier": {
+      "version": "2.8.8",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+      "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin-prettier.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
     "node_modules/@cspell/cspell-bundled-dicts": {
       "version": "6.31.1",
       "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-6.31.1.tgz",
@@ -8495,9 +8525,9 @@
       }
     },
     "node_modules/graphql-http": {
-      "version": "1.19.0",
-      "resolved": "https://registry.npmjs.org/graphql-http/-/graphql-http-1.19.0.tgz",
-      "integrity": "sha512-fOD3hfp/G+Lhx2FWW5HsfmtJSsw6CikcpOboG7/mFo/pPUzn3yOwKdTFRnJ8MVY4ru69MT1nSPr/1gI/iuGNlw==",
+      "version": "1.20.0",
+      "resolved": "https://registry.npmjs.org/graphql-http/-/graphql-http-1.20.0.tgz",
+      "integrity": "sha512-XMTieJkI8NRugwuTjWlT8OQAYH7+DCIuworslawJwRwraq4laJwvWkngYDVBIrDHPhR2sgdL/Bh8Kw5IwDDeRg==",
       "engines": {
         "node": ">=12"
       },
@@ -11709,15 +11739,15 @@
       }
     },
     "node_modules/prettier": {
-      "version": "2.8.8",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
-      "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
+      "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
       "dev": true,
       "bin": {
-        "prettier": "bin-prettier.js"
+        "prettier": "bin/prettier.cjs"
       },
       "engines": {
-        "node": ">=10.13.0"
+        "node": ">=14"
       },
       "funding": {
         "url": "https://github.com/prettier/prettier?sponsor=1"
@@ -14209,7 +14239,7 @@
         "@josephg/resolvable": "^1.0.1",
         "body-parser": "^1.20.0",
         "express": "^4.18.1",
-        "graphql-http": "1.19.0",
+        "graphql-http": "1.20.0",
         "graphql-tag": "^2.12.6",
         "loglevel": "^1.8.0",
         "node-fetch": "^2.6.7",
@@ -14542,7 +14572,7 @@
         "@josephg/resolvable": "^1.0.1",
         "body-parser": "^1.20.0",
         "express": "^4.18.1",
-        "graphql-http": "1.19.0",
+        "graphql-http": "1.20.0",
         "graphql-tag": "^2.12.6",
         "loglevel": "^1.8.0",
         "node-fetch": "^2.6.7",
@@ -15449,6 +15479,14 @@
         "prettier": "^2.7.1",
         "resolve-from": "^5.0.0",
         "semver": "^7.5.3"
+      },
+      "dependencies": {
+        "prettier": {
+          "version": "2.8.8",
+          "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+          "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+          "dev": true
+        }
       }
     },
     "@changesets/assemble-release-plan": {
@@ -15926,6 +15964,14 @@
         "fs-extra": "^7.0.1",
         "human-id": "^1.0.2",
         "prettier": "^2.7.1"
+      },
+      "dependencies": {
+        "prettier": {
+          "version": "2.8.8",
+          "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+          "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+          "dev": true
+        }
       }
     },
     "@cspell/cspell-bundled-dicts": {
@@ -20868,9 +20914,9 @@
       }
     },
     "graphql-http": {
-      "version": "1.19.0",
-      "resolved": "https://registry.npmjs.org/graphql-http/-/graphql-http-1.19.0.tgz",
-      "integrity": "sha512-fOD3hfp/G+Lhx2FWW5HsfmtJSsw6CikcpOboG7/mFo/pPUzn3yOwKdTFRnJ8MVY4ru69MT1nSPr/1gI/iuGNlw==",
+      "version": "1.20.0",
+      "resolved": "https://registry.npmjs.org/graphql-http/-/graphql-http-1.20.0.tgz",
+      "integrity": "sha512-XMTieJkI8NRugwuTjWlT8OQAYH7+DCIuworslawJwRwraq4laJwvWkngYDVBIrDHPhR2sgdL/Bh8Kw5IwDDeRg==",
       "requires": {}
     },
     "graphql-request": {
@@ -23103,9 +23149,9 @@
       "dev": true
     },
     "prettier": {
-      "version": "2.8.8",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
-      "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
+      "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
       "dev": true
     },
     "pretty-format": {
diff --git a/package.json b/package.json
index a1debbd36..856aa28a5 100644
--- a/package.json
+++ b/package.json
@@ -93,7 +93,7 @@
     "lodash.sumby": "4.6.0",
     "nock": "13.3.1",
     "node-fetch": "2.6.12",
-    "prettier": "2.8.8",
+    "prettier": "3.0.0",
     "qs-middleware": "1.0.3",
     "requisition": "1.7.0",
     "rollup": "3.26.2",
diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts
index 7daeb7078..62b15fea9 100644
--- a/packages/server/src/ApolloServer.ts
+++ b/packages/server/src/ApolloServer.ts
@@ -15,6 +15,7 @@ import {
   type TypedQueryDocumentNode,
   type ValidationContext,
   type ValidationRule,
+  type FormattedExecutionResult,
 } from 'graphql';
 import {
   type KeyValueCache,
@@ -179,6 +180,7 @@ export interface ApolloServerInternals<TContext extends BaseContext> {
   // flip default behavior.
   status400ForVariableCoercionErrors?: boolean;
   __testing_incrementalExecutionResults?: GraphQLExperimentalIncrementalExecutionResults;
+  stringifyResult: (value: FormattedExecutionResult) => string;
 }
 
 function defaultLogger(): Logger {
@@ -332,6 +334,7 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
         config.status400ForVariableCoercionErrors ?? false,
       __testing_incrementalExecutionResults:
         config.__testing_incrementalExecutionResults,
+      stringifyResult: config.stringifyResult ?? prettyJSONStringify,
     };
   }
 
@@ -499,8 +502,8 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
 
       try {
         await Promise.all(
-          this.internals.plugins.map(async (plugin) =>
-            plugin.startupDidFail?.({ error }),
+          this.internals.plugins.map(
+            async (plugin) => plugin.startupDidFail?.({ error }),
           ),
         );
       } catch (pluginError) {
@@ -1054,10 +1057,11 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
         const error = ensureError(maybeError);
         try {
           await Promise.all(
-            this.internals.plugins.map(async (plugin) =>
-              plugin.contextCreationDidFail?.({
-                error,
-              }),
+            this.internals.plugins.map(
+              async (plugin) =>
+                plugin.contextCreationDidFail?.({
+                  error,
+                }),
             ),
           );
         } catch (pluginError) {
@@ -1090,8 +1094,9 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {
       ) {
         try {
           await Promise.all(
-            this.internals.plugins.map(async (plugin) =>
-              plugin.invalidRequestWasReceived?.({ error: maybeError }),
+            this.internals.plugins.map(
+              async (plugin) =>
+                plugin.invalidRequestWasReceived?.({ error: maybeError }),
             ),
           );
         } catch (pluginError) {
@@ -1313,11 +1318,12 @@ export async function internalExecuteOperation<TContext extends BaseContext>(
     // If *these* hooks throw then we'll still get a 500 but won't mask its
     // error.
     await Promise.all(
-      internals.plugins.map(async (plugin) =>
-        plugin.unexpectedErrorProcessingRequest?.({
-          requestContext,
-          error,
-        }),
+      internals.plugins.map(
+        async (plugin) =>
+          plugin.unexpectedErrorProcessingRequest?.({
+            requestContext,
+            error,
+          }),
       ),
     );
     // Mask unexpected error externally.
diff --git a/packages/server/src/externalTypes/constructor.ts b/packages/server/src/externalTypes/constructor.ts
index 7ab4addf8..ffd1cf6f7 100644
--- a/packages/server/src/externalTypes/constructor.ts
+++ b/packages/server/src/externalTypes/constructor.ts
@@ -8,6 +8,7 @@ import type { Logger } from '@apollo/utils.logger';
 import type { IExecutableSchemaDefinition } from '@graphql-tools/schema';
 import type {
   DocumentNode,
+  FormattedExecutionResult,
   GraphQLFieldResolver,
   GraphQLFormattedError,
   GraphQLSchema,
@@ -88,7 +89,7 @@ interface ApolloServerOptionsBase<TContext extends BaseContext> {
   includeStacktraceInErrorResponses?: boolean;
   logger?: Logger;
   allowBatchedHttpRequests?: boolean;
-
+  stringifyResult?: (value: FormattedExecutionResult) => string;
   introspection?: boolean;
   plugins?: ApolloServerPlugin<TContext>[];
   persistedQueries?: PersistedQueryOptions | false;
diff --git a/packages/server/src/requestPipeline.ts b/packages/server/src/requestPipeline.ts
index 58fbf9e6b..339879740 100644
--- a/packages/server/src/requestPipeline.ts
+++ b/packages/server/src/requestPipeline.ts
@@ -188,10 +188,11 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
   // syntactically correct, so this string should not be trusted as a valid
   // document until after it's parsed and validated.
   await Promise.all(
-    requestListeners.map((l) =>
-      l.didResolveSource?.(
-        requestContext as GraphQLRequestContextDidResolveSource<TContext>,
-      ),
+    requestListeners.map(
+      (l) =>
+        l.didResolveSource?.(
+          requestContext as GraphQLRequestContextDidResolveSource<TContext>,
+        ),
     ),
   );
 
@@ -320,10 +321,11 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
 
   try {
     await Promise.all(
-      requestListeners.map((l) =>
-        l.didResolveOperation?.(
-          requestContext as GraphQLRequestContextDidResolveOperation<TContext>,
-        ),
+      requestListeners.map(
+        (l) =>
+          l.didResolveOperation?.(
+            requestContext as GraphQLRequestContextDidResolveOperation<TContext>,
+          ),
       ),
     );
   } catch (err: unknown) {
@@ -372,10 +374,11 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
   } else {
     const executionListeners = (
       await Promise.all(
-        requestListeners.map((l) =>
-          l.executionDidStart?.(
-            requestContext as GraphQLRequestContextExecutionDidStart<TContext>,
-          ),
+        requestListeners.map(
+          (l) =>
+            l.executionDidStart?.(
+              requestContext as GraphQLRequestContextExecutionDidStart<TContext>,
+            ),
         ),
       )
     ).filter(isDefined);
@@ -387,8 +390,9 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
       // symbol so it can be invoked by our `wrapField` method during execution.
       const invokeWillResolveField: GraphQLRequestExecutionListener<TContext>['willResolveField'] =
         (...args) =>
-          invokeSyncDidStartHook(executionListeners, (l) =>
-            l.willResolveField?.(...args),
+          invokeSyncDidStartHook(
+            executionListeners,
+            (l) => l.willResolveField?.(...args),
           );
 
       Object.defineProperty(
@@ -577,11 +581,12 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
                   const { errors } = incrementalResult;
                   if (errors) {
                     await Promise.all(
-                      requestListeners.map((l) =>
-                        l.didEncounterSubsequentErrors?.(
-                          requestContext as GraphQLRequestContextDidEncounterSubsequentErrors<TContext>,
-                          errors,
-                        ),
+                      requestListeners.map(
+                        (l) =>
+                          l.didEncounterSubsequentErrors?.(
+                            requestContext as GraphQLRequestContextDidEncounterSubsequentErrors<TContext>,
+                            errors,
+                          ),
                       ),
                     );
 
@@ -601,11 +606,12 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
 
       // Invoke hook, which is allowed to mutate payload if it really wants to.
       await Promise.all(
-        requestListeners.map((l) =>
-          l.willSendSubsequentPayload?.(
-            requestContext as GraphQLRequestContextWillSendSubsequentPayload<TContext>,
-            payload,
-          ),
+        requestListeners.map(
+          (l) =>
+            l.willSendSubsequentPayload?.(
+              requestContext as GraphQLRequestContextWillSendSubsequentPayload<TContext>,
+              payload,
+            ),
         ),
       );
 
@@ -615,10 +621,11 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
 
   async function invokeWillSendResponse() {
     await Promise.all(
-      requestListeners.map((l) =>
-        l.willSendResponse?.(
-          requestContext as GraphQLRequestContextWillSendResponse<TContext>,
-        ),
+      requestListeners.map(
+        (l) =>
+          l.willSendResponse?.(
+            requestContext as GraphQLRequestContextWillSendResponse<TContext>,
+          ),
       ),
     );
   }
@@ -629,10 +636,11 @@ export async function processGraphQLRequest<TContext extends BaseContext>(
     requestContext.errors = errors;
 
     return await Promise.all(
-      requestListeners.map((l) =>
-        l.didEncounterErrors?.(
-          requestContext as GraphQLRequestContextDidEncounterErrors<TContext>,
-        ),
+      requestListeners.map(
+        (l) =>
+          l.didEncounterErrors?.(
+            requestContext as GraphQLRequestContextDidEncounterErrors<TContext>,
+          ),
       ),
     );
   }
diff --git a/packages/server/src/runHttpQuery.ts b/packages/server/src/runHttpQuery.ts
index b47684c3c..a22df8aa2 100644
--- a/packages/server/src/runHttpQuery.ts
+++ b/packages/server/src/runHttpQuery.ts
@@ -260,7 +260,7 @@ export async function runHttpQuery<TContext extends BaseContext>({
       ...graphQLResponse.http,
       body: {
         kind: 'complete',
-        string: prettyJSONStringify(
+        string: internals.stringifyResult(
           orderExecutionResultFields(graphQLResponse.body.singleResult),
         ),
       },
diff --git a/packages/server/src/standalone/index.ts b/packages/server/src/standalone/index.ts
index c3c9fc4ca..b1837dadc 100644
--- a/packages/server/src/standalone/index.ts
+++ b/packages/server/src/standalone/index.ts
@@ -55,7 +55,7 @@ export async function startStandaloneServer<TContext extends BaseContext>(
 
   await server.start();
 
-  const context = options?.context ?? (async () => ({} as TContext));
+  const context = options?.context ?? (async () => ({}) as TContext);
   app.use(
     cors(),
     bodyParser.json({ limit: '50mb' }),


Test patch:
diff --git a/packages/integration-testsuite/package.json b/packages/integration-testsuite/package.json
index c0ffa13e6..c47d463e7 100644
--- a/packages/integration-testsuite/package.json
+++ b/packages/integration-testsuite/package.json
@@ -35,7 +35,7 @@
     "@josephg/resolvable": "^1.0.1",
     "body-parser": "^1.20.0",
     "express": "^4.18.1",
-    "graphql-http": "1.19.0",
+    "graphql-http": "1.20.0",
     "graphql-tag": "^2.12.6",
     "loglevel": "^1.8.0",
     "node-fetch": "^2.6.7",
diff --git a/packages/server/src/__tests__/ApolloServer.test.ts b/packages/server/src/__tests__/ApolloServer.test.ts
index 0656e9026..d48a0cf0e 100644
--- a/packages/server/src/__tests__/ApolloServer.test.ts
+++ b/packages/server/src/__tests__/ApolloServer.test.ts
@@ -141,6 +141,42 @@ describe('ApolloServer construction', () => {
     // @ts-expect-error
     takesConfig({ modules: [] });
   });
+  describe('with configuration options', () => {
+    it('stringifyResult', async () => {
+      const server = new ApolloServer({
+        typeDefs,
+        resolvers,
+        stringifyResult: (value: FormattedExecutionResult) => {
+          let result = JSON.stringify(value, null, 10000);
+          result = result.replace('world', 'stringifyResults works!'); // replace text with something custom
+          return result;
+        },
+      });
+
+      await server.start();
+
+      const request = {
+        httpGraphQLRequest: {
+          method: 'POST',
+          headers: new HeaderMap([['content-type', 'application-json']]),
+          body: { query: '{ hello }' },
+          search: '',
+        },
+        context: async () => ({}),
+      };
+
+      const { body } = await server.executeHTTPGraphQLRequest(request);
+      assert(body.kind === 'complete');
+      expect(body.string).toMatchInlineSnapshot(`
+      "{
+                "data": {
+                          "hello": "stringifyResults works!"
+                }
+      }"
+      `);
+      await server.stop();
+    });
+  });
 });
 
 const failToStartPlugin: ApolloServerPlugin<BaseContext> = {