From e55f231291aa204f9858fbf72c6f3c6467c8718f Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 1 Feb 2019 16:54:58 +0200 Subject: [PATCH 01/11] Setup the foundation for a new `apollo-graphql` utility library. As of this commit, this package provides nothing! --- package-lock.json | 3 +++ package.json | 1 + packages/apollo-graphql/.npmignore | 6 ++++++ packages/apollo-graphql/CHANGELOG.md | 4 ++++ packages/apollo-graphql/README.md | 1 + packages/apollo-graphql/jest.config.js | 3 +++ packages/apollo-graphql/package.json | 17 +++++++++++++++++ .../apollo-graphql/src/__tests__/tsconfig.json | 7 +++++++ packages/apollo-graphql/src/index.ts | 0 packages/apollo-graphql/tsconfig.json | 10 ++++++++++ tsconfig.build.json | 1 + tsconfig.test.json | 1 + 12 files changed, 54 insertions(+) create mode 100644 packages/apollo-graphql/.npmignore create mode 100644 packages/apollo-graphql/CHANGELOG.md create mode 100644 packages/apollo-graphql/README.md create mode 100644 packages/apollo-graphql/jest.config.js create mode 100644 packages/apollo-graphql/package.json create mode 100644 packages/apollo-graphql/src/__tests__/tsconfig.json create mode 100644 packages/apollo-graphql/src/index.ts create mode 100644 packages/apollo-graphql/tsconfig.json diff --git a/package-lock.json b/package-lock.json index a6c5fd8b2c1..a31bfb847af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2077,6 +2077,9 @@ "cross-fetch": "^1.0.0" } }, + "apollo-graphql": { + "version": "file:packages/apollo-graphql" + }, "apollo-link": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.6.tgz", diff --git a/package.json b/package.json index 90e51db8a2a..15393355eca 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "apollo-datasource-rest": "file:packages/apollo-datasource-rest", "apollo-engine-reporting": "file:packages/apollo-engine-reporting", "apollo-engine-reporting-protobuf": "file:packages/apollo-engine-reporting-protobuf", + "apollo-graphql": "file:packages/apollo-graphql", "apollo-server": "file:packages/apollo-server", "apollo-server-azure-functions": "file:packages/apollo-server-azure-functions", "apollo-server-cache-memcached": "file:packages/apollo-server-cache-memcached", diff --git a/packages/apollo-graphql/.npmignore b/packages/apollo-graphql/.npmignore new file mode 100644 index 00000000000..a165046d359 --- /dev/null +++ b/packages/apollo-graphql/.npmignore @@ -0,0 +1,6 @@ +* +!src/**/* +!dist/**/* +dist/**/*.test.* +!package.json +!README.md diff --git a/packages/apollo-graphql/CHANGELOG.md b/packages/apollo-graphql/CHANGELOG.md new file mode 100644 index 00000000000..ef45b84f48c --- /dev/null +++ b/packages/apollo-graphql/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +### vNEXT + diff --git a/packages/apollo-graphql/README.md b/packages/apollo-graphql/README.md new file mode 100644 index 00000000000..07909aab7ea --- /dev/null +++ b/packages/apollo-graphql/README.md @@ -0,0 +1 @@ +# `apollo-graphql` diff --git a/packages/apollo-graphql/jest.config.js b/packages/apollo-graphql/jest.config.js new file mode 100644 index 00000000000..a383fbc925f --- /dev/null +++ b/packages/apollo-graphql/jest.config.js @@ -0,0 +1,3 @@ +const config = require('../../jest.config.base'); + +module.exports = Object.assign(Object.create(null), config); diff --git a/packages/apollo-graphql/package.json b/packages/apollo-graphql/package.json new file mode 100644 index 00000000000..955794a4b09 --- /dev/null +++ b/packages/apollo-graphql/package.json @@ -0,0 +1,17 @@ +{ + "name": "apollo-graphql", + "version": "0.0.0", + "description": "Apollo GraphQL utility library", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "keywords": [], + "author": "Apollo ", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "devDependencies": {}, + "peerDependencies": { + "graphql": "^14.0.0" + } +} diff --git a/packages/apollo-graphql/src/__tests__/tsconfig.json b/packages/apollo-graphql/src/__tests__/tsconfig.json new file mode 100644 index 00000000000..428259da813 --- /dev/null +++ b/packages/apollo-graphql/src/__tests__/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../../tsconfig.test.base", + "include": ["**/*"], + "references": [ + { "path": "../../" } + ] +} diff --git a/packages/apollo-graphql/src/index.ts b/packages/apollo-graphql/src/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/apollo-graphql/tsconfig.json b/packages/apollo-graphql/tsconfig.json new file mode 100644 index 00000000000..4fcb63fe14f --- /dev/null +++ b/packages/apollo-graphql/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["**/__tests__", "**/__mocks__"], + "references": [] +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 24a42b86b9e..ebb88ce4ba9 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -9,6 +9,7 @@ { "path": "./packages/apollo-datasource" }, { "path": "./packages/apollo-datasource-rest" }, { "path": "./packages/apollo-engine-reporting" }, + { "path": "./packages/apollo-graphql" }, { "path": "./packages/apollo-server" }, { "path": "./packages/apollo-server-azure-functions" }, { "path": "./packages/apollo-server-cache-memcached" }, diff --git a/tsconfig.test.json b/tsconfig.test.json index 1d906e03b7a..85f4028c15d 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -8,6 +8,7 @@ { "path": "./packages/apollo-cache-control/src/__tests__/" }, { "path": "./packages/apollo-datasource-rest/src/__tests__/" }, { "path": "./packages/apollo-engine-reporting/src/__tests__/" }, + { "path": "./packages/apollo-graphql/src/__tests__/" }, { "path": "./packages/apollo-server/src/__tests__/" }, { "path": "./packages/apollo-server-azure-functions/src/__tests__/" }, { "path": "./packages/apollo-server-cache-memcached/src/__tests__/" }, From 6092e99bf1f16f6f2b70409fbfc054e49ccaad56 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 1 Feb 2019 18:22:58 +0200 Subject: [PATCH 02/11] Publish - apollo-graphql@0.0.1-alpha.0 --- packages/apollo-graphql/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/apollo-graphql/package.json b/packages/apollo-graphql/package.json index 955794a4b09..ed7ab1471e8 100644 --- a/packages/apollo-graphql/package.json +++ b/packages/apollo-graphql/package.json @@ -1,6 +1,6 @@ { "name": "apollo-graphql", - "version": "0.0.0", + "version": "0.0.1-alpha.0", "description": "Apollo GraphQL utility library", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -10,7 +10,6 @@ "engines": { "node": ">=6" }, - "devDependencies": {}, "peerDependencies": { "graphql": "^14.0.0" } From 1a3ac9b4a0f567d96aab1dc38b220d293f388b71 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 1 Feb 2019 17:16:43 +0200 Subject: [PATCH 03/11] Move Apollo Engine signatures into `apollo-graphql`. --- package-lock.json | 8 +++++--- packages/apollo-engine-reporting/package.json | 3 +-- packages/apollo-graphql/package.json | 1 + .../src/__tests__/signature.test.ts | 0 .../src/signature.ts | 0 5 files changed, 7 insertions(+), 5 deletions(-) rename packages/{apollo-engine-reporting => apollo-graphql}/src/__tests__/signature.test.ts (100%) rename packages/{apollo-engine-reporting => apollo-graphql}/src/signature.ts (100%) diff --git a/package-lock.json b/package-lock.json index a31bfb847af..c4781cd61e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2042,8 +2042,7 @@ "apollo-server-core": "file:packages/apollo-server-core", "apollo-server-env": "file:packages/apollo-server-env", "async-retry": "^1.2.1", - "graphql-extensions": "file:packages/graphql-extensions", - "lodash": "^4.17.10" + "graphql-extensions": "file:packages/graphql-extensions" } }, "apollo-engine-reporting-protobuf": { @@ -2078,7 +2077,10 @@ } }, "apollo-graphql": { - "version": "file:packages/apollo-graphql" + "version": "file:packages/apollo-graphql", + "requires": { + "lodash": "^4.17.10" + } }, "apollo-link": { "version": "1.2.6", diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index ce2de71077d..ae10934a74e 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -15,7 +15,6 @@ "apollo-server-core": "file:../apollo-server-core", "apollo-server-env": "file:../apollo-server-env", "async-retry": "^1.2.1", - "graphql-extensions": "file:../graphql-extensions", - "lodash": "^4.17.10" + "graphql-extensions": "file:../graphql-extensions" } } diff --git a/packages/apollo-graphql/package.json b/packages/apollo-graphql/package.json index ed7ab1471e8..517ea163531 100644 --- a/packages/apollo-graphql/package.json +++ b/packages/apollo-graphql/package.json @@ -10,6 +10,7 @@ "engines": { "node": ">=6" }, + "devDependencies": {}, "peerDependencies": { "graphql": "^14.0.0" } diff --git a/packages/apollo-engine-reporting/src/__tests__/signature.test.ts b/packages/apollo-graphql/src/__tests__/signature.test.ts similarity index 100% rename from packages/apollo-engine-reporting/src/__tests__/signature.test.ts rename to packages/apollo-graphql/src/__tests__/signature.test.ts diff --git a/packages/apollo-engine-reporting/src/signature.ts b/packages/apollo-graphql/src/signature.ts similarity index 100% rename from packages/apollo-engine-reporting/src/signature.ts rename to packages/apollo-graphql/src/signature.ts From e565226370205c7900ddf5942ed540f9459ca6a2 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 1 Feb 2019 18:00:06 +0200 Subject: [PATCH 04/11] Move `apollo-engine-reporting` signature calculations to new `apollo-graphql`. Many of these signature calculation functions are now utilized in tools or helpers which are not directly related to `apollo-server` functionality, including various aspects of the `apollo` CLI which live within `apollo-tooling`. Currently, because of `apollo`'s dependency on `apollo-engine-reporting` for this signature, this requires bringing in the entire dependency tree which `apollo-server-core` relies on since `apollo-engine-reporting` depends on `apollo-server-core`. By moving this into this new `apollo-graphql` utility library, we're able to trim that rather hefty dependency tree and drastically reduce the download for running, say, `npx apollo`. --- package-lock.json | 1 + packages/apollo-engine-reporting/package.json | 1 + packages/apollo-engine-reporting/src/extension.ts | 4 ++-- packages/apollo-engine-reporting/src/index.ts | 9 --------- packages/apollo-graphql/package.json | 3 +++ packages/apollo-graphql/src/index.ts | 1 + packages/apollo-graphql/src/signature.ts | 2 +- 7 files changed, 9 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index c4781cd61e5..deb566a14a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2039,6 +2039,7 @@ "version": "file:packages/apollo-engine-reporting", "requires": { "apollo-engine-reporting-protobuf": "file:packages/apollo-engine-reporting-protobuf", + "apollo-graphql": "file:packages/apollo-graphql", "apollo-server-core": "file:packages/apollo-server-core", "apollo-server-env": "file:packages/apollo-server-env", "async-retry": "^1.2.1", diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index ae10934a74e..fd723338cf6 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "apollo-engine-reporting-protobuf": "file:../apollo-engine-reporting-protobuf", + "apollo-graphql": "file:../apollo-graphql", "apollo-server-core": "file:../apollo-server-core", "apollo-server-env": "file:../apollo-server-env", "async-retry": "^1.2.1", diff --git a/packages/apollo-engine-reporting/src/extension.ts b/packages/apollo-engine-reporting/src/extension.ts index 37ca3adfcc1..86ad82476a5 100644 --- a/packages/apollo-engine-reporting/src/extension.ts +++ b/packages/apollo-engine-reporting/src/extension.ts @@ -16,7 +16,7 @@ import { import { Trace, google } from 'apollo-engine-reporting-protobuf'; import { EngineReportingOptions, GenerateClientInfo } from './agent'; -import { defaultSignature } from './signature'; +import { defaultEngineReportingSignature } from 'apollo-graphql'; import { GraphQLRequestContext } from 'apollo-server-core/dist/requestPipelineAPI'; const clientNameHeaderKey = 'apollographql-client-name'; @@ -214,7 +214,7 @@ export class EngineReportingExtension let signature; if (this.documentAST) { const calculateSignature = - this.options.calculateSignature || defaultSignature; + this.options.calculateSignature || defaultEngineReportingSignature; signature = calculateSignature(this.documentAST, operationName); } else if (this.queryString) { // We didn't get an AST, possibly because of a parse failure. Let's just diff --git a/packages/apollo-engine-reporting/src/index.ts b/packages/apollo-engine-reporting/src/index.ts index 6fc476833b0..1f7d07fbb74 100644 --- a/packages/apollo-engine-reporting/src/index.ts +++ b/packages/apollo-engine-reporting/src/index.ts @@ -1,10 +1 @@ -export { - hideLiterals, - dropUnusedDefinitions, - sortAST, - removeAliases, - printWithReducedWhitespace, - defaultSignature, -} from './signature'; - export { EngineReportingOptions, EngineReportingAgent } from './agent'; diff --git a/packages/apollo-graphql/package.json b/packages/apollo-graphql/package.json index 517ea163531..a703b273283 100644 --- a/packages/apollo-graphql/package.json +++ b/packages/apollo-graphql/package.json @@ -10,6 +10,9 @@ "engines": { "node": ">=6" }, + "dependencies": { + "lodash": "^4.17.10" + }, "devDependencies": {}, "peerDependencies": { "graphql": "^14.0.0" diff --git a/packages/apollo-graphql/src/index.ts b/packages/apollo-graphql/src/index.ts index e69de29bb2d..bfff26d0b08 100644 --- a/packages/apollo-graphql/src/index.ts +++ b/packages/apollo-graphql/src/index.ts @@ -0,0 +1 @@ +export { defaultEngineReportingSignature } from './signature'; \ No newline at end of file diff --git a/packages/apollo-graphql/src/signature.ts b/packages/apollo-graphql/src/signature.ts index 64ca5389d17..6fa6f6d28c3 100644 --- a/packages/apollo-graphql/src/signature.ts +++ b/packages/apollo-graphql/src/signature.ts @@ -230,7 +230,7 @@ export function printWithReducedWhitespace(ast: DocumentNode): string { // The default signature function consists of removing unused definitions // and whitespace. // XXX consider caching somehow -export function defaultSignature( +export function defaultEngineReportingSignature( ast: DocumentNode, operationName: string, ): string { From 96de2d71d7055709d0c92d41c10101b777f48ed8 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 1 Feb 2019 18:14:30 +0200 Subject: [PATCH 05/11] Move Engine signature AST traversals/transforms into `./transforms` module. These AST visitors and transformations are more generally usable for other purposes rather than just the Apollo Engine signature reporting and would seem to belong in a module of their own. --- .../src/__tests__/signature.test.ts | 72 +------ .../src/__tests__/transforms.test.ts | 77 +++++++ packages/apollo-graphql/src/index.ts | 2 +- packages/apollo-graphql/src/signature.ts | 194 +---------------- packages/apollo-graphql/src/transforms.ts | 195 ++++++++++++++++++ 5 files changed, 284 insertions(+), 256 deletions(-) create mode 100644 packages/apollo-graphql/src/__tests__/transforms.test.ts create mode 100644 packages/apollo-graphql/src/transforms.ts diff --git a/packages/apollo-graphql/src/__tests__/signature.test.ts b/packages/apollo-graphql/src/__tests__/signature.test.ts index 7e3e7314480..161e07ef30f 100644 --- a/packages/apollo-graphql/src/__tests__/signature.test.ts +++ b/packages/apollo-graphql/src/__tests__/signature.test.ts @@ -7,82 +7,12 @@ import { dropUnusedDefinitions, sortAST, removeAliases, -} from '../signature'; +} from '../transforms'; // The gql duplicate fragment warning feature really is just warnings; nothing // breaks if you turn it off in tests. disableFragmentWarnings(); -describe('printWithReducedWhitespace', () => { - const cases = [ - { - name: 'lots of whitespace', - // Note: there's a tab after "tab->", which prettier wants to keep as a - // literal tab rather than \t. In the output, there should be a literal - // backslash-t. - input: gql` - query Foo($a: Int) { - user( - name: " tab-> yay" - other: """ - apple - bag - cat - """ - ) { - name - } - } - `, - output: - 'query Foo($a:Int){user(name:" tab->\\tyay",other:"apple\\n bag\\ncat"){name}}', - }, - ]; - cases.forEach(({ name, input, output }) => { - test(name, () => { - expect(printWithReducedWhitespace(input)).toEqual(output); - }); - }); -}); - -describe('hideLiterals', () => { - const cases = [ - { - name: 'full test', - input: gql` - query Foo($b: Int, $a: Boolean) { - user(name: "hello", age: 5) { - ...Bar - ... on User { - hello - bee - } - tz - aliased: name - } - } - - fragment Bar on User { - age @skip(if: $a) - ...Nested - } - - fragment Nested on User { - blah - } - `, - output: - 'query Foo($b:Int,$a:Boolean){user(name:"",age:0){...Bar...on User{hello bee}tz aliased:name}}' + - 'fragment Bar on User{age@skip(if:$a)...Nested}fragment Nested on User{blah}', - }, - ]; - cases.forEach(({ name, input, output }) => { - test(name, () => { - expect(printWithReducedWhitespace(hideLiterals(input))).toEqual(output); - }); - }); -}); - describe('aggressive signature', () => { function aggressive(ast: DocumentNode, operationName: string): string { return printWithReducedWhitespace( diff --git a/packages/apollo-graphql/src/__tests__/transforms.test.ts b/packages/apollo-graphql/src/__tests__/transforms.test.ts new file mode 100644 index 00000000000..e0ad5e471d3 --- /dev/null +++ b/packages/apollo-graphql/src/__tests__/transforms.test.ts @@ -0,0 +1,77 @@ +import { default as gql, disableFragmentWarnings } from 'graphql-tag'; + +import { printWithReducedWhitespace, hideLiterals } from '../transforms'; + +// The gql duplicate fragment warning feature really is just warnings; nothing +// breaks if you turn it off in tests. +disableFragmentWarnings(); + +describe('printWithReducedWhitespace', () => { + const cases = [ + { + name: 'lots of whitespace', + // Note: there's a tab after "tab->", which prettier wants to keep as a + // literal tab rather than \t. In the output, there should be a literal + // backslash-t. + input: gql` + query Foo($a: Int) { + user( + name: " tab-> yay" + other: """ + apple + bag + cat + """ + ) { + name + } + } + `, + output: + 'query Foo($a:Int){user(name:" tab->\\tyay",other:"apple\\n bag\\ncat"){name}}', + }, + ]; + cases.forEach(({ name, input, output }) => { + test(name, () => { + expect(printWithReducedWhitespace(input)).toEqual(output); + }); + }); +}); + +describe('hideLiterals', () => { + const cases = [ + { + name: 'full test', + input: gql` + query Foo($b: Int, $a: Boolean) { + user(name: "hello", age: 5) { + ...Bar + ... on User { + hello + bee + } + tz + aliased: name + } + } + + fragment Bar on User { + age @skip(if: $a) + ...Nested + } + + fragment Nested on User { + blah + } + `, + output: + 'query Foo($b:Int,$a:Boolean){user(name:"",age:0){...Bar...on User{hello bee}tz aliased:name}}' + + 'fragment Bar on User{age@skip(if:$a)...Nested}fragment Nested on User{blah}', + }, + ]; + cases.forEach(({ name, input, output }) => { + test(name, () => { + expect(printWithReducedWhitespace(hideLiterals(input))).toEqual(output); + }); + }); +}); diff --git a/packages/apollo-graphql/src/index.ts b/packages/apollo-graphql/src/index.ts index bfff26d0b08..8161d6647f0 100644 --- a/packages/apollo-graphql/src/index.ts +++ b/packages/apollo-graphql/src/index.ts @@ -1 +1 @@ -export { defaultEngineReportingSignature } from './signature'; \ No newline at end of file +export { defaultEngineReportingSignature } from './signature'; diff --git a/packages/apollo-graphql/src/signature.ts b/packages/apollo-graphql/src/signature.ts index 6fa6f6d28c3..8ddc71fe9f2 100644 --- a/packages/apollo-graphql/src/signature.ts +++ b/packages/apollo-graphql/src/signature.ts @@ -1,5 +1,3 @@ -// XXX maybe this should just be its own graphql-signature package - // In Engine, 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 @@ -16,9 +14,9 @@ // valid GraphQL query, though as of now the Engine servers do not re-parse your // signature and do not expect it to match the execution tree in the trace. // -// This file provides several useful building blocks for writing your own -// signature function. These are: -// +// This module utilizes several AST transformations from the adjacent +// 'transforms' module (which are also for writing your own signature method). + // - 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 @@ -46,186 +44,14 @@ // algorithm on it, and the details of the signature algorithm are now up to the // reporting agent. -import { sortBy, ListIteratee } from 'lodash'; - +import { DocumentNode } from 'graphql'; import { - print, - visit, - DocumentNode, - OperationDefinitionNode, - SelectionSetNode, - FieldNode, - FragmentSpreadNode, - InlineFragmentNode, - FragmentDefinitionNode, - DirectiveNode, - IntValueNode, - FloatValueNode, - StringValueNode, - ListValueNode, - ObjectValueNode, - separateOperations, -} from 'graphql'; - -// 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 Engine servers, but in general -// avoiding those situations is better than working around them. -export 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.) -export 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; -} - -// Like lodash's sortBy, but sorted(undefined) === undefined rather than []. It -// is a stable non-in-place sort. -function sorted( - items: ReadonlyArray | undefined, - ...iteratees: Array> -): Array | undefined { - if (items) { - return sortBy(items, ...iteratees); - } - return undefined; -} - -// 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. -export function sortAST(ast: DocumentNode): DocumentNode { - return visit(ast, { - 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') }; - }, - }); -} - -// 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. -export function removeAliases(ast: DocumentNode): DocumentNode { - return visit(ast, { - Field(node: FieldNode): FieldNode { - return { - ...node, - alias: undefined, - }; - }, - }); -} - -// 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. -export function printWithReducedWhitespace(ast: DocumentNode): string { - // 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. - - 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')), - ); -} + printWithReducedWhitespace, + dropUnusedDefinitions, + removeAliases, + sortAST, + hideLiterals, +} from './transforms'; // The default signature function consists of removing unused definitions // and whitespace. diff --git a/packages/apollo-graphql/src/transforms.ts b/packages/apollo-graphql/src/transforms.ts new file mode 100644 index 00000000000..77759db0901 --- /dev/null +++ b/packages/apollo-graphql/src/transforms.ts @@ -0,0 +1,195 @@ +import { visit } from 'graphql/language/visitor'; +import { + DocumentNode, + FloatValueNode, + IntValueNode, + StringValueNode, + OperationDefinitionNode, + SelectionSetNode, + FragmentSpreadNode, + InlineFragmentNode, + DirectiveNode, + FieldNode, + FragmentDefinitionNode, + ObjectValueNode, + ListValueNode, +} from 'graphql/language/ast'; +import { print } from 'graphql/language/printer'; +import { separateOperations } from 'graphql/utilities'; +import { sortBy, ListIteratee } from 'lodash'; + +// 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 Engine servers, but in general +// avoiding those situations is better than working around them. +export 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: [] }; + }, + }); +} + +// In the same spirit as the similarly named `hideLiterals` function, only +// hide string and numeric literals. +export function hideStringAndNumericLiterals(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 }; + }, + }); +} + +// 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.) +export 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; +} + +// Like lodash's sortBy, but sorted(undefined) === undefined rather than []. It +// is a stable non-in-place sort. +function sorted( + items: ReadonlyArray | undefined, + ...iteratees: Array> +): Array | undefined { + if (items) { + return sortBy(items, ...iteratees); + } + return undefined; +} + +// 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. +export function sortAST(ast: DocumentNode): DocumentNode { + return visit(ast, { + 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') }; + }, + }); +} + +// 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. +export function removeAliases(ast: DocumentNode): DocumentNode { + return visit(ast, { + Field(node: FieldNode): FieldNode { + return { + ...node, + alias: undefined, + }; + }, + }); +} + +// 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. +export function printWithReducedWhitespace(ast: DocumentNode): string { + // 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. + + 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')), + ); +} From 21c274870f37570016213dea804be0568039fbb3 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 1 Feb 2019 19:16:48 +0200 Subject: [PATCH 06/11] Use `lodash.sortby` modularly, rather than all of `lodash`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, the only place that we use `lodash` in the entire `apollo-server` repository is to utilize the `sortBy` function in this signature generation. Looking at the bundle stats, it appears that lodash represents 7.1% of the `apollo-server` package. We're a server, so bundle size is generally less of a concern, but it's still not to be ignored, particularly as we move into worker environments. More pressingly though, since this package will be utilized by the `apollo` CLI, we'll be shaving precious download time off the invocation of `npx apollo` if we can get this down. By switching to the modular package (but still depending on `@types/lodash` for _just_ the `ListIteratee` type — which we only need in development — we should be able to trim 55.4kB minified (19.1kB minified+gzip'd) off the `apollo-server` build. cc @trevor-scheer @jbaxleyiii @martijnwalraven --- package-lock.json | 14 +++++++++++--- package.json | 1 + packages/apollo-graphql/package.json | 2 +- packages/apollo-graphql/src/transforms.ts | 6 +++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index deb566a14a6..3fef81fa001 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1428,6 +1428,15 @@ "integrity": "sha512-jQ21kQ120mo+IrDs1nFNVm/AsdFxIx2+vZ347DbogHJPd/JzKNMOqU6HCYin1W6v8l5R9XSO2/e9cxmn7HAnVw==", "dev": true }, + "@types/lodash.sortby": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/@types/lodash.sortby/-/lodash.sortby-4.7.4.tgz", + "integrity": "sha512-Byy/JXUl7VCKOjqk2XyOEa4kRp2UBuPPkdQpIwSi+54t3KDa1vkIRU+qFEoWZMLcMUbBq8+Iy8Ybri8AqFYLTA==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", @@ -2080,7 +2089,7 @@ "apollo-graphql": { "version": "file:packages/apollo-graphql", "requires": { - "lodash": "^4.17.10" + "lodash.sortby": "^4.7.0" } }, "apollo-link": { @@ -9403,8 +9412,7 @@ "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" }, "lodash.template": { "version": "4.4.0", diff --git a/package.json b/package.json index 15393355eca..ec94f902f5f 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@types/koa-multer": "1.0.0", "@types/koa-router": "7.0.39", "@types/lodash": "4.14.120", + "@types/lodash.sortby": "4.7.4", "@types/lru-cache": "4.1.1", "@types/memcached": "2.2.5", "@types/micro": "7.3.3", diff --git a/packages/apollo-graphql/package.json b/packages/apollo-graphql/package.json index a703b273283..8c70e35a2ca 100644 --- a/packages/apollo-graphql/package.json +++ b/packages/apollo-graphql/package.json @@ -11,7 +11,7 @@ "node": ">=6" }, "dependencies": { - "lodash": "^4.17.10" + "lodash.sortby": "^4.7.0" }, "devDependencies": {}, "peerDependencies": { diff --git a/packages/apollo-graphql/src/transforms.ts b/packages/apollo-graphql/src/transforms.ts index 77759db0901..c8565f5c37f 100644 --- a/packages/apollo-graphql/src/transforms.ts +++ b/packages/apollo-graphql/src/transforms.ts @@ -16,7 +16,11 @@ import { } from 'graphql/language/ast'; import { print } from 'graphql/language/printer'; import { separateOperations } from 'graphql/utilities'; -import { sortBy, ListIteratee } from 'lodash'; +// We'll only fetch the `ListIteratee` type from the `@types/lodash`, but get +// `sortBy` from the modularized version of the package to avoid bringing in +// all of `lodash`. +import { ListIteratee } from 'lodash'; +import sortBy from 'lodash.sortby'; // Replace numeric, string, list, and object literals with "empty" // values. Leaves enums alone (since there's no consistent "zero" enum). This From 9eca8213459e7475b5f3edb054ed2e6fd5095059 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 1 Feb 2019 19:50:43 +0200 Subject: [PATCH 07/11] Bump the major version of `apollo-engine-reporting`. --- packages/apollo-engine-reporting/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index fd723338cf6..efee5abfa13 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -1,6 +1,6 @@ { "name": "apollo-engine-reporting", - "version": "0.2.2", + "version": "1.0.0", "description": "Send reports about your GraphQL services to Apollo Engine", "main": "./dist/index.js", "types": "./dist/index.d.ts", From b3758d8e477d54074793f49a5d421e9a33ef0556 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 1 Feb 2019 19:52:17 +0200 Subject: [PATCH 08/11] Update `apollo-engine-reporting`'s `CHANGELOG.md` for a major breaking change. --- packages/apollo-engine-reporting/CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/apollo-engine-reporting/CHANGELOG.md b/packages/apollo-engine-reporting/CHANGELOG.md index 70a5527e966..cb79cc96467 100644 --- a/packages/apollo-engine-reporting/CHANGELOG.md +++ b/packages/apollo-engine-reporting/CHANGELOG.md @@ -1,4 +1,10 @@ ### vNext -* Initial release. +# v1.0.0 + +* The signature functions which were previously exported from this package's + main module have been removed from `apollo-engine-reporting` and + moved to the `apollo-graphql` package. They should be more universally + helpful in that library, and should avoid tooling which needs to use them + from needing to bring in all of `apollo-server-core`. From d4a5cad2d137f568784e175e25086b6ab3103891 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 1 Feb 2019 20:10:10 +0200 Subject: [PATCH 09/11] Also update `jest.config.base.js` RegExp with new `apollo-graphql` package. --- jest.config.base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.base.js b/jest.config.base.js index 328505f1da7..45592377f7e 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -21,7 +21,7 @@ module.exports = { // We don't want to match `apollo-server-env` and // `apollo-engine-reporting-protobuf`, because these don't depend on // compilation but need to be initialized from as parto of `prepare`. - '^(?!apollo-server-env|apollo-engine-reporting-protobuf)(apollo-(?:server|datasource|cache-control|tracing|engine)[^/]*|graphql-extensions)(?:/dist)?((?:/.*)|$)': '/../../packages/$1/src$2' + '^(?!apollo-server-env|apollo-engine-reporting-protobuf)(apollo-(?:server|graphql|datasource|cache-control|tracing|engine)[^/]*|graphql-extensions)(?:/dist)?((?:/.*)|$)': '/../../packages/$1/src$2' }, clearMocks: true, globals: { From 59d5a5088af5e376290ce02bcd14d2e2a35f8962 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 1 Feb 2019 20:11:48 +0200 Subject: [PATCH 10/11] Add `apollo-graphql` as a project reference to `apollo-engine-reporting`. --- packages/apollo-engine-reporting/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/apollo-engine-reporting/tsconfig.json b/packages/apollo-engine-reporting/tsconfig.json index c226f290c52..07e4625888e 100644 --- a/packages/apollo-engine-reporting/tsconfig.json +++ b/packages/apollo-engine-reporting/tsconfig.json @@ -8,6 +8,7 @@ "exclude": ["**/__tests__", "**/__mocks__"], "references": [ { "path": "../graphql-extensions" }, + { "path": "../apollo-graphql" }, { "path": "../apollo-server-core/tsconfig.requestPipelineAPI.json" } ] } From e84aca256c76379e4d2ccecf86827671f2f601a2 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Fri, 1 Feb 2019 20:37:54 +0200 Subject: [PATCH 11/11] Update `apollo-server.md` to reflect new location of signature reference. --- docs/source/api/apollo-server.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/api/apollo-server.md b/docs/source/api/apollo-server.md index f7edfa336ef..54cc0372094 100644 --- a/docs/source/api/apollo-server.md +++ b/docs/source/api/apollo-server.md @@ -303,8 +303,10 @@ addMockFunctionsToSchema({ * `calculateSignature`: (ast: DocumentNode, operationName: string) => string - Specify the function for creating a signature for a query. See signature.ts - for details. + Specify the function for creating a signature for a query. + + > See [`apollo-graphql`'s `signature.ts`](https://npm.im/apollo-graphql) + > for more information on how the default signature is generated. * `reportIntervalMs`: number