Merge branch 'master' into re-stage-pr-1971

This commit is contained in:
Jesse Rosenberger 2019-02-14 15:01:00 +02:00
commit 8ac2884cfa
No known key found for this signature in database
GPG key ID: C0CCCF81AA6C08D8
45 changed files with 4524 additions and 1971 deletions

View file

@ -2,11 +2,22 @@
### vNEXT
- Move `apollo-graphql` package to the `apollo-tooling` repository [PR #2316](https://github.com/apollographql/apollo-server/pull/2316)
### v2.4.1
- Fix inaccurate total duration in apollo-tracing [PR #2298](https://github.com/apollographql/apollo-server/pull/2298)
- Avoid importing entire `crypto` dependency tree if not in Node.js. [PR #2304](https://github.com/apollographql/apollo-server/pull/2304)
- Allow passing `parseOptions` to `ApolloServerBase` constructor. [PR #2289](https://github.com/apollographql/apollo-server/pull/2289)
- Rename `azureFunctions.d.ts` to `azureFunctions.ts`. [PR #2287](https://github.com/apollographql/apollo-server/pull/2287)
- Require `apollo-engine-reporting` only if `EngineReportingAgent` used. [PR #2305](https://github.com/apollographql/apollo-server/pull/2305)
### v2.4.0
- Implement an in-memory cache store to save parsed and validated documents and provide performance benefits for repeat executions of the same document. [PR #2111](https://github.com/apollographql/apollo-server/pull/2111) (`>=2.4.0-alpha.0`)
- Fix: Serialize arrays as JSON on fetch in `RESTDataSource`. [PR #2219](https://github.com/apollographql/apollo-server/pull/2219)
- Fix: The `privateHeaders` configuration for `apollo-engine-reporting` now allows headers to be specified using any case and lower-cases them prior to comparison. [PR #2276](https://github.com/apollographql/apollo-server/pull/2276)
- Fix broken `apollo-server-azure-functions` TypeScript definitions. [PR #2287](https://github.com/apollographql/apollo-server/pull/2287)
### v2.3.3

View file

@ -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|graphql|datasource|cache-control|tracing|engine)[^/]*|graphql-extensions)(?:/dist)?((?:/.*)|$)': '<rootDir>/../../packages/$1/src$2'
'^(?!apollo-server-env|apollo-engine-reporting-protobuf)(apollo-(?:server|datasource|cache-control|tracing|engine)[^/]*|graphql-extensions)(?:/dist)?((?:/.*)|$)': '<rootDir>/../../packages/$1/src$2'
},
clearMocks: true,
globals: {

5620
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -34,13 +34,12 @@
"node": ">=6"
},
"dependencies": {
"@apollographql/apollo-tools": "^0.3.0",
"@apollographql/apollo-tools": "^0.3.3",
"apollo-cache-control": "file:packages/apollo-cache-control",
"apollo-datasource": "file:packages/apollo-datasource",
"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",
@ -65,24 +64,24 @@
},
"devDependencies": {
"@types/async-retry": "1.2.1",
"@types/aws-lambda": "8.10.19",
"@types/aws-lambda": "8.10.18",
"@types/body-parser": "1.17.0",
"@types/connect": "3.4.32",
"@types/fast-json-stable-stringify": "2.0.0",
"@types/fibers": "0.0.30",
"@types/graphql": "14.0.5",
"@types/graphql": "14.0.7",
"@types/hapi": "17.8.5",
"@types/jest": "23.3.14",
"@types/jest": "24.0.4",
"@types/koa-multer": "1.0.0",
"@types/koa-router": "7.0.39",
"@types/lodash": "4.14.120",
"@types/lodash.sortby": "4.7.4",
"@types/lodash": "4.14.121",
"@types/lodash.sortby": "4.7.5",
"@types/lru-cache": "4.1.1",
"@types/memcached": "2.2.5",
"@types/memcached": "2.2.6",
"@types/micro": "7.3.3",
"@types/multer": "1.3.7",
"@types/node": "10.12.23",
"@types/node-fetch": "2.1.5",
"@types/node": "10.12.26",
"@types/node-fetch": "2.1.6",
"@types/redis": "2.8.10",
"@types/request": "2.48.1",
"@types/request-promise": "4.1.42",
@ -94,7 +93,7 @@
"apollo-link-http": "1.5.11",
"apollo-link-persisted-queries": "0.2.2",
"body-parser": "1.18.3",
"codecov": "3.1.0",
"codecov": "3.2.0",
"connect": "3.6.6",
"express": "4.16.4",
"fastify": "1.13.0",
@ -106,14 +105,14 @@
"graphql-tools": "4.0.4",
"hapi": "17.8.4",
"husky": "1.3.1",
"jest": "23.6.0",
"jest": "24.1.0",
"jest-junit": "5.2.0",
"jest-matcher-utils": "23.6.0",
"jest-matcher-utils": "24.0.0",
"js-sha256": "0.9.0",
"koa": "2.7.0",
"koa-multer": "1.0.2",
"lerna": "3.11.0",
"lint-staged": "8.1.3",
"lerna": "3.11.1",
"lint-staged": "8.1.4",
"memcached-mock": "0.1.0",
"meteor-promise": "0.8.7",
"mock-req": "0.2.0",

View file

@ -1,6 +1,6 @@
{
"name": "apollo-engine-reporting",
"version": "1.0.0",
"version": "1.0.1",
"description": "Send reports about your GraphQL services to Apollo Engine",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@ -12,10 +12,10 @@
},
"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",
"graphql-extensions": "file:../graphql-extensions",
"async-retry": "^1.2.1",
"graphql-extensions": "file:../graphql-extensions"
"apollo-graphql": "^0.1.0"
}
}

View file

@ -8,7 +8,6 @@
"exclude": ["**/__tests__", "**/__mocks__"],
"references": [
{ "path": "../graphql-extensions" },
{ "path": "../apollo-graphql" },
{ "path": "../apollo-server-core/tsconfig.requestPipelineAPI.json" }
]
}

View file

@ -1,6 +0,0 @@
*
!src/**/*
!dist/**/*
dist/**/*.test.*
!package.json
!README.md

View file

@ -1,4 +0,0 @@
# Change Log
### vNEXT

View file

@ -1 +0,0 @@
# `apollo-graphql`

View file

@ -1,19 +0,0 @@
{
"name": "apollo-graphql",
"version": "0.1.0",
"description": "Apollo GraphQL utility library",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"keywords": [],
"author": "Apollo <opensource@apollographql.com>",
"license": "MIT",
"engines": {
"node": ">=6"
},
"dependencies": {
"lodash.sortby": "^4.7.0"
},
"peerDependencies": {
"graphql": "^14.0.0"
}
}

View file

@ -1,148 +0,0 @@
import { DocumentNode } from 'graphql';
import { default as gql, disableFragmentWarnings } from 'graphql-tag';
import {
printWithReducedWhitespace,
hideLiterals,
dropUnusedDefinitions,
sortAST,
removeAliases,
} from '../transforms';
// The gql duplicate fragment warning feature really is just warnings; nothing
// breaks if you turn it off in tests.
disableFragmentWarnings();
describe('aggressive signature', () => {
function aggressive(ast: DocumentNode, operationName: string): string {
return printWithReducedWhitespace(
removeAliases(
hideLiterals(sortAST(dropUnusedDefinitions(ast, operationName))),
),
);
}
const cases = [
// Test cases borrowed from optics-agent-js.
{
name: 'basic test',
operationName: '',
input: gql`
{
user {
name
}
}
`,
output: '{user{name}}',
},
{
name: 'basic test with query',
operationName: '',
input: gql`
query {
user {
name
}
}
`,
output: '{user{name}}',
},
{
name: 'basic with operation name',
operationName: 'OpName',
input: gql`
query OpName {
user {
name
}
}
`,
output: '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" })
}
}
`,
output: 'query OpName{user{name(apple:[],bag:{},cat:ENUM_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)
}
}
`,
output:
'query OpName($a:[[Boolean!]!],$b:EnumType,$c:Int!){user{name(apple:$a,bag:$b,cat:$c)}}',
},
{
name: 'fragment',
operationName: '',
input: gql`
{
user {
name
...Bar
}
}
fragment Bar on User {
asd
}
fragment Baz on User {
jkl
}
`,
output: '{user{name...Bar}}fragment Bar on User{asd}',
},
{
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
}
`,
output:
'query Foo($a:Boolean,$b:Int){user(age:0,name:""){name tz...Bar...on User{bee hello}}}' +
'fragment Bar on User{age@skip(if:$a)...Nested}fragment Nested on User{blah}',
},
];
cases.forEach(({ name, operationName, input, output }) => {
test(name, () => {
expect(aggressive(input, operationName)).toEqual(output);
});
});
});

View file

@ -1,77 +0,0 @@
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);
});
});
});

View file

@ -1 +0,0 @@
export { defaultEngineReportingSignature } from './signature';

View file

@ -1,68 +0,0 @@
// 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
// to Apollo Engine's servers, either because it contains sensitive data or
// because it contains extraneous operations or fragments?
//
// To solve these problems, EngineReportingAgent has the concept of
// "signatures". We don't (by default) send the full query string of queries to
// the Engine servers. Instead, each trace has its query string's "signature".
//
// You can specify any function mapping a GraphQL query AST (DocumentNode) to
// string as your signature algorithm by providing it as the 'signature' option
// to the EngineReportingAgent constructor. Ideally, your signature should be a
// 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 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
// 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
//
// defaultSignature 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 the Engine 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 Engine UI wasn't the same as the one actually
// sent. apollo-engine-reporting uses a new 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.
import { DocumentNode } from 'graphql';
import {
printWithReducedWhitespace,
dropUnusedDefinitions,
removeAliases,
sortAST,
hideLiterals,
} from './transforms';
// The default signature function consists of removing unused definitions
// and whitespace.
// XXX consider caching somehow
export function defaultEngineReportingSignature(
ast: DocumentNode,
operationName: string,
): string {
return printWithReducedWhitespace(
sortAST(
removeAliases(hideLiterals(dropUnusedDefinitions(ast, operationName))),
),
);
}

View file

@ -1,199 +0,0 @@
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';
// 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
// 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<T>(
items: ReadonlyArray<T> | undefined,
...iteratees: Array<ListIteratee<T>>
): Array<T> | 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')),
);
}

View file

@ -1,10 +0,0 @@
{
"extends": "../../tsconfig.base",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["**/__tests__", "**/__mocks__"],
"references": []
}

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-azure-functions",
"version": "2.4.0",
"version": "2.4.1",
"description": "Production-ready Node.js GraphQL server for Azure Functions",
"keywords": [
"GraphQL",

View file

@ -2,6 +2,7 @@
"extends": "../../../../tsconfig.test.base",
"include": ["**/*"],
"references": [
{ "path": "../../" }
{ "path": "../../" },
{ "path": "../../../apollo-server-integration-testsuite" },
]
}
}

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-cloud-functions",
"version": "2.4.0",
"version": "2.4.1",
"description": "Production-ready Node.js GraphQL server for Google Cloud Functions",
"keywords": [
"GraphQL",

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-cloudflare",
"version": "2.4.0",
"version": "2.4.1",
"description": "Production-ready Node.js GraphQL server for Cloudflare workers",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-core",
"version": "2.4.0",
"version": "2.4.1",
"description": "Core engine for Apollo GraphQL server",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -24,7 +24,7 @@
"node": ">=6"
},
"dependencies": {
"@apollographql/apollo-tools": "^0.3.0",
"@apollographql/apollo-tools": "^0.3.3",
"@apollographql/graphql-playground-html": "^1.6.6",
"@types/ws": "^6.0.0",
"apollo-cache-control": "file:../apollo-cache-control",
@ -41,7 +41,7 @@
"graphql-tag": "^2.9.2",
"graphql-tools": "^4.0.0",
"graphql-upload": "^8.0.2",
"lodash": "^4.17.10",
"sha.js": "^2.4.11",
"subscriptions-transport-ws": "^0.9.11",
"ws": "^6.0.0"
},

View file

@ -1,4 +1,8 @@
import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
import {
makeExecutableSchema,
addMockFunctionsToSchema,
GraphQLParseOptions,
} from 'graphql-tools';
import { Server as HttpServer } from 'http';
import {
execute,
@ -12,7 +16,6 @@ import {
DocumentNode,
} from 'graphql';
import { GraphQLExtension } from 'graphql-extensions';
import { EngineReportingAgent } from 'apollo-engine-reporting';
import { InMemoryLRUCache } from 'apollo-server-caching';
import { ApolloServerPlugin } from 'apollo-server-plugin-base';
import runtimeSupportsUploads from './utils/runtimeSupportsUploads';
@ -103,7 +106,7 @@ export class ApolloServerBase {
public requestOptions: Partial<GraphQLOptions<any>> = Object.create(null);
private context?: Context | ContextFunction;
private engineReportingAgent?: EngineReportingAgent;
private engineReportingAgent?: import('apollo-engine-reporting').EngineReportingAgent;
private engineServiceId?: string;
private extensions: Array<() => GraphQLExtension>;
private schemaHash: string;
@ -124,6 +127,8 @@ export class ApolloServerBase {
// on the same operation to be executed immediately.
private documentStore?: InMemoryLRUCache<DocumentNode>;
private parseOptions: GraphQLParseOptions;
// The constructor should be universal across all environments. All environment specific behavior should be set by adding or overriding methods
constructor(config: Config) {
if (!config) throw new Error('ApolloServer requires options.');
@ -134,6 +139,7 @@ export class ApolloServerBase {
schemaDirectives,
modules,
typeDefs,
parseOptions = {},
introspection,
mocks,
mockEntireSchema,
@ -292,9 +298,12 @@ export class ApolloServerBase {
typeDefs: augmentedTypeDefs,
schemaDirectives,
resolvers,
parseOptions,
});
}
this.parseOptions = parseOptions;
if (mocks || (typeof mockEntireSchema !== 'undefined' && mocks !== false)) {
addMockFunctionsToSchema({
schema: this.schema,
@ -333,6 +342,7 @@ export class ApolloServerBase {
this.engineServiceId = getEngineServiceId(engine);
if (this.engineServiceId) {
const { EngineReportingAgent } = require('apollo-engine-reporting');
this.engineReportingAgent = new EngineReportingAgent(
typeof engine === 'object' ? engine : Object.create(null),
{
@ -546,6 +556,7 @@ export class ApolloServerBase {
any,
any
>,
parseOptions: this.parseOptions,
...this.requestOptions,
} as GraphQLOptions;
}

View file

@ -9,6 +9,7 @@ import { CacheControlExtensionOptions } from 'apollo-cache-control';
import { KeyValueCache, InMemoryLRUCache } from 'apollo-server-caching';
import { DataSource } from 'apollo-datasource';
import { ApolloServerPlugin } from 'apollo-server-plugin-base';
import { GraphQLParseOptions } from 'graphql-tools';
/*
* GraphQLServerOptions
@ -22,6 +23,7 @@ import { ApolloServerPlugin } from 'apollo-server-plugin-base';
* - (optional) fieldResolver: a custom default field resolver
* - (optional) debug: a boolean that will print additional debug logging if execution errors occur
* - (optional) extensions: an array of functions which create GraphQLExtensions (each GraphQLExtension object is used for one request)
* - (optional) parseOptions: options to pass when parsing schemas and queries
*
*/
export interface GraphQLServerOptions<
@ -44,6 +46,7 @@ export interface GraphQLServerOptions<
persistedQueries?: PersistedQueryOptions;
plugins?: ApolloServerPlugin[];
documentStore?: InMemoryLRUCache<DocumentNode>;
parseOptions?: GraphQLParseOptions;
}
export type DataSources<TContext> = {

View file

@ -29,7 +29,6 @@ import {
PersistedQueryNotSupportedError,
PersistedQueryNotFoundError,
} from 'apollo-server-errors';
import { createHash } from 'crypto';
import {
GraphQLRequest,
GraphQLResponse,
@ -45,6 +44,7 @@ import {
import { Dispatcher } from './utils/dispatcher';
import { InMemoryLRUCache, KeyValueCache } from 'apollo-server-caching';
import { GraphQLParseOptions } from 'graphql-tools';
export {
GraphQLRequest,
@ -53,8 +53,10 @@ export {
InvalidGraphQLRequestError,
};
import createSHA from './utils/createSHA';
function computeQueryHash(query: string) {
return createHash('sha256')
return createSHA('sha256')
.update(query)
.digest('hex');
}
@ -78,6 +80,8 @@ export interface GraphQLRequestPipelineConfig<TContext> {
plugins?: ApolloServerPlugin[];
documentStore?: InMemoryLRUCache<DocumentNode>;
parseOptions?: GraphQLParseOptions;
}
export type DataSources<TContext> = {
@ -194,7 +198,7 @@ export async function processGraphQLRequest<TContext>(
);
try {
requestContext.document = parse(query);
requestContext.document = parse(query, config.parseOptions);
parsingDidEnd();
} catch (syntaxError) {
parsingDidEnd(syntaxError);
@ -306,13 +310,16 @@ export async function processGraphQLRequest<TContext>(
requestDidEnd();
}
function parse(query: string): DocumentNode {
function parse(
query: string,
parseOptions?: GraphQLParseOptions,
): DocumentNode {
const parsingDidEnd = extensionStack.parsingDidStart({
queryString: query,
});
try {
return graphql.parse(query);
return graphql.parse(query, parseOptions);
} finally {
parsingDidEnd();
}
@ -356,7 +363,7 @@ export async function processGraphQLRequest<TContext>(
});
try {
return graphql.execute(executionArgs);
return await graphql.execute(executionArgs);
} finally {
executionDidEnd();
}

View file

@ -1,5 +1,10 @@
import { GraphQLSchema, DocumentNode } from 'graphql';
import { SchemaDirectiveVisitor, IResolvers, IMocks } from 'graphql-tools';
import {
SchemaDirectiveVisitor,
IResolvers,
IMocks,
GraphQLParseOptions,
} from 'graphql-tools';
import { ConnectionContext } from 'subscriptions-transport-ws';
import WebSocket from 'ws';
import { GraphQLExtension } from 'graphql-extensions';
@ -42,23 +47,25 @@ export interface SubscriptionServerOptions {
onDisconnect?: (websocket: WebSocket, context: ConnectionContext) => any;
}
type BaseConfig = Pick<
GraphQLOptions<Context<any>>,
| 'formatError'
| 'debug'
| 'rootValue'
| 'validationRules'
| 'formatResponse'
| 'fieldResolver'
| 'tracing'
| 'dataSources'
| 'cache'
>;
// This configuration is shared between all integrations and should include
// fields that are not specific to a single integration
export interface Config
extends Pick<
GraphQLOptions<Context<any>>,
| 'formatError'
| 'debug'
| 'rootValue'
| 'validationRules'
| 'formatResponse'
| 'fieldResolver'
| 'tracing'
| 'dataSources'
| 'cache'
> {
export interface Config extends BaseConfig {
modules?: GraphQLSchemaModule[];
typeDefs?: DocumentNode | Array<DocumentNode>;
parseOptions?: GraphQLParseOptions;
resolvers?: IResolvers;
schema?: GraphQLSchema;
schemaDirectives?: Record<string, typeof SchemaDirectiveVisitor>;

View file

@ -0,0 +1,10 @@
import isNode from './isNode';
export default function(kind: string): import('crypto').Hash {
if (isNode) {
// Use module.require instead of just require to avoid bundling whatever
// crypto polyfills a non-Node bundler might fall back to.
return module.require('crypto').createHash(kind);
}
return require('sha.js')(kind);
}

View file

@ -0,0 +1,6 @@
export default typeof process === 'object' &&
process &&
process.release &&
process.release.name === 'node' &&
process.versions &&
typeof process.versions.node === 'string';

View file

@ -1,11 +1,7 @@
import isNode from './isNode';
const runtimeSupportsUploads = (() => {
if (
process &&
process.release &&
process.release.name === 'node' &&
process.versions &&
typeof process.versions.node === 'string'
) {
if (isNode) {
const [nodeMajor, nodeMinor] = process.versions.node
.split('.', 2)
.map(segment => parseInt(segment, 10));

View file

@ -1,44 +1,44 @@
import { parse } from 'graphql/language';
import { execute, ExecutionResult } from 'graphql/execution';
import { getIntrospectionQuery, IntrospectionSchema } from 'graphql/utilities';
import stableStringify from 'fast-json-stable-stringify';
import { GraphQLSchema } from 'graphql/type';
import { createHash } from 'crypto';
export function generateSchemaHash(schema: GraphQLSchema): string {
const introspectionQuery = getIntrospectionQuery();
const documentAST = parse(introspectionQuery);
const result = execute(schema, documentAST) as ExecutionResult;
// If the execution of an introspection query results in a then-able, it
// indicates that one or more of its resolvers is behaving in an asynchronous
// manner. This is not the expected behavior of a introspection query
// which does not have any asynchronous resolvers.
if (
result &&
typeof (result as PromiseLike<typeof result>).then === 'function'
) {
throw new Error(
[
'The introspection query is resolving asynchronously; execution of an introspection query is not expected to return a `Promise`.',
'',
'Wrapped type resolvers should maintain the existing execution dynamics of the resolvers they wrap (i.e. async vs sync) or introspection types should be excluded from wrapping by checking them with `graphql/type`s, `isIntrospectionType` predicate function prior to wrapping.',
].join('\n'),
);
}
if (!result || !result.data || !result.data.__schema) {
throw new Error('Unable to generate server introspection document.');
}
const introspectionSchema: IntrospectionSchema = result.data.__schema;
// It's important that we perform a deterministic stringification here
// since, depending on changes in the underlying `graphql-js` execution
// layer, varying orders of the properties in the introspection
const stringifiedSchema = stableStringify(introspectionSchema);
return createHash('sha512')
.update(stringifiedSchema)
.digest('hex');
}
import { parse } from 'graphql/language';
import { execute, ExecutionResult } from 'graphql/execution';
import { getIntrospectionQuery, IntrospectionSchema } from 'graphql/utilities';
import stableStringify from 'fast-json-stable-stringify';
import { GraphQLSchema } from 'graphql/type';
import createSHA from './createSHA';
export function generateSchemaHash(schema: GraphQLSchema): string {
const introspectionQuery = getIntrospectionQuery();
const documentAST = parse(introspectionQuery);
const result = execute(schema, documentAST) as ExecutionResult;
// If the execution of an introspection query results in a then-able, it
// indicates that one or more of its resolvers is behaving in an asynchronous
// manner. This is not the expected behavior of a introspection query
// which does not have any asynchronous resolvers.
if (
result &&
typeof (result as PromiseLike<typeof result>).then === 'function'
) {
throw new Error(
[
'The introspection query is resolving asynchronously; execution of an introspection query is not expected to return a `Promise`.',
'',
'Wrapped type resolvers should maintain the existing execution dynamics of the resolvers they wrap (i.e. async vs sync) or introspection types should be excluded from wrapping by checking them with `graphql/type`s, `isIntrospectionType` predicate function prior to wrapping.',
].join('\n'),
);
}
if (!result || !result.data || !result.data.__schema) {
throw new Error('Unable to generate server introspection document.');
}
const introspectionSchema: IntrospectionSchema = result.data.__schema;
// It's important that we perform a deterministic stringification here
// since, depending on changes in the underlying `graphql-js` execution
// layer, varying orders of the properties in the introspection
const stringifiedSchema = stableStringify(introspectionSchema);
return createSHA('sha512')
.update(stringifiedSchema)
.digest('hex');
}

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-express",
"version": "2.4.0",
"version": "2.4.1",
"description": "Production-ready Node.js GraphQL server for Express and Connect",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-hapi",
"version": "2.4.0",
"version": "2.4.1",
"description": "Production-ready Node.js GraphQL server for Hapi",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -1,7 +1,7 @@
{
"name": "apollo-server-integration-testsuite",
"private": true,
"version": "2.4.0",
"version": "2.4.1",
"description": "Apollo Server Integrations testsuite",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -37,6 +37,7 @@ import {
ApolloServerBase,
} from 'apollo-server-core';
import { GraphQLExtension, GraphQLResponse } from 'graphql-extensions';
import { TracingFormat } from 'apollo-tracing';
export function createServerInfo<AS extends ApolloServerBase>(
server: AS,
@ -1260,5 +1261,67 @@ export function testApolloServer<AS extends ApolloServerBase>(
}, done.fail);
});
});
describe('Tracing', () => {
const typeDefs = gql`
type Book {
title: String
author: String
}
type Movie {
title: String
}
type Query {
books: [Book]
movies: [Movie]
}
`;
const resolvers = {
Query: {
books: () =>
new Promise(resolve =>
setTimeout(() => resolve([{ title: 'H', author: 'J' }]), 10),
),
movies: () =>
new Promise(resolve =>
setTimeout(() => resolve([{ title: 'H' }]), 12),
),
},
};
it('reports a total duration that is longer than the duration of its resolvers', async () => {
const { url: uri } = await createApolloServer({
typeDefs,
resolvers,
tracing: true,
});
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({
query: `{ books { title author } }`,
});
const tracing: TracingFormat = result.extensions.tracing;
const earliestStartOffset = tracing.execution.resolvers
.map(resolver => resolver.startOffset)
.reduce((currentEarliestOffset, nextOffset) =>
Math.min(currentEarliestOffset, nextOffset),
);
const latestEndOffset = tracing.execution.resolvers
.map(resolver => resolver.startOffset + resolver.duration)
.reduce((currentLatestEndOffset, nextEndOffset) =>
Math.min(currentLatestEndOffset, nextEndOffset),
);
const resolverDuration = latestEndOffset - earliestStartOffset;
expect(resolverDuration).not.toBeGreaterThan(tracing.duration);
});
});
});
}

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-koa",
"version": "2.4.0",
"version": "2.4.1",
"description": "Production-ready Node.js GraphQL server for Koa",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-lambda",
"version": "2.4.0",
"version": "2.4.1",
"description": "Production-ready Node.js GraphQL server for AWS Lambda",
"keywords": [
"GraphQL",

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-micro",
"version": "2.4.0",
"version": "2.4.1",
"description": "Production-ready Node.js GraphQL server for Micro",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-plugin-base",
"version": "0.3.0",
"version": "0.3.1",
"description": "Apollo Server plugin base classes",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-testing",
"version": "2.4.0",
"version": "2.4.1",
"description": "Test utils for apollo-server",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server",
"version": "2.4.0",
"version": "2.4.1",
"description": "Production ready GraphQL Server",
"author": "opensource@apollographql.com",
"main": "dist/index.js",

View file

@ -1,6 +1,6 @@
{
"name": "graphql-extensions",
"version": "0.5.0",
"version": "0.5.1",
"description": "Add extensions to GraphQL servers",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@ -14,7 +14,7 @@
"node": ">=6.0"
},
"dependencies": {
"@apollographql/apollo-tools": "^0.3.0"
"@apollographql/apollo-tools": "^0.3.3"
},
"devDependencies": {
"apollo-server-core": "file:../apollo-server-core",

View file

@ -2,6 +2,7 @@
"extends": [
"apollo-open-source"
],
"automerge": false,
"packageRules": [
{
"paths": [

View file

@ -9,7 +9,6 @@
{ "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" },

View file

@ -8,7 +8,6 @@
{ "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__/" },