From 270d8c25d325d584b7b446af472e55abd9e72448 Mon Sep 17 00:00:00 2001 From: Trevor Scheer Date: Fri, 20 May 2022 16:23:37 -0700 Subject: [PATCH] Use usage reporting code from new utils package (#6449) --- cspell-dict.txt | 1 + package-lock.json | 148 +++++++-- package.json | 1 - packages/apollo-server-core/package.json | 2 +- ...efaultUsageReportingSignature.test.ts.snap | 17 - .../defaultUsageReportingSignature.test.ts | 144 --------- .../__tests__/referencedFields.test.ts | 299 ------------------ .../defaultUsageReportingSignature.ts | 266 ---------------- .../operationDerivedDataCache.ts | 2 +- .../src/plugin/usageReporting/plugin.ts | 6 +- .../plugin/usageReporting/referencedFields.ts | 82 ----- .../src/plugin/usageReporting/stats.ts | 2 +- 12 files changed, 134 insertions(+), 836 deletions(-) delete mode 100644 packages/apollo-server-core/src/plugin/usageReporting/__tests__/__snapshots__/defaultUsageReportingSignature.test.ts.snap delete mode 100644 packages/apollo-server-core/src/plugin/usageReporting/__tests__/defaultUsageReportingSignature.test.ts delete mode 100644 packages/apollo-server-core/src/plugin/usageReporting/__tests__/referencedFields.test.ts delete mode 100644 packages/apollo-server-core/src/plugin/usageReporting/defaultUsageReportingSignature.ts delete mode 100644 packages/apollo-server-core/src/plugin/usageReporting/referencedFields.ts diff --git a/cspell-dict.txt b/cspell-dict.txt index 6f81cef2309..f7d02e67caa 100644 --- a/cspell-dict.txt +++ b/cspell-dict.txt @@ -168,6 +168,7 @@ Unsubscriber Unversioned Uppercasing urlencode +usagereporting USERCONFIG uuidv vendia diff --git a/package-lock.json b/package-lock.json index f21971c8189..c9023c072b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,6 @@ "@types/jest": "27.5.1", "@types/koa-router": "7.4.4", "@types/lodash": "4.14.182", - "@types/lodash.sortby": "4.7.7", "@types/lodash.sumby": "4.6.7", "@types/lodash.xorby": "4.7.7", "@types/lru-cache": "5.1.1", @@ -198,11 +197,91 @@ "version": "10.17.59", "license": "MIT" }, + "node_modules/@apollo/utils.dropunuseddefinitions": { + "version": "1.1.0", + "resolved": "https://pkg.csb.dev/apollographql/apollo-utils/commit/c0685309/@apollo/utils.dropunuseddefinitions", + "integrity": "sha512-R2iZgXB+vTEx+B+N2mME0SP61+iEt+5fM26bYBvubyZflKed1VH2YiPLIl44nrahIph5JsTIvi/CeUsveJRZkg==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, "node_modules/@apollo/utils.logger": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@apollo/utils.logger/-/utils.logger-1.0.0.tgz", "integrity": "sha512-dx9XrjyisD2pOa+KsB5RcDbWIAdgC91gJfeyLCgy0ctJMjQe7yZK5kdWaWlaOoCeX0z6YI9iYlg7vMPyMpQF3Q==" }, + "node_modules/@apollo/utils.printwithreducedwhitespace": { + "version": "1.1.0", + "resolved": "https://pkg.csb.dev/apollographql/apollo-utils/commit/c0685309/@apollo/utils.printwithreducedwhitespace", + "integrity": "sha512-r7ys4PHpkXQIMmas8v9KSBaCV4XinDDAgnJk+IDpomGigxylpKOFZlUoZm5s6LF3LJmgb0DHAypyOJ5MzlL2jA==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.removealiases": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.removealiases/-/utils.removealiases-1.0.0.tgz", + "integrity": "sha512-6cM8sEOJW2LaGjL/0vHV0GtRaSekrPQR4DiywaApQlL9EdROASZU5PsQibe2MWeZCOhNrPRuHh4wDMwPsWTn8A==", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.sortast": { + "version": "1.1.0", + "resolved": "https://pkg.csb.dev/apollographql/apollo-utils/commit/c0685309/@apollo/utils.sortast", + "integrity": "sha512-YCm3iJ8BHO1x0OTkpVOd216x6K2kQ6cenfsbcJ/aCKa4fYvI0GhXWEm+5aX0tUW2PlHMRWqkKwFU+z6JtjA4IA==", + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.stripsensitiveliterals": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.stripsensitiveliterals/-/utils.stripsensitiveliterals-1.2.0.tgz", + "integrity": "sha512-E41rDUzkz/cdikM5147d8nfCFVKovXxKBcjvLEQ7bjZm/cg9zEcXvS6vFY8ugTubI3fn6zoqo0CyU8zT+BGP9w==", + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.usagereporting": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.usagereporting/-/utils.usagereporting-1.0.0.tgz", + "integrity": "sha512-5PL7hJMkTPmdo3oxPtigRrIyPxDk/ddrUryHPDaezL1lSFExpNzsDd2f1j0XJoHOg350GRd3LyD64caLA2PU1w==", + "dependencies": { + "@apollo/utils.dropunuseddefinitions": "^1.1.0", + "@apollo/utils.printwithreducedwhitespace": "^1.1.0", + "@apollo/utils.removealiases": "1.0.0", + "@apollo/utils.sortast": "^1.1.0", + "@apollo/utils.stripsensitiveliterals": "^1.2.0", + "apollo-reporting-protobuf": "^3.3.1" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, "node_modules/@apollographql/apollo-tools": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.5.4.tgz", @@ -6517,15 +6596,6 @@ "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", "dev": true }, - "node_modules/@types/lodash.sortby": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/@types/lodash.sortby/-/lodash.sortby-4.7.7.tgz", - "integrity": "sha512-J/4IS+jQopGBrrRetBXDCX0KnSeXJZ0rOTmGAxR9MWGV24YdHxX8IRi9LCGAU9GKWlBov9KRSfQpuup9PReqrw==", - "dev": true, - "dependencies": { - "@types/lodash": "*" - } - }, "node_modules/@types/lodash.sumby": { "version": "4.6.7", "resolved": "https://registry.npmjs.org/@types/lodash.sumby/-/lodash.sumby-4.6.7.tgz", @@ -21338,6 +21408,7 @@ "license": "MIT", "dependencies": { "@apollo/utils.logger": "^1.0.0", + "@apollo/utils.usagereporting": "^1.0.0", "@apollographql/apollo-tools": "^0.5.3", "@apollographql/graphql-playground-html": "1.6.29", "@graphql-tools/mock": "^8.1.2", @@ -21353,7 +21424,6 @@ "async-retry": "^1.2.1", "fast-json-stable-stringify": "^2.1.0", "graphql-tag": "^2.11.0", - "lodash.sortby": "^4.7.0", "loglevel": "^1.6.8", "lru-cache": "^6.0.0", "sha.js": "^2.4.11", @@ -21674,11 +21744,56 @@ } } }, + "@apollo/utils.dropunuseddefinitions": { + "version": "1.1.0", + "resolved": "https://pkg.csb.dev/apollographql/apollo-utils/commit/c0685309/@apollo/utils.dropunuseddefinitions", + "integrity": "sha512-R2iZgXB+vTEx+B+N2mME0SP61+iEt+5fM26bYBvubyZflKed1VH2YiPLIl44nrahIph5JsTIvi/CeUsveJRZkg==", + "requires": {} + }, "@apollo/utils.logger": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@apollo/utils.logger/-/utils.logger-1.0.0.tgz", "integrity": "sha512-dx9XrjyisD2pOa+KsB5RcDbWIAdgC91gJfeyLCgy0ctJMjQe7yZK5kdWaWlaOoCeX0z6YI9iYlg7vMPyMpQF3Q==" }, + "@apollo/utils.printwithreducedwhitespace": { + "version": "1.1.0", + "resolved": "https://pkg.csb.dev/apollographql/apollo-utils/commit/c0685309/@apollo/utils.printwithreducedwhitespace", + "integrity": "sha512-r7ys4PHpkXQIMmas8v9KSBaCV4XinDDAgnJk+IDpomGigxylpKOFZlUoZm5s6LF3LJmgb0DHAypyOJ5MzlL2jA==", + "requires": {} + }, + "@apollo/utils.removealiases": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.removealiases/-/utils.removealiases-1.0.0.tgz", + "integrity": "sha512-6cM8sEOJW2LaGjL/0vHV0GtRaSekrPQR4DiywaApQlL9EdROASZU5PsQibe2MWeZCOhNrPRuHh4wDMwPsWTn8A==", + "requires": {} + }, + "@apollo/utils.sortast": { + "version": "1.1.0", + "resolved": "https://pkg.csb.dev/apollographql/apollo-utils/commit/c0685309/@apollo/utils.sortast", + "integrity": "sha512-YCm3iJ8BHO1x0OTkpVOd216x6K2kQ6cenfsbcJ/aCKa4fYvI0GhXWEm+5aX0tUW2PlHMRWqkKwFU+z6JtjA4IA==", + "requires": { + "lodash.sortby": "^4.7.0" + } + }, + "@apollo/utils.stripsensitiveliterals": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.stripsensitiveliterals/-/utils.stripsensitiveliterals-1.2.0.tgz", + "integrity": "sha512-E41rDUzkz/cdikM5147d8nfCFVKovXxKBcjvLEQ7bjZm/cg9zEcXvS6vFY8ugTubI3fn6zoqo0CyU8zT+BGP9w==", + "requires": {} + }, + "@apollo/utils.usagereporting": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.usagereporting/-/utils.usagereporting-1.0.0.tgz", + "integrity": "sha512-5PL7hJMkTPmdo3oxPtigRrIyPxDk/ddrUryHPDaezL1lSFExpNzsDd2f1j0XJoHOg350GRd3LyD64caLA2PU1w==", + "requires": { + "@apollo/utils.dropunuseddefinitions": "^1.1.0", + "@apollo/utils.printwithreducedwhitespace": "^1.1.0", + "@apollo/utils.removealiases": "1.0.0", + "@apollo/utils.sortast": "^1.1.0", + "@apollo/utils.stripsensitiveliterals": "^1.2.0", + "apollo-reporting-protobuf": "^3.3.1" + } + }, "@apollographql/apollo-tools": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.5.4.tgz", @@ -26566,15 +26681,6 @@ "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==", "dev": true }, - "@types/lodash.sortby": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/@types/lodash.sortby/-/lodash.sortby-4.7.7.tgz", - "integrity": "sha512-J/4IS+jQopGBrrRetBXDCX0KnSeXJZ0rOTmGAxR9MWGV24YdHxX8IRi9LCGAU9GKWlBov9KRSfQpuup9PReqrw==", - "dev": true, - "requires": { - "@types/lodash": "*" - } - }, "@types/lodash.sumby": { "version": "4.6.7", "resolved": "https://registry.npmjs.org/@types/lodash.sumby/-/lodash.sumby-4.6.7.tgz", @@ -27101,6 +27207,7 @@ "version": "file:packages/apollo-server-core", "requires": { "@apollo/utils.logger": "^1.0.0", + "@apollo/utils.usagereporting": "^1.0.0", "@apollographql/apollo-tools": "^0.5.3", "@apollographql/graphql-playground-html": "1.6.29", "@graphql-tools/mock": "^8.1.2", @@ -27116,7 +27223,6 @@ "async-retry": "^1.2.1", "fast-json-stable-stringify": "^2.1.0", "graphql-tag": "^2.11.0", - "lodash.sortby": "^4.7.0", "loglevel": "^1.6.8", "lru-cache": "^6.0.0", "sha.js": "^2.4.11", diff --git a/package.json b/package.json index 9137e8b2ab5..2a01799ed7b 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "@types/jest": "27.5.1", "@types/koa-router": "7.4.4", "@types/lodash": "4.14.182", - "@types/lodash.sortby": "4.7.7", "@types/lodash.sumby": "4.6.7", "@types/lodash.xorby": "4.7.7", "@types/lru-cache": "5.1.1", diff --git a/packages/apollo-server-core/package.json b/packages/apollo-server-core/package.json index 59aeb5f3816..30b99af35ca 100644 --- a/packages/apollo-server-core/package.json +++ b/packages/apollo-server-core/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@apollo/utils.logger": "^1.0.0", + "@apollo/utils.usagereporting": "^1.0.0", "@apollographql/apollo-tools": "^0.5.3", "@apollographql/graphql-playground-html": "1.6.29", "@graphql-tools/mock": "^8.1.2", @@ -41,7 +42,6 @@ "async-retry": "^1.2.1", "fast-json-stable-stringify": "^2.1.0", "graphql-tag": "^2.11.0", - "lodash.sortby": "^4.7.0", "loglevel": "^1.6.8", "lru-cache": "^6.0.0", "sha.js": "^2.4.11", diff --git a/packages/apollo-server-core/src/plugin/usageReporting/__tests__/__snapshots__/defaultUsageReportingSignature.test.ts.snap b/packages/apollo-server-core/src/plugin/usageReporting/__tests__/__snapshots__/defaultUsageReportingSignature.test.ts.snap deleted file mode 100644 index eed69eb4ed1..00000000000 --- a/packages/apollo-server-core/src/plugin/usageReporting/__tests__/__snapshots__/defaultUsageReportingSignature.test.ts.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`defaultUsageReportingSignature basic test 1`] = `"{user{name}}"`; - -exports[`defaultUsageReportingSignature basic test with query 1`] = `"{user{name}}"`; - -exports[`defaultUsageReportingSignature basic with operation name 1`] = `"query OpName{user{name}}"`; - -exports[`defaultUsageReportingSignature fragment 1`] = `"fragment Bar on User{asd}{user{name...Bar}}"`; - -exports[`defaultUsageReportingSignature fragments in various order 1`] = `"fragment Bar on User{asd}{user{name...Bar}}"`; - -exports[`defaultUsageReportingSignature full test 1`] = `"fragment Bar on User{age@skip(if:$a)...Nested}fragment Nested on User{blah}query Foo($a:Boolean,$b:Int){user(age:0,name:\\"\\"){name tz...Bar...on User{bee hello}}}"`; - -exports[`defaultUsageReportingSignature with various argument types 1`] = `"query OpName($a:[[Boolean!]!],$b:EnumType,$c:Int!){user{name(apple:$a,bag:$b,cat:$c)}}"`; - -exports[`defaultUsageReportingSignature with various inline types 1`] = `"query OpName{user{name(apple:[],bag:{},cat:ENUM_VALUE)}}"`; diff --git a/packages/apollo-server-core/src/plugin/usageReporting/__tests__/defaultUsageReportingSignature.test.ts b/packages/apollo-server-core/src/plugin/usageReporting/__tests__/defaultUsageReportingSignature.test.ts deleted file mode 100644 index d9d98e491a7..00000000000 --- a/packages/apollo-server-core/src/plugin/usageReporting/__tests__/defaultUsageReportingSignature.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { default as gql, disableFragmentWarnings } from 'graphql-tag'; -import { defaultUsageReportingSignature } from '../defaultUsageReportingSignature'; - -// The gql duplicate fragment warning feature really is just warnings; nothing -// breaks if you turn it off in tests. -disableFragmentWarnings(); - -describe('defaultUsageReportingSignature', () => { - const cases = [ - // Test cases borrowed from optics-agent-js. - { - name: 'basic test', - operationName: '', - input: gql` - { - user { - name - } - } - `, - }, - { - name: 'basic test with query', - operationName: '', - input: gql` - query { - user { - name - } - } - `, - }, - { - name: 'basic with operation name', - operationName: 'OpName', - input: gql` - query OpName { - user { - name - } - } - `, - }, - { - name: 'with various inline types', - operationName: 'OpName', - input: gql` - query OpName { - user { - name(apple: [[10]], cat: ENUM_VALUE, bag: { input: "value" }) - } - } - `, - }, - { - name: 'with various argument types', - operationName: 'OpName', - input: gql` - query OpName($c: Int!, $a: [[Boolean!]!], $b: EnumType) { - user { - name(apple: $a, cat: $c, bag: $b) - } - } - `, - }, - { - name: 'fragment', - operationName: '', - input: gql` - { - user { - name - ...Bar - } - } - - fragment Bar on User { - asd - } - - fragment Baz on User { - jkl - } - `, - }, - { - name: 'fragments in various order', - operationName: '', - input: gql` - fragment Bar on User { - asd - } - - { - user { - name - ...Bar - } - } - - fragment Baz on User { - jkl - } - `, - }, - { - name: 'full test', - operationName: 'Foo', - input: gql` - query Foo($b: Int, $a: Boolean) { - user(name: "hello", age: 5) { - ...Bar - ... on User { - hello - bee - } - tz - aliased: name - } - } - - fragment Baz on User { - asd - } - - fragment Bar on User { - age @skip(if: $a) - ...Nested - } - - fragment Nested on User { - blah - } - `, - }, - ]; - cases.forEach(({ name, operationName, input }) => { - test(name, () => { - expect( - defaultUsageReportingSignature(input, operationName), - ).toMatchSnapshot(); - }); - }); -}); diff --git a/packages/apollo-server-core/src/plugin/usageReporting/__tests__/referencedFields.test.ts b/packages/apollo-server-core/src/plugin/usageReporting/__tests__/referencedFields.test.ts deleted file mode 100644 index b0c74b66ad0..00000000000 --- a/packages/apollo-server-core/src/plugin/usageReporting/__tests__/referencedFields.test.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { buildASTSchema, DocumentNode, validate } from 'graphql'; -import gql from 'graphql-tag'; -import { calculateReferencedFieldsByType } from '../referencedFields'; - -const schema = buildASTSchema(gql` - type Query { - f1: Int - f2: Int - a: A - aa: A - myInterface: MyInterface - } - - type A implements MyInterface { - x: ID - y: String! - } - - interface MyInterface { - x: ID - } -`); - -function validateAndCalculate({ - document, - resolvedOperationName = null, -}: { - document: DocumentNode; - resolvedOperationName?: string | null; -}) { - // First validate the document, since calculateReferencedFieldsByType expects - // that. - expect(validate(schema, document)).toStrictEqual([]); - return calculateReferencedFieldsByType({ - schema, - document, - resolvedOperationName, - }); -} - -describe('calculateReferencedFieldsByType', () => { - it('basic', () => { - expect( - validateAndCalculate({ - document: gql` - { - f1 - } - `, - }), - ).toMatchInlineSnapshot(` - Object { - "Query": Object { - "fieldNames": Array [ - "f1", - ], - "isInterface": false, - }, - } - `); - }); - - it('aliases use actual field name', () => { - expect( - validateAndCalculate({ - document: gql` - { - aliased: f1 - } - `, - }), - ).toMatchInlineSnapshot(` - Object { - "Query": Object { - "fieldNames": Array [ - "f1", - ], - "isInterface": false, - }, - } - `); - }); - - it('multiple operations and fragments', () => { - expect( - validateAndCalculate({ - document: gql` - query Q1 { - f1 - a { - ...AStuff - } - } - query Q2 { - f2 - aa { - ...OtherAStuff - } - } - fragment AStuff on A { - x - } - fragment OtherAStuff on A { - y - } - `, - resolvedOperationName: 'Q1', - }), - ).toMatchInlineSnapshot(` - Object { - "A": Object { - "fieldNames": Array [ - "x", - ], - "isInterface": false, - }, - "Query": Object { - "fieldNames": Array [ - "f1", - "a", - ], - "isInterface": false, - }, - } - `); - }); - - it('interfaces', () => { - expect( - validateAndCalculate({ - document: gql` - query { - myInterface { - x - } - } - `, - }), - ).toMatchInlineSnapshot(` - Object { - "MyInterface": Object { - "fieldNames": Array [ - "x", - ], - "isInterface": true, - }, - "Query": Object { - "fieldNames": Array [ - "myInterface", - ], - "isInterface": false, - }, - } - `); - }); - - it('interface with fragment', () => { - expect( - validateAndCalculate({ - document: gql` - query { - myInterface { - x - ... on A { - y - } - } - } - `, - }), - ).toMatchInlineSnapshot(` - Object { - "A": Object { - "fieldNames": Array [ - "y", - ], - "isInterface": false, - }, - "MyInterface": Object { - "fieldNames": Array [ - "x", - ], - "isInterface": true, - }, - "Query": Object { - "fieldNames": Array [ - "myInterface", - ], - "isInterface": false, - }, - } - `); - }); -}); - -it('interface with fragment that uses interface field', () => { - expect( - validateAndCalculate({ - document: gql` - query { - myInterface { - ... on A { - # Even though x exists on the interface, we only want this to - # count towards A.x below, because this operation would work just - # as well if x were removed from the interface as long as it was - # left on A. - x - } - } - } - `, - }), - ).toMatchInlineSnapshot(` - Object { - "A": Object { - "fieldNames": Array [ - "x", - ], - "isInterface": false, - }, - "Query": Object { - "fieldNames": Array [ - "myInterface", - ], - "isInterface": false, - }, - } - `); -}); - -it('using field both with interface and object should work', () => { - expect( - validateAndCalculate({ - document: gql` - query { - myInterface { - x - ... on A { - x - } - } - } - `, - }), - ).toMatchInlineSnapshot(` - Object { - "A": Object { - "fieldNames": Array [ - "x", - ], - "isInterface": false, - }, - "MyInterface": Object { - "fieldNames": Array [ - "x", - ], - "isInterface": true, - }, - "Query": Object { - "fieldNames": Array [ - "myInterface", - ], - "isInterface": false, - }, - } - `); -}); - -it('using field multiple times (same level or otherwise) de-dupes', () => { - expect( - validateAndCalculate({ - document: gql` - query { - a1: a { - y - } - a2: a { - y - } - } - `, - }), - ).toMatchInlineSnapshot(` - Object { - "A": Object { - "fieldNames": Array [ - "y", - ], - "isInterface": false, - }, - "Query": Object { - "fieldNames": Array [ - "a", - ], - "isInterface": false, - }, - } - `); -}); diff --git a/packages/apollo-server-core/src/plugin/usageReporting/defaultUsageReportingSignature.ts b/packages/apollo-server-core/src/plugin/usageReporting/defaultUsageReportingSignature.ts deleted file mode 100644 index b032439a9ac..00000000000 --- a/packages/apollo-server-core/src/plugin/usageReporting/defaultUsageReportingSignature.ts +++ /dev/null @@ -1,266 +0,0 @@ -// In Apollo Studio, we want to group requests making the same query together, -// and treat different queries distinctly. But what does it mean for two queries -// to be "the same"? And what if you don't want to send the full text of the -// query to Apollo's servers, either because it contains sensitive data or -// because it contains extraneous operations or fragments? -// -// To solve these problems, ApolloServerPluginUsageReporting has the concept of -// "signatures". We don't (by default) send the full query string of queries to -// Apollo's servers. Instead, each trace has its query string's "signature". -// -// You can technically specify any function mapping a GraphQL query AST -// (DocumentNode) to string as your signature algorithm by providing it as the -// 'calculateSignature' option to ApolloServerPluginUsageReporting. (This option -// is not recommended, because Apollo's servers make some assumptions about the -// semantics of your operation based on the signature.) This file defines the -// default function used for this purpose: defaultUsageReportingSignature -// (formerly known as defaultEngineReportingSignature). -// -// This module utilizes several AST transformations from the adjacent -// 'transforms' file. (You could use them to build your own `calculateSignature` -// callback, but as mentioned above, you shouldn't really define that callback, -// so they are not exported from the package.) - -// - dropUnusedDefinitions, which removes operations and fragments that aren't -// going to be used in execution -// - hideLiterals, which replaces all numeric and string literals as well as -// list and object input values with "empty" values -// - removeAliases, which removes field aliasing from the query -// - sortAST, which sorts the children of most multi-child nodes consistently -// - printWithReducedWhitespace, a variant on graphql-js's 'print' which gets -// rid of unneeded whitespace -// -// defaultUsageReportingSignature consists of applying all of these building -// blocks. -// -// Historical note: the default signature algorithm of the Go engineproxy -// performed all of the above operations, and Apollo's servers then re-ran a -// mostly identical signature implementation on received traces. This was -// primarily to deal with edge cases where some users used literal interpolation -// instead of GraphQL variables, included randomized alias names, etc. In -// addition, the servers relied on the fact that dropUnusedDefinitions had been -// called in order (and that the signature could be parsed as GraphQL) to -// extract the name of the operation for display. This caused confusion, as the -// query document shown in the Studio UI wasn't the same as the one actually -// sent. ApolloServerPluginUsageReporting (previously apollo-engine-reporting) -// uses a reporting API which requires it to explicitly include the operation -// name with each signature; this means that the server no longer needs to parse -// the signature or run its own signature algorithm on it, and the details of -// the signature algorithm are now up to the reporting agent. That said, not all -// Studio features will work properly if your signature function changes the -// signature in unexpected ways. -// -// This function used to live in the `apollo-graphql` package in the -// `apollo-tooling` repository. -// -// Note that this is not exactly the same algorithm as the operation registry -// signature function, which continues to live in `apollo-graphql`. -import { - DirectiveNode, - DocumentNode, - FieldNode, - FloatValueNode, - FragmentDefinitionNode, - FragmentSpreadNode, - InlineFragmentNode, - IntValueNode, - ListValueNode, - ObjectValueNode, - OperationDefinitionNode, - print, - SelectionSetNode, - separateOperations, - StringValueNode, - visit, -} from 'graphql'; -import sortBy from 'lodash.sortby'; - -export function defaultUsageReportingSignature( - ast: DocumentNode, - operationName: string, -): string { - return printWithReducedWhitespace( - sortAST( - removeAliases(hideLiterals(dropUnusedDefinitions(ast, operationName))), - ), - ); -} - -// Like the graphql-js print function, but deleting whitespace wherever -// feasible. Specifically, all whitespace (outside of string literals) is -// reduced to at most one space, and even that space is removed anywhere except -// for between two alphanumerics. -// -// Note that recent versions of graphql-js contain a stripIgnoredCharacters -// function; it would be better to use that instead, though whenever we change -// the signature algorithm it does make every operation appear to change in -// Studio. -// -// In a GraphQL AST (which notably does not contain comments), the only place -// where meaningful whitespace (or double quotes) can exist is in StringNodes. -// So to print with reduced whitespace, we: -// - temporarily sanitize strings by replacing their contents with hex -// - use the default GraphQL printer -// - minimize the whitespace with a simple regexp replacement -// - convert strings back to their actual value We normalize all strings to -// non-block strings for simplicity. -// (Unlike stripIgnoredCharacters, this does not remove commas.) -function printWithReducedWhitespace(ast: DocumentNode): string { - const sanitizedAST = visit(ast, { - StringValue(node: StringValueNode): StringValueNode { - return { - ...node, - value: Buffer.from(node.value, 'utf8').toString('hex'), - block: false, - }; - }, - }); - const withWhitespace = print(sanitizedAST); - const minimizedButStillHex = withWhitespace - .replace(/\s+/g, ' ') - .replace(/([^_a-zA-Z0-9]) /g, (_, c) => c) - .replace(/ ([^_a-zA-Z0-9])/g, (_, c) => c); - return minimizedButStillHex.replace(/"([a-f0-9]+)"/g, (_, hex) => - JSON.stringify(Buffer.from(hex, 'hex').toString('utf8')), - ); -} - -// sortAST sorts most multi-child nodes alphabetically. Using this as part of -// your signature calculation function may make it easier to tell the difference -// between queries that are similar to each other, and if for some reason your -// GraphQL client generates query strings with elements in nondeterministic -// order, it can make sure the queries are treated as identical. -function sortAST(ast: DocumentNode): DocumentNode { - return visit(ast, { - Document(node: DocumentNode) { - return { - ...node, - // Use sortBy instead of sorted because 'definitions' is not optional. - // The sort on "kind" places fragments before operations within the document - definitions: sortBy(node.definitions, 'kind', 'name.value'), - }; - }, - OperationDefinition( - node: OperationDefinitionNode, - ): OperationDefinitionNode { - return { - ...node, - variableDefinitions: sorted( - node.variableDefinitions, - 'variable.name.value', - ), - }; - }, - SelectionSet(node: SelectionSetNode): SelectionSetNode { - return { - ...node, - // Define an ordering for field names in a SelectionSet. Field first, - // then FragmentSpread, then InlineFragment. By a lovely coincidence, - // the order we want them to appear in is alphabetical by node.kind. - // Use sortBy instead of sorted because 'selections' is not optional. - selections: sortBy(node.selections, 'kind', 'name.value'), - }; - }, - Field(node: FieldNode): FieldNode { - return { - ...node, - arguments: sorted(node.arguments, 'name.value'), - }; - }, - FragmentSpread(node: FragmentSpreadNode): FragmentSpreadNode { - return { ...node, directives: sorted(node.directives, 'name.value') }; - }, - InlineFragment(node: InlineFragmentNode): InlineFragmentNode { - return { ...node, directives: sorted(node.directives, 'name.value') }; - }, - FragmentDefinition(node: FragmentDefinitionNode): FragmentDefinitionNode { - return { - ...node, - directives: sorted(node.directives, 'name.value'), - variableDefinitions: sorted( - node.variableDefinitions, - 'variable.name.value', - ), - }; - }, - Directive(node: DirectiveNode): DirectiveNode { - return { ...node, arguments: sorted(node.arguments, 'name.value') }; - }, - }); -} - -// Like lodash's sortBy, but sorted(undefined) === undefined rather than []. It -// is a stable non-in-place sort. -function sorted( - items: ReadonlyArray | undefined, - ...iteratees: string[] -): T[] | undefined { - if (items) { - return sortBy(items, ...iteratees); - } - return undefined; -} - -// removeAliases gets rid of GraphQL aliases, a feature by which you can tell a -// server to return a field's data under a different name from the field -// name. Maybe this is useful if somebody somewhere inserts random aliases into -// their queries. -function removeAliases(ast: DocumentNode): DocumentNode { - return visit(ast, { - Field(node: FieldNode): FieldNode { - return { - ...node, - alias: undefined, - }; - }, - }); -} - -// Replace numeric, string, list, and object literals with "empty" -// values. Leaves enums alone (since there's no consistent "zero" enum). This -// can help combine similar queries if you substitute values directly into -// queries rather than use GraphQL variables, and can hide sensitive data in -// your query (say, a hardcoded API key) from Apollo's servers, but in general -// avoiding those situations is better than working around them. -function hideLiterals(ast: DocumentNode): DocumentNode { - return visit(ast, { - IntValue(node: IntValueNode): IntValueNode { - return { ...node, value: '0' }; - }, - FloatValue(node: FloatValueNode): FloatValueNode { - return { ...node, value: '0' }; - }, - StringValue(node: StringValueNode): StringValueNode { - return { ...node, value: '', block: false }; - }, - ListValue(node: ListValueNode): ListValueNode { - return { ...node, values: [] }; - }, - ObjectValue(node: ObjectValueNode): ObjectValueNode { - return { ...node, fields: [] }; - }, - }); -} - -// A GraphQL query may contain multiple named operations, with the operation to -// use specified separately by the client. This transformation drops unused -// operations from the query, as well as any fragment definitions that are not -// referenced. (In general we recommend that unused definitions are dropped on -// the client before sending to the server to save bandwidth and parsing time.) -// -// This has the unfortunate side effect that Studio usage reporting never finds -// out about fields that are referenced in operations and fragments in a request -// that are not executed, so (for example) schema checks don't understand that -// deleting those fields would make the client's request fail to validate. -function dropUnusedDefinitions( - ast: DocumentNode, - operationName: string, -): DocumentNode { - const separated = separateOperations(ast)[operationName]; - if (!separated) { - // If the given operationName isn't found, just make this whole transform a - // no-op instead of crashing. - return ast; - } - return separated; -} diff --git a/packages/apollo-server-core/src/plugin/usageReporting/operationDerivedDataCache.ts b/packages/apollo-server-core/src/plugin/usageReporting/operationDerivedDataCache.ts index 60f2fae3669..57eba5f5a5c 100644 --- a/packages/apollo-server-core/src/plugin/usageReporting/operationDerivedDataCache.ts +++ b/packages/apollo-server-core/src/plugin/usageReporting/operationDerivedDataCache.ts @@ -1,6 +1,6 @@ import LRUCache from 'lru-cache'; import type { Logger } from '@apollo/utils.logger'; -import type { ReferencedFieldsByType } from './referencedFields'; +import type { ReferencedFieldsByType } from '@apollo/utils.usagereporting'; export interface OperationDerivedData { signature: string; diff --git a/packages/apollo-server-core/src/plugin/usageReporting/plugin.ts b/packages/apollo-server-core/src/plugin/usageReporting/plugin.ts index 3205eff1e1d..7d644fbf3af 100644 --- a/packages/apollo-server-core/src/plugin/usageReporting/plugin.ts +++ b/packages/apollo-server-core/src/plugin/usageReporting/plugin.ts @@ -19,7 +19,7 @@ import { OperationDerivedData, operationDerivedDataCacheKey, } from './operationDerivedDataCache'; -import { defaultUsageReportingSignature } from './defaultUsageReportingSignature'; +import { usageReportingSignature } from '@apollo/utils.usagereporting'; import type { ApolloServerPluginUsageReportingOptions, SendValuesBaseOptions, @@ -34,7 +34,7 @@ import { defaultSendOperationsAsTrace } from './defaultSendOperationsAsTrace'; import { calculateReferencedFieldsByType, ReferencedFieldsByType, -} from './referencedFields'; +} from '@apollo/utils.usagereporting'; import type LRUCache from 'lru-cache'; const reportHeaderDefaults = { @@ -746,7 +746,7 @@ export function ApolloServerPluginUsageReporting( } const generatedSignature = ( - options.calculateSignature || defaultUsageReportingSignature + options.calculateSignature || usageReportingSignature )(requestContext.document, requestContext.operationName || ''); const generatedOperationDerivedData: OperationDerivedData = { diff --git a/packages/apollo-server-core/src/plugin/usageReporting/referencedFields.ts b/packages/apollo-server-core/src/plugin/usageReporting/referencedFields.ts deleted file mode 100644 index 480d5d3052a..00000000000 --- a/packages/apollo-server-core/src/plugin/usageReporting/referencedFields.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - DocumentNode, - GraphQLSchema, - isInterfaceType, - separateOperations, - TypeInfo, - visit, - visitWithTypeInfo, -} from 'graphql'; -import { ReferencedFieldsForType } from 'apollo-reporting-protobuf'; - -export type ReferencedFieldsByType = Record; - -export function calculateReferencedFieldsByType({ - document, - schema, - resolvedOperationName, -}: { - document: DocumentNode; - resolvedOperationName: string | null; - schema: GraphQLSchema; -}): ReferencedFieldsByType { - // If the document contains multiple operations, we only care about fields - // referenced in the operation we're using and in fragments that are - // (transitively) spread by that operation. (This is because Studio's field - // usage accounting is all by operation, not by document.) This does mean that - // a field can be textually present in a GraphQL document (and need to exist - // for validation) without being represented in the reported referenced fields - // structure, but we'd need to change the data model of Studio to be based on - // documents rather than fields if we wanted to improve that. - const documentSeparatedByOperation = separateOperations(document); - const filteredDocument = - documentSeparatedByOperation[resolvedOperationName ?? '']; - if (!filteredDocument) { - // This shouldn't happen because we only should call this function on - // properly executable documents. - throw Error( - `shouldn't happen: operation '${resolvedOperationName ?? ''}' not found`, - ); - } - const typeInfo = new TypeInfo(schema); - const interfaces = new Set(); - const referencedFieldSetByType: Record> = Object.create( - null, - ); - visit( - filteredDocument, - visitWithTypeInfo(typeInfo, { - Field(field) { - const fieldName = field.name.value; - const parentType = typeInfo.getParentType(); - if (!parentType) { - throw Error( - `shouldn't happen: missing parent type for field ${fieldName}`, - ); - } - const parentTypeName = parentType.name; - if (!referencedFieldSetByType[parentTypeName]) { - referencedFieldSetByType[parentTypeName] = new Set(); - if (isInterfaceType(parentType)) { - interfaces.add(parentTypeName); - } - } - referencedFieldSetByType[parentTypeName].add(fieldName); - }, - }), - ); - - // Convert from initial representation (which uses Sets to avoid quadratic - // behavior) to the protobufjs objects. (We could also use js_use_toArray here - // but that seems a little overkill.) - const referencedFieldsByType = Object.create(null); - for (const [typeName, fieldNames] of Object.entries( - referencedFieldSetByType, - )) { - referencedFieldsByType[typeName] = new ReferencedFieldsForType({ - fieldNames: [...fieldNames], - isInterface: interfaces.has(typeName), - }); - } - return referencedFieldsByType; -} diff --git a/packages/apollo-server-core/src/plugin/usageReporting/stats.ts b/packages/apollo-server-core/src/plugin/usageReporting/stats.ts index 480d8cfb7a3..2285c612e52 100644 --- a/packages/apollo-server-core/src/plugin/usageReporting/stats.ts +++ b/packages/apollo-server-core/src/plugin/usageReporting/stats.ts @@ -13,7 +13,7 @@ import { IReport, } from 'apollo-reporting-protobuf'; import { iterateOverTrace, ResponseNamePath } from './iterateOverTrace'; -import type { ReferencedFieldsByType } from './referencedFields'; +import type { ReferencedFieldsByType } from '@apollo/utils.usagereporting'; // protobuf.js exports both a class and an interface (starting with I) for each // message type. The class is what it produces when it decodes the message; the