From 7156621b54655e675ff03b6f8944d0526a9a0a54 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Tue, 16 Jan 2024 12:05:15 +0100 Subject: [PATCH 1/3] fix: do not report operations that don't pass GraphQL validation --- .changeset/eight-swans-teach.md | 5 + packages/libraries/client/package.json | 1 + packages/libraries/client/src/yoga.ts | 44 ++++---- packages/libraries/client/tests/yoga.spec.ts | 103 +++++++++++++++++++ packages/services/server/package.json | 2 +- pnpm-lock.yaml | 24 ++++- 6 files changed, 153 insertions(+), 26 deletions(-) create mode 100644 .changeset/eight-swans-teach.md diff --git a/.changeset/eight-swans-teach.md b/.changeset/eight-swans-teach.md new file mode 100644 index 0000000000..81d0d471e6 --- /dev/null +++ b/.changeset/eight-swans-teach.md @@ -0,0 +1,5 @@ +--- +"@graphql-hive/client": patch +--- + +Do not report operations that do not pass GraphQL validation. diff --git a/packages/libraries/client/package.json b/packages/libraries/client/package.json index 769e78d7e0..dcef912e0a 100644 --- a/packages/libraries/client/package.json +++ b/packages/libraries/client/package.json @@ -61,6 +61,7 @@ "@apollo/server": "4.10.0", "@apollo/subgraph": "2.6.3", "@envelop/types": "5.0.0", + "@graphql-yoga/plugin-disable-introspection": "2.1.1", "@types/async-retry": "1.4.8", "graphql": "16.8.1", "graphql-yoga": "5.1.1", diff --git a/packages/libraries/client/src/yoga.ts b/packages/libraries/client/src/yoga.ts index 737776004d..3fe6881b6f 100644 --- a/packages/libraries/client/src/yoga.ts +++ b/packages/libraries/client/src/yoga.ts @@ -72,6 +72,7 @@ export function useHive(clientOrOptions: HiveClient | HivePluginOptions): Plugin return; } + // Report if execution happened (aka executionArgs have been set within onExecute) if (record.executionArgs) { record.callback( { @@ -83,27 +84,30 @@ export function useHive(clientOrOptions: HiveClient | HivePluginOptions): Plugin return; } - if (!record.paramsArgs.query || !latestSchema) { - return; - } - - try { - let document = parsedDocumentCache.get(record.paramsArgs.query); - if (document === undefined) { - document = parse(record.paramsArgs.query); - parsedDocumentCache.set(record.paramsArgs.query, document); + // Report if execution was skipped due to response cache ( Symbol.for('servedFromResponseCache') in context.result) + if ( + record.paramsArgs.query && + latestSchema && + Symbol.for('servedFromResponseCache') in context.result + ) { + try { + let document = parsedDocumentCache.get(record.paramsArgs.query); + if (document === undefined) { + document = parse(record.paramsArgs.query); + parsedDocumentCache.set(record.paramsArgs.query, document); + } + record.callback( + { + document, + schema: latestSchema, + variableValues: record.paramsArgs.variables, + operationName: record.paramsArgs.operationName, + }, + context.result, + ); + } catch (err) { + console.error(err); } - record.callback( - { - document, - schema: latestSchema, - variableValues: record.paramsArgs.variables, - operationName: record.paramsArgs.operationName, - }, - context.result, - ); - } catch { - // ignore } }, }; diff --git a/packages/libraries/client/tests/yoga.spec.ts b/packages/libraries/client/tests/yoga.spec.ts index 2d43f57c0a..b88873a236 100644 --- a/packages/libraries/client/tests/yoga.spec.ts +++ b/packages/libraries/client/tests/yoga.spec.ts @@ -4,6 +4,8 @@ import { createSchema, createYoga } from 'graphql-yoga'; // eslint-disable-next-line import/no-extraneous-dependencies import nock from 'nock'; // eslint-disable-next-line import/no-extraneous-dependencies +import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection'; +// eslint-disable-next-line import/no-extraneous-dependencies import { useResponseCache } from '@graphql-yoga/plugin-response-cache'; import { useHive } from '../src/yoga.js'; @@ -225,3 +227,104 @@ it('reports usage with response cache', async () => { }, 1000); }); }); + +it('does not report usage for operation that does not pass validation', async () => { + const callback = vi.fn(); + const graphqlScope = nock('http://localhost') + .post('/graphql') + .reply(200, { + data: { + __typename: 'Query', + tokenInfo: { + __typename: 'TokenInfo', + token: { + name: 'brrrt', + }, + organization: { + name: 'mom', + cleanId: 'ur-mom', + }, + project: { + name: 'projecto', + type: 'FEDERATION', + cleanId: 'projecto', + }, + target: { + name: 'projecto', + cleanId: 'projecto', + }, + canReportSchema: true, + canCollectUsage: true, + canReadOperations: true, + }, + }, + }); + + const yoga = createYoga({ + schema: createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hi: String + } + `, + }), + plugins: [ + useDisableIntrospection(), + useHive({ + enabled: true, + debug: true, + token: 'brrrt', + selfHosting: { + applicationUrl: 'http://localhost/foo', + graphqlEndpoint: 'http://localhost/graphql', + usageEndpoint: 'http://localhost/usage', + }, + usage: { + endpoint: 'http://localhost/usage', + clientInfo() { + return { + name: 'brrr', + version: '1', + }; + }, + }, + agent: { + maxSize: 1, + }, + }), + ], + }); + + // eslint-disable-next-line no-async-promise-executor + await new Promise(async (resolve, reject) => { + nock.emitter.once('no match', (req: any) => { + reject(new Error(`Unexpected request was sent to ${req.path}`)); + }); + + const res = await yoga.fetch('http://localhost/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: /* GraphQL */ ` + { + __schema { + types { + name + } + } + } + `, + }), + }); + expect(res.status).toBe(200); + expect(await res.text()).toContain('GraphQL introspection has been disabled'); + + setTimeout(() => { + graphqlScope.done(); + expect(callback).not.toHaveBeenCalled(); + resolve(); + }, 1000); + }); +}); diff --git a/packages/services/server/package.json b/packages/services/server/package.json index 4b17036a29..0fa6f087c8 100644 --- a/packages/services/server/package.json +++ b/packages/services/server/package.json @@ -22,7 +22,7 @@ "@escape.tech/graphql-armor-max-tokens": "2.3.0", "@graphql-hive/client": "workspace:*", "@graphql-yoga/plugin-persisted-operations": "3.1.1", - "@graphql-yoga/plugin-response-cache": "3.2.1", + "@graphql-yoga/plugin-response-cache": "3.3.0-alpha-20240116103344-655e7299", "@hive/api": "workspace:*", "@hive/cdn-script": "workspace:*", "@hive/service-common": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e112a0de4..7da5fd7a12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -434,6 +434,9 @@ importers: '@apollo/subgraph': specifier: 2.6.3 version: 2.6.3(graphql@16.8.1) + '@graphql-yoga/plugin-disable-introspection': + specifier: 2.1.1 + version: 2.1.1(graphql-yoga@5.1.1)(graphql@16.8.1) '@types/async-retry': specifier: 1.4.8 version: 1.4.8 @@ -1049,8 +1052,8 @@ importers: specifier: 3.1.1 version: 3.1.1(@graphql-tools/utils@10.0.12)(graphql-yoga@5.1.1)(graphql@16.8.1) '@graphql-yoga/plugin-response-cache': - specifier: 3.2.1 - version: 3.2.1(@envelop/core@5.0.0)(graphql-yoga@5.1.1)(graphql@16.8.1) + specifier: 3.3.0-alpha-20240116103344-655e7299 + version: 3.3.0-alpha-20240116103344-655e7299(@envelop/core@5.0.0)(graphql-yoga@5.1.1)(graphql@16.8.1) '@hive/api': specifier: workspace:* version: link:../api @@ -7454,6 +7457,17 @@ packages: tslib: 2.6.2 dev: true + /@graphql-yoga/plugin-disable-introspection@2.1.1(graphql-yoga@5.1.1)(graphql@16.8.1): + resolution: {integrity: sha512-vFeYDegjeCMa4agvKE/+1waINUBmckBTozpmRLSvElDlb18e18CWj1jm18hTDl6jV9R5GS4y5VMdpbFTukyh/w==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^15.2.0 || ^16.0.0 + graphql-yoga: ^5.1.1 + dependencies: + graphql: 16.8.1 + graphql-yoga: 5.1.1(graphql@16.8.1) + dev: true + /@graphql-yoga/plugin-persisted-operations@3.1.1(@graphql-tools/utils@10.0.12)(graphql-yoga@5.1.1)(graphql@16.8.1): resolution: {integrity: sha512-cD7+V19ipICnCZsUtLgOyZV9jcwtTiSFJOGYIZnUG25egUIzXfMmg+EWUZD1uGLKZBKbGWULn9ayc7UW52GSLg==} engines: {node: '>=18.0.0'} @@ -7467,11 +7481,11 @@ packages: graphql-yoga: 5.1.1(graphql@16.8.1) dev: true - /@graphql-yoga/plugin-response-cache@3.2.1(@envelop/core@5.0.0)(graphql-yoga@5.1.1)(graphql@16.8.1): - resolution: {integrity: sha512-GBalsZzJxp3pTllLx/cdTyVFu3zR/YevfntC3bFJifQcBuBxnAXqsyrbHp87gc/boBiZP11u6NhG++Ta9mvxew==} + /@graphql-yoga/plugin-response-cache@3.3.0-alpha-20240116103344-655e7299(@envelop/core@5.0.0)(graphql-yoga@5.1.1)(graphql@16.8.1): + resolution: {integrity: sha512-MNaYFY5VMoPpP9H63FF2Q2Z2d8xLcuLgzRGSH5nuX4vM6tD+nvBThpYDEgP9XfM4qSncT/99PnExtcZxHB+D1A==} engines: {node: '>=18.0.0'} peerDependencies: - graphql: ^15.2.0 || ^16.0.0 + graphql: 16.6.0 graphql-yoga: ^5.1.1 dependencies: '@envelop/response-cache': 6.1.2(@envelop/core@5.0.0)(graphql@16.8.1) From 8dc51b42eb8d1ae4a336aea3a9d7566cdc52b249 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Tue, 16 Jan 2024 13:18:24 +0100 Subject: [PATCH 2/3] test: ensure operation is not reported if context creation raises error --- packages/libraries/client/tests/yoga.spec.ts | 102 +++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/packages/libraries/client/tests/yoga.spec.ts b/packages/libraries/client/tests/yoga.spec.ts index b88873a236..f8648baa55 100644 --- a/packages/libraries/client/tests/yoga.spec.ts +++ b/packages/libraries/client/tests/yoga.spec.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { GraphQLError } from 'graphql'; // eslint-disable-next-line import/no-extraneous-dependencies import { createSchema, createYoga } from 'graphql-yoga'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -328,3 +329,104 @@ it('does not report usage for operation that does not pass validation', async () }, 1000); }); }); + +it('does not report usage if context creating raises an error', async () => { + const callback = vi.fn(); + const graphqlScope = nock('http://localhost') + .post('/graphql') + .reply(200, { + data: { + __typename: 'Query', + tokenInfo: { + __typename: 'TokenInfo', + token: { + name: 'brrrt', + }, + organization: { + name: 'mom', + cleanId: 'ur-mom', + }, + project: { + name: 'projecto', + type: 'FEDERATION', + cleanId: 'projecto', + }, + target: { + name: 'projecto', + cleanId: 'projecto', + }, + canReportSchema: true, + canCollectUsage: true, + canReadOperations: true, + }, + }, + }); + + const yoga = createYoga({ + schema: createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hi: String + } + `, + }), + plugins: [ + { + onContextBuilding() { + throw new GraphQLError('Not authenticated.'); + }, + }, + useHive({ + enabled: true, + debug: true, + token: 'brrrt', + selfHosting: { + applicationUrl: 'http://localhost/foo', + graphqlEndpoint: 'http://localhost/graphql', + usageEndpoint: 'http://localhost/usage', + }, + usage: { + endpoint: 'http://localhost/usage', + clientInfo() { + return { + name: 'brrr', + version: '1', + }; + }, + }, + agent: { + maxSize: 1, + }, + }), + ], + }); + + // eslint-disable-next-line no-async-promise-executor + await new Promise(async (resolve, reject) => { + nock.emitter.once('no match', (req: any) => { + reject(new Error(`Unexpected request was sent to ${req.path}`)); + }); + + const res = await yoga.fetch('http://localhost/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: /* GraphQL */ ` + { + hi + } + `, + }), + }); + expect(res.status).toBe(200); + expect(await res.text()).toMatchInlineSnapshot(`{"errors":[{"message":"Not authenticated."}]}`); + + setTimeout(() => { + graphqlScope.done(); + expect(callback).not.toHaveBeenCalled(); + resolve(); + }, 1000); + }); +}); From 0296755a1ea7ac7507d3cd239be65f5996f362c3 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Tue, 16 Jan 2024 15:24:39 +0100 Subject: [PATCH 3/3] chore: bump deps to stable version --- packages/services/server/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/services/server/package.json b/packages/services/server/package.json index 0fa6f087c8..b115998f70 100644 --- a/packages/services/server/package.json +++ b/packages/services/server/package.json @@ -22,7 +22,7 @@ "@escape.tech/graphql-armor-max-tokens": "2.3.0", "@graphql-hive/client": "workspace:*", "@graphql-yoga/plugin-persisted-operations": "3.1.1", - "@graphql-yoga/plugin-response-cache": "3.3.0-alpha-20240116103344-655e7299", + "@graphql-yoga/plugin-response-cache": "3.3.0", "@hive/api": "workspace:*", "@hive/cdn-script": "workspace:*", "@hive/service-common": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7da5fd7a12..895e32bde6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1052,8 +1052,8 @@ importers: specifier: 3.1.1 version: 3.1.1(@graphql-tools/utils@10.0.12)(graphql-yoga@5.1.1)(graphql@16.8.1) '@graphql-yoga/plugin-response-cache': - specifier: 3.3.0-alpha-20240116103344-655e7299 - version: 3.3.0-alpha-20240116103344-655e7299(@envelop/core@5.0.0)(graphql-yoga@5.1.1)(graphql@16.8.1) + specifier: 3.3.0 + version: 3.3.0(@envelop/core@5.0.0)(graphql-yoga@5.1.1)(graphql@16.8.1) '@hive/api': specifier: workspace:* version: link:../api @@ -7481,11 +7481,11 @@ packages: graphql-yoga: 5.1.1(graphql@16.8.1) dev: true - /@graphql-yoga/plugin-response-cache@3.3.0-alpha-20240116103344-655e7299(@envelop/core@5.0.0)(graphql-yoga@5.1.1)(graphql@16.8.1): - resolution: {integrity: sha512-MNaYFY5VMoPpP9H63FF2Q2Z2d8xLcuLgzRGSH5nuX4vM6tD+nvBThpYDEgP9XfM4qSncT/99PnExtcZxHB+D1A==} + /@graphql-yoga/plugin-response-cache@3.3.0(@envelop/core@5.0.0)(graphql-yoga@5.1.1)(graphql@16.8.1): + resolution: {integrity: sha512-SoVpPqR3tBeSyrdVb81zBGehWgtdeqBxEh2HTuv10jhLCL62nvCrCMfe1DYD6AvRE8DlRfLY/uCn8lwS3CAOwg==} engines: {node: '>=18.0.0'} peerDependencies: - graphql: 16.6.0 + graphql: ^15.2.0 || ^16.0.0 graphql-yoga: ^5.1.1 dependencies: '@envelop/response-cache': 6.1.2(@envelop/core@5.0.0)(graphql@16.8.1)