Merge branch 'master' into release-vNEXT

This commit is contained in:
Jesse Rosenberger 2018-12-18 19:44:06 +02:00
commit ccd974dbc6
No known key found for this signature in database
GPG key ID: C0CCCF81AA6C08D8
38 changed files with 386 additions and 291 deletions

View file

@ -1,5 +1,9 @@
# Changelog
### v2.3.1
- Provide types for `graphql-upload` in a location where they can be accessed by TypeScript consumers of `apollo-server` packages. [ccf935f9](https://github.com/apollographql/apollo-server/commit/ccf935f9) [Issue #2092](https://github.com/apollographql/apollo-server/issues/2092)
### v2.3.0
- **BREAKING FOR NODE.JS <= 8.5.0 ONLY**: To continue using Apollo Server 2.x in versions of Node.js prior to v8.5.0, file uploads must be disabled by setting `uploads: false` on the `ApolloServer` constructor options. Without explicitly disabling file-uploads, the server will `throw` at launch (with instructions and a link to our documentation).
@ -10,6 +14,8 @@
**We intend to drop support for Node.js 6.x in the next major version of Apollo Server.**
For more information, see [PR #2054](https://github.com/apollographql/apollo-server/pull/2054) and [our documentation](https://www.apollographql.com/docs/apollo-server/v2/migration-file-uploads.html).
### v2.2.7
- `apollo-engine-reporting`: When multiple instances of `apollo-engine-reporting` are loaded (an uncommon edge case), ensure that `encodedTraces` are handled only once rather than once per loaded instance. [PR #2040](https://github.com/apollographql/apollo-server/pull/2040)

View file

@ -16,18 +16,18 @@ const { gql } = require('apollo-server');
const { find, filter } = require('lodash');
const schema = gql`
type Book {
title: String
author: Author
}
type Book {
title: String
author: Author
}
type Author {
books: [Book]
}
type Author {
books: [Book]
}
type Query {
author: Author
}
type Query {
author: Author
}
`;
const resolvers = {

View file

@ -53,10 +53,11 @@ In order to apply this implementation to a schema that contains `@deprecated` di
const { ApolloServer, gql } = require("apollo-server");
const typeDefs = gql`
type ExampleType {
newField: String
oldField: String @deprecated(reason: "Use \`newField\`.")
}`;
type ExampleType {
newField: String
oldField: String @deprecated(reason: "Use \`newField\`.")
}
`;
const server = new ApolloServer({
typeDefs,
@ -96,11 +97,12 @@ const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
const { defaultFieldResolver } = require("graphql");
const typeDefs = gql`
directive @upper on FIELD_DEFINITION
directive @upper on FIELD_DEFINITION
type Query {
hello: String @upper
}`;
type Query {
hello: String @upper
}
`;
class UpperCaseDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
@ -138,11 +140,12 @@ Suppose you've defined an object type that corresponds to a [REST](https://en.wi
const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
const typeDefs = gql`
directive @rest(url: String) on FIELD_DEFINITION
directive @rest(url: String) on FIELD_DEFINITION
type Query {
people: [Person] @rest(url: "/api/v1/people")
}`;
type Query {
people: [Person] @rest(url: "/api/v1/people")
}
`;
class RestDirective extends SchemaDirectiveVisitor {
public visitFieldDefinition(field) {
@ -173,13 +176,14 @@ Suppose your resolver returns a `Date` object but you want to return a formatted
const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
const typeDefs = gql`
directive @date(format: String) on FIELD_DEFINITION
directive @date(format: String) on FIELD_DEFINITION
scalar Date
scalar Date
type Post {
published: Date @date(format: "mmmm d, yyyy")
}`;
type Post {
published: Date @date(format: "mmmm d, yyyy")
}
`;
class DateFormatDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
@ -214,15 +218,16 @@ const formatDate = require("dateformat");
const { defaultFieldResolver, GraphQLString } = require("graphql");
const typeDefs = gql`
directive @date(
defaultFormat: String = "mmmm d, yyyy"
) on FIELD_DEFINITION
directive @date(
defaultFormat: String = "mmmm d, yyyy"
) on FIELD_DEFINITION
scalar Date
scalar Date
type Query {
today: Date @date
}`;
type Query {
today: Date @date
}
`;
class FormattableDateDirective extends SchemaDirectiveVisitor {
public visitFieldDefinition(field) {
@ -292,11 +297,12 @@ Here's how you might make sure `translate` is used to localize the `greeting` fi
const { ApolloServer, gql, SchemaDirectiveVisitor } = require("apollo-server");
const typeDefs = gql`
directive @intl on FIELD_DEFINITION
directive @intl on FIELD_DEFINITION
type Query {
greeting: String @intl
}`;
type Query {
greeting: String @intl
}
`;
class IntlDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field, details) {
@ -519,27 +525,28 @@ const { GraphQLID } = require("graphql");
const { createHash } = require("crypto");
const typeDefs = gql`
directive @uniqueID(
# The name of the new ID field, "uid" by default:
name: String = "uid"
directive @uniqueID(
# The name of the new ID field, "uid" by default:
name: String = "uid"
# Which fields to include in the new ID:
from: [String] = ["id"]
) on OBJECT
# Which fields to include in the new ID:
from: [String] = ["id"]
) on OBJECT
# Since this type just uses the default values of name and from,
# we don't have to pass any arguments to the directive:
type Location @uniqueID {
id: Int
address: String
}
# Since this type just uses the default values of name and from,
# we don't have to pass any arguments to the directive:
type Location @uniqueID {
id: Int
address: String
}
# This type uses both the person's name and the personID field,
# in addition to the "Person" type name, to construct the ID:
type Person @uniqueID(from: ["name", "personID"]) {
personID: Int
name: String
}`;
# This type uses both the person's name and the personID field,
# in addition to the "Person" type name, to construct the ID:
type Person @uniqueID(from: ["name", "personID"]) {
personID: Int
name: String
}
`;
class UniqueIdDirective extends SchemaDirectiveVisitor {
visitObject(type) {

View file

@ -15,9 +15,9 @@ This example demonstrates mocking a GraphQL schema with just one line of code, u
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
type Query {
hello: String
}
type Query {
hello: String
}
`;
const server = new ApolloServer({
@ -32,7 +32,7 @@ server.listen().then(({ url }) => {
> Note: If `typeDefs` has custom scalar types, `resolvers` must still contain the `serialize`, `parseValue`, and `parseLiteral` functions
Mocking logic simply looks at the type definitions and returns a string where a string is expected, a number for a number, etc. This provides the right shape of result. By default, when using mocks, any existing resolvers are ignored. See the ["Using existing resolvers with mocks"](#existing-resolvers) section below for more info on how to change this behavior.
Mocking logic simply looks at the type definitions and returns a string where a string is expected, a number for a number, etc. This provides the right shape of result. By default, when using mocks, any existing resolvers are ignored. See the ["Using existing resolvers with mocks"](#existing-resolvers) section below for more info on how to change this behavior.
For more sophisticated testing, mocks can be customized to a particular data model.
@ -44,10 +44,10 @@ In addition to a boolean, `mocks` can be an object that describes custom mocking
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
type Query {
hello: String
resolved: String
}
type Query {
hello: String
resolved: String
}
`;
const resolvers = {
@ -149,16 +149,16 @@ For some more background and flavor on this approach, read the ["Mocking your se
The default behavior for mocks is to overwrite the resolvers already present in the schema. To keep the existing resolvers, set the `mockEntireSchema` option to false.
> Note: mocking resolvers will not work if the `mocks` option is `false`, even if `mockEntireSchema` is true.
> Note: mocking resolvers will not work if the `mocks` option is `false`, even if `mockEntireSchema` is true.
```js line=26
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
type Query {
hello: String
resolved: String
}
type Query {
hello: String
resolved: String
}
`;
const resolvers = {

View file

@ -36,17 +36,15 @@ const { ApolloServer, gql } = require('apollo-server');
const GraphQLJSON = require('graphql-type-json');
const schemaString = gql`
scalar JSON
scalar JSON
type Foo {
aField: JSON
}
type Query {
foo: Foo
}
type Foo {
aField: JSON
}
type Query {
foo: Foo
}
`;
const resolveFunctions = {
@ -92,17 +90,15 @@ const myCustomScalarType = new GraphQLScalarType({
});
const schemaString = gql`
scalar MyCustomScalar
scalar MyCustomScalar
type Foo {
aField: MyCustomScalar
}
type Query {
foo: Foo
}
type Foo {
aField: MyCustomScalar
}
type Query {
foo: Foo
}
`;
const resolverFunctions = {
@ -127,11 +123,12 @@ The goal is to define a `Date` data type for returning `Date` values from the da
The following is the implementation of the `Date` data type. First, the schema:
```js
const typeDefs = gql`scalar Date
const typeDefs = gql`
scalar Date
type MyType {
created: Date
}
type MyType {
created: Date
}
`
```
@ -172,11 +169,12 @@ server.listen().then(({ url }) => {
In this example, we follow the [official GraphQL documentation](http://graphql.org/docs/api-reference-type-system/) for the scalar datatype, which demonstrates how to validate a database field that should only contain odd numbers in GraphQL. First, the schema:
```js
const typeDefs = gql`scalar Odd
const typeDefs = gql`
scalar Odd
type MyType {
type MyType {
oddValue: Odd
}
}
`
```

View file

@ -21,19 +21,22 @@ Subscriptions are another root level type, similar to Query and Mutation. To sta
```js line=2-4
const typeDefs = gql`
type Subscription {
postAdded: Post
}
type Query {
posts: [Post]
}
type Mutation {
addPost(author: String, comment: String): Post
}
type Post {
author: String
comment: String
}
type Subscription {
postAdded: Post
}
type Query {
posts: [Post]
}
type Mutation {
addPost(author: String, comment: String): Post
}
type Post {
author: String
comment: String
}
`
```
@ -84,7 +87,7 @@ const server = new ApolloServer({
});
```
> `connection` contains various metadata, found [here](https://github.com/apollographql/subscriptions-transport-ws/blob/88970eaf6d2e3f68f98696de00631acf4062c088/src/server.ts#L312-L321).
> `connection` contains various metadata, found [here](https://github.com/apollographql/subscriptions-transport-ws/blob/88970eaf6d2e3f68f98696de00631acf4062c088/src/server.ts#L312-L321).
As you can see Apollo Server 2.0 allows realtime data without invasive changes to existing code.
For a full working example please have a look to [this repo](https://github.com/daniele-zurico/apollo2-subscriptions-how-to) provided by [Daniele Zurico](https://github.com/daniele-zurico/apollo2-subscriptions-how-to)

View file

@ -13,19 +13,20 @@ The `Union` type indicates that a field can return more than one object type, bu
const { gql } = require('apollo-server');
const typeDefs = gql`
union Result = Book | Author
union Result = Book | Author
type Book {
title: String
}
type Book {
title: String
}
type Author {
name: String
}
type Author {
name: String
}
type Query {
search: [Result]
}`;
type Query {
search: [Result]
}
`;
```
Since a query requesting a union field, a query being made on a field which is union-typed must specify the object types containing the fields it wants. This ambiguity is solved by an extra `__resolveType` field in the resolver map. `__resolveType` defines the type of the result is out of the available options to GraphQL execution environment.

View file

@ -92,9 +92,9 @@ In Apollo server, a resolver that returns an enum can use the direct string repr
```js
const schema = gql`
type Query {
genre: Genre
}
type Query {
genre: Genre
}
`;
const resolvers = {
@ -110,9 +110,9 @@ Lists are defined with as type modifier that wraps object types, scalars, and en
```js
const schema = gql`
type Query {
todos: [String]
}
type Query {
todos: [String]
}
`;
const resolvers = {
@ -132,9 +132,9 @@ Using the exclamation mark to declare a field as non-nullable simplifies the con
```js
const schema = gql`
type Query {
genre: String!
}
type Query {
genre: String!
}
`;
const resolvers = {

133
package-lock.json generated
View file

@ -1290,9 +1290,9 @@
"dev": true
},
"@types/hapi": {
"version": "17.8.0",
"resolved": "https://registry.npmjs.org/@types/hapi/-/hapi-17.8.0.tgz",
"integrity": "sha512-hI713RUrMY4dn4Q1exYDg4RFYpWwgH5TLROsDFcH8j03mUqCJaorosq7OBegHFSZu0MC/W2fVqKCZ0JwT+nQ2A==",
"version": "17.8.1",
"resolved": "https://registry.npmjs.org/@types/hapi/-/hapi-17.8.1.tgz",
"integrity": "sha512-ruW5d7KiEHzkV97rzytHTgzG6zxKngx+6L+qdhYEX6qvILGX5knG/0kXNfseDhbaE7DzdS75Maf7gtgziy7i+w==",
"dev": true,
"requires": {
"@types/boom": "*",
@ -1326,9 +1326,9 @@
"dev": true
},
"@types/joi": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@types/joi/-/joi-14.0.0.tgz",
"integrity": "sha512-q3r+5QfNrthJL+5Pvb9MOP7rJniTfAV+cGbsO2a3ItGSuwmp2vdjjZ/VCWKwGT5lNx4eOvZI4O3IpSIXh3f4RA==",
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/@types/joi/-/joi-14.0.1.tgz",
"integrity": "sha512-0uZZ+nffpr480zwwUXsk0Z5O0szllffNW1EbkI+dDzKhNKhiX4QOwpwK37WpKIpaPLk9V8U9y2We/VOeD6zyhQ==",
"dev": true
},
"@types/json-stable-stringify": {
@ -1468,9 +1468,9 @@
}
},
"@types/node": {
"version": "10.12.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.14.tgz",
"integrity": "sha512-0rVcFRhM93kRGAU88ASCjX9Y3FWDCh+33G5Z5evpKOea4xcpLqDGwmo64+DjgaSezTN5j9KdnUzvxhOw7fNciQ=="
"version": "10.12.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.15.tgz",
"integrity": "sha512-9kROxduaN98QghwwHmxXO2Xz3MaWf+I1sLVAA6KJDF5xix+IyXVhds0MAfdNwtcpSrzhaTsNB0/jnL86fgUhqA=="
},
"@types/node-fetch": {
"version": "2.1.4",
@ -2041,31 +2041,31 @@
}
},
"apollo-link": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.4.tgz",
"integrity": "sha512-B1z+9H2nTyWEhMXRFSnoZ1vSuAYP+V/EdUJvRx9uZ8yuIBZMm6reyVtr1n0BWlKeSFyPieKJy2RLzmITAAQAMQ==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.6.tgz",
"integrity": "sha512-sUNlA20nqIF3gG3F8eyMD+mO80fmf3dPZX+GUOs3MI9oZR8ug09H3F0UsWJMcpEg6h55Yy5wZ+BMmAjrbenF/Q==",
"requires": {
"apollo-utilities": "^1.0.0",
"zen-observable-ts": "^0.8.11"
"zen-observable-ts": "^0.8.13"
}
},
"apollo-link-http": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.7.tgz",
"integrity": "sha512-EZ9nynHjwYCpGYP5IsRrZGTWidUVpshk7MuSG4joqGtJMwpFCgMQz+y3BHdUhowHtfAd9z60XmeOTG9FJolb8A==",
"version": "1.5.9",
"resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.9.tgz",
"integrity": "sha512-9tJy2zGm4Cm/1ycScDNZJe51dgnTSfKx7pKIgPZmcxkdDpgUY2DZitDH6ZBv4yp9z8MC9Xr9wgwc29s6hcadUQ==",
"dev": true,
"requires": {
"apollo-link": "^1.2.4",
"apollo-link-http-common": "^0.2.6"
"apollo-link": "^1.2.6",
"apollo-link-http-common": "^0.2.8"
}
},
"apollo-link-http-common": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.6.tgz",
"integrity": "sha512-LUOMWvrZuBP1hyWLBXyaW0KyFeKo79j+k3N+Q4HSkXKbLibnllXQ+JxxoSKGhm0bhREygiLtJAG9JnGlhxGO/Q==",
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.8.tgz",
"integrity": "sha512-gGmXZN8mr7e9zjopzKQfZ7IKnh8H12NxBDzvp9nXI3U82aCVb72p+plgoYLcpMY8w6krvoYjgicFmf8LO20TCQ==",
"dev": true,
"requires": {
"apollo-link": "^1.2.4"
"apollo-link": "^1.2.6"
}
},
"apollo-link-persisted-queries": {
@ -3116,6 +3116,24 @@
"integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=",
"dev": true
},
"caller-callsite": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
"integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
"dev": true,
"requires": {
"callsites": "^2.0.0"
}
},
"caller-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
"integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
"dev": true,
"requires": {
"caller-callsite": "^2.0.0"
}
},
"callsites": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
@ -6931,12 +6949,12 @@
}
},
"husky": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/husky/-/husky-1.2.0.tgz",
"integrity": "sha512-/ib3+iycykXC0tYIxsyqierikVa9DA2DrT32UEirqNEFVqOj1bFMTgP3jAz8HM7FgC/C8pc/BTUa9MV2GEkZaA==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/husky/-/husky-1.2.1.tgz",
"integrity": "sha512-4Ylal3HWhnDvIszuiyLoVrSGI7QLg/ogkNCoHE34c+yZYzb9kBZNrlTOsdw92cGi3cJT8pPb6CdVfxFkLnc8Dg==",
"dev": true,
"requires": {
"cosmiconfig": "^5.0.6",
"cosmiconfig": "^5.0.7",
"execa": "^1.0.0",
"find-up": "^3.0.0",
"get-stdin": "^6.0.0",
@ -6948,41 +6966,16 @@
"slash": "^2.0.0"
},
"dependencies": {
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"cosmiconfig": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.7.tgz",
"integrity": "sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA==",
"dev": true,
"requires": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
"semver": "^5.5.0",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
}
},
"execa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
"dev": true,
"requires": {
"cross-spawn": "^6.0.0",
"get-stream": "^4.0.0",
"is-stream": "^1.1.0",
"npm-run-path": "^2.0.0",
"p-finally": "^1.0.0",
"signal-exit": "^3.0.0",
"strip-eof": "^1.0.0"
}
},
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
"dev": true,
"requires": {
"pump": "^3.0.0"
"import-fresh": "^2.0.0",
"is-directory": "^0.3.1",
"js-yaml": "^3.9.0",
"parse-json": "^4.0.0"
}
}
}
@ -7016,6 +7009,16 @@
"minimatch": "^3.0.4"
}
},
"import-fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
"integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
"dev": true,
"requires": {
"caller-path": "^2.0.0",
"resolve-from": "^3.0.0"
}
},
"import-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz",
@ -13359,9 +13362,9 @@
}
},
"ws": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz",
"integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz",
"integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==",
"requires": {
"async-limiter": "~1.0.0"
}
@ -13495,9 +13498,9 @@
"integrity": "sha512-N3xXQVr4L61rZvGMpWe8XoCGX8vhU35dPyQ4fm5CY/KDlG0F75un14hjbckPXTDuKUY6V0dqR2giT6xN8Y4GEQ=="
},
"zen-observable-ts": {
"version": "0.8.11",
"resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.11.tgz",
"integrity": "sha512-8bs7rgGV4kz5iTb9isudkuQjtWwPnQ8lXq6/T76vrepYZVMsDEv6BXaEA+DHdJSK3KVLduagi9jSpSAJ5NgKHw==",
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.13.tgz",
"integrity": "sha512-WDb8SM0tHCb6c0l1k60qXWlm1ok3zN9U4VkLdnBKQwIYwUoB9psH7LIFgR+JVCCMmBxUgOjskIid8/N02k/2Bg==",
"requires": {
"zen-observable": "^0.8.0"
}

View file

@ -68,7 +68,7 @@
"@types/connect": "3.4.32",
"@types/fibers": "0.0.30",
"@types/graphql": "14.0.3",
"@types/hapi": "17.8.0",
"@types/hapi": "17.8.1",
"@types/jest": "23.3.10",
"@types/json-stable-stringify": "1.0.32",
"@types/koa-multer": "1.0.0",
@ -78,7 +78,7 @@
"@types/memcached": "2.2.5",
"@types/micro": "7.3.3",
"@types/multer": "1.3.7",
"@types/node": "10.12.14",
"@types/node": "10.12.15",
"@types/node-fetch": "2.1.4",
"@types/redis": "2.8.8",
"@types/request": "2.48.1",
@ -87,8 +87,8 @@
"@types/type-is": "1.6.2",
"@types/ws": "6.0.1",
"apollo-fetch": "0.7.0",
"apollo-link": "1.2.4",
"apollo-link-http": "1.5.7",
"apollo-link": "1.2.6",
"apollo-link-http": "1.5.9",
"apollo-link-persisted-queries": "0.2.2",
"body-parser": "1.18.3",
"codecov": "3.1.0",
@ -101,7 +101,7 @@
"graphql-tag": "2.10.0",
"graphql-tools": "4.0.3",
"hapi": "17.8.1",
"husky": "1.2.0",
"husky": "1.2.1",
"jest": "23.6.0",
"jest-junit": "5.2.0",
"jest-matcher-utils": "23.6.0",
@ -127,7 +127,7 @@
"ts-jest": "23.10.5",
"tslint": "5.11.0",
"typescript": "3.2.2",
"ws": "6.1.0",
"ws": "6.1.2",
"yup": "0.26.5"
},
"jest": {

View file

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

View file

@ -2,6 +2,13 @@
jest.mock('memcached', () => require('memcached-mock'));
import { MemcachedCache } from '../index';
import { testKeyValueCache } from '../../../apollo-server-caching/src/__tests__/testsuite';
import {
testKeyValueCache_Basics,
testKeyValueCache_Expiration,
} from '../../../apollo-server-caching/src/__tests__/testsuite';
testKeyValueCache(new MemcachedCache('localhost'));
describe('Memcached', () => {
const cache = new MemcachedCache('localhost');
testKeyValueCache_Basics(cache);
testKeyValueCache_Expiration(cache);
});

View file

@ -22,11 +22,11 @@ export class MemcachedCache implements KeyValueCache {
async set(
key: string,
data: string,
value: string,
options?: { ttl?: number },
): Promise<void> {
const { ttl } = Object.assign({}, this.defaultSetOptions, options);
await this.client.set(key, data, ttl);
await this.client.set(key, value, ttl);
}
async get(key: string): Promise<string | undefined> {

View file

@ -3,6 +3,13 @@ jest.mock('redis', () => require('redis-mock'));
jest.useFakeTimers(); // mocks out setTimeout that is used in redis-mock
import { RedisCache } from '../index';
import { testKeyValueCache } from '../../../apollo-server-caching/src/__tests__/testsuite';
import {
testKeyValueCache_Basics,
testKeyValueCache_Expiration,
} from '../../../apollo-server-caching/src/__tests__/testsuite';
testKeyValueCache(new RedisCache({ host: 'localhost' }));
describe('Redis', () => {
const cache = new RedisCache({ host: 'localhost' });
testKeyValueCache_Basics(cache);
testKeyValueCache_Expiration(cache);
});

View file

@ -3,7 +3,7 @@ import Redis from 'redis';
import { promisify } from 'util';
import DataLoader from 'dataloader';
export class RedisCache implements KeyValueCache {
export class RedisCache implements KeyValueCache<string> {
// FIXME: Replace any with proper promisified type
readonly client: any;
readonly defaultSetOptions = {
@ -31,11 +31,11 @@ export class RedisCache implements KeyValueCache {
async set(
key: string,
data: string,
value: string,
options?: { ttl?: number },
): Promise<void> {
const { ttl } = Object.assign({}, this.defaultSetOptions, options);
await this.client.set(key, data, 'EX', ttl);
await this.client.set(key, value, 'EX', ttl);
}
async get(key: string): Promise<string | undefined> {

View file

@ -16,7 +16,9 @@ export interface KeyValueCache {
}
```
## Running test suite
## Testing cache implementations
### Test helpers
You can export and run a jest test suite from `apollo-server-caching` to test your implementation:
@ -28,4 +30,15 @@ import { testKeyValueCache } from 'apollo-server-caching';
testKeyValueCache(new MemcachedCache('localhost'));
```
The default `testKeyValueCache` helper will run all key-value store tests on the specified store, including basic `get` and `set` functionality, along with time-based expunging rules.
Some key-value cache implementations may not be able to support the full suite of tests (for example, some tests might not be able to expire based on time). For those cases, there are more granular implementations which can be used:
* `testKeyValueCache_Basic`
* `testKeyValueCache_Expiration`
For more details, consult the [source for `apollo-server-caching`](./src/__tests__/testsuite.ts).
### Running tests
Run tests with `jest --verbose`

View file

@ -1,24 +1,46 @@
import LRU from 'lru-cache';
import { KeyValueCache } from './KeyValueCache';
export class InMemoryLRUCache implements KeyValueCache {
private store: LRU.Cache<string, string>;
export class InMemoryLRUCache<V = string> implements KeyValueCache<V> {
private store: LRU.Cache<string, V>;
// FIXME: Define reasonable default max size of the cache
constructor({ maxSize = Infinity }: { maxSize?: number } = {}) {
this.store = new LRU({
max: maxSize,
length: item => item.length,
length(item) {
if (Array.isArray(item) || typeof item === 'string') {
return item.length;
}
// If it's an object, we'll use the length to get an approximate,
// relative size of what it would take to store it. It's certainly not
// 100% accurate, but it's a very, very fast implementation and it
// doesn't require bringing in other dependencies or logic which we need
// to maintain. In the future, we might consider something like:
// npm.im/object-sizeof, but this should be sufficient for now.
if (typeof item === 'object') {
return JSON.stringify(item).length;
}
// Go with the lru-cache default "naive" size, in lieu anything better:
// https://github.com/isaacs/node-lru-cache/blob/a71be6cd/index.js#L17
return 1;
},
});
}
async get(key: string) {
return this.store.get(key);
}
async set(key: string, value: string) {
this.store.set(key, value);
async set(key: string, value: V, options?: { ttl?: number }) {
const maxAge = options && options.ttl && options.ttl * 1000;
this.store.set(key, value, maxAge);
}
async delete(key: string) {
this.store.del(key);
}
async flush(): Promise<void> {
this.store.reset();
}
}

View file

@ -1,5 +1,5 @@
export interface KeyValueCache {
get(key: string): Promise<string | undefined>;
set(key: string, value: string, options?: { ttl?: number }): Promise<void>;
export interface KeyValueCache<V = string> {
get(key: string): Promise<V | undefined>;
set(key: string, value: V, options?: { ttl?: number }): Promise<void>;
delete(key: string): Promise<boolean | void>;
}

View file

@ -0,0 +1,11 @@
import {
testKeyValueCache_Basics,
testKeyValueCache_Expiration,
} from '../../../apollo-server-caching/src/__tests__/testsuite';
import { InMemoryLRUCache } from '../InMemoryLRUCache';
describe('InMemoryLRUCache', () => {
const cache = new InMemoryLRUCache();
testKeyValueCache_Basics(cache);
testKeyValueCache_Expiration(cache);
});

View file

@ -1,21 +1,11 @@
import { advanceTimeBy, mockDate, unmockDate } from '__mocks__/date';
export function testKeyValueCache(keyValueCache: any) {
describe('KeyValueCache Test Suite', () => {
beforeAll(() => {
mockDate();
jest.useFakeTimers();
});
export function testKeyValueCache_Basics(keyValueCache: any) {
describe('basic cache functionality', () => {
beforeEach(() => {
keyValueCache.flush();
});
afterAll(() => {
unmockDate();
keyValueCache.close();
});
it('can do a basic get and set', async () => {
await keyValueCache.set('hello', 'world');
expect(await keyValueCache.get('hello')).toBe('world');
@ -28,6 +18,24 @@ export function testKeyValueCache(keyValueCache: any) {
await keyValueCache.delete('hello');
expect(await keyValueCache.get('hello')).toBeUndefined();
});
});
}
export function testKeyValueCache_Expiration(keyValueCache: any) {
describe('time-based cache expunging', () => {
beforeAll(() => {
mockDate();
jest.useFakeTimers();
});
beforeEach(() => {
keyValueCache.flush();
});
afterAll(() => {
unmockDate();
keyValueCache.close();
});
it('is able to expire keys based on ttl', async () => {
await keyValueCache.set('short', 's', { ttl: 1 });
@ -45,3 +53,10 @@ export function testKeyValueCache(keyValueCache: any) {
});
});
}
export function testKeyValueCache(keyValueCache: any) {
describe('KeyValueCache Test Suite', () => {
testKeyValueCache_Basics(keyValueCache);
testKeyValueCache_Expiration(keyValueCache);
});
}

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-cloud-functions",
"version": "2.3.0",
"version": "2.3.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.3.0",
"version": "2.3.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.3.0",
"version": "2.3.1",
"description": "Core engine for Apollo GraphQL server",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -9,8 +9,6 @@ import {
GraphQLNonNull,
parse,
DocumentNode,
ValidationContext,
GraphQLFieldResolver,
} from 'graphql';
import {
@ -19,10 +17,9 @@ import {
GraphQLResponse,
} from 'graphql-extensions';
import { CacheControlExtensionOptions } from 'apollo-cache-control';
import { processGraphQLRequest, GraphQLRequest } from '../requestPipeline';
import { Request } from 'apollo-server-env';
import { GraphQLOptions, Context as GraphQLContext } from 'apollo-server-core';
// This is a temporary kludge to ensure we preserve runQuery behavior with the
// GraphQLRequestProcessor refactoring.
@ -46,25 +43,26 @@ function runQuery(options: QueryOptions): Promise<GraphQLResponse> {
});
}
interface QueryOptions {
schema: GraphQLSchema;
interface QueryOptions
extends Pick<
GraphQLOptions<GraphQLContext<any>>,
| 'cacheControl'
| 'context'
| 'debug'
| 'extensions'
| 'fieldResolver'
| 'formatError'
| 'formatResponse'
| 'rootValue'
| 'schema'
| 'tracing'
| 'validationRules'
> {
queryString?: string;
parsedQuery?: DocumentNode;
rootValue?: any;
context?: any;
variables?: { [key: string]: any };
operationName?: string;
validationRules?: Array<(context: ValidationContext) => any>;
fieldResolver?: GraphQLFieldResolver<any, any>;
formatError?: Function;
formatResponse?: Function;
debug?: boolean;
tracing?: boolean;
cacheControl?: CacheControlExtensionOptions;
request: Pick<Request, 'url' | 'method' | 'headers'>;
extensions?: Array<() => GraphQLExtension>;
}
const queryType = new GraphQLObjectType({

View file

@ -1,3 +1,5 @@
/// <reference path="./types/graphql-upload.d.ts" />
import supportsUploadsInNode from './utils/supportsUploadsInNode';
// We'll memoize this function once at module load time since it should never

View file

@ -22,6 +22,7 @@ import {
} from 'apollo-cache-control';
import { TracingExtension } from 'apollo-tracing';
import {
ApolloError,
fromGraphQLError,
SyntaxError,
ValidationError,
@ -135,15 +136,9 @@ export async function processGraphQLRequest<TContext>(
persistedQueryRegister = true;
// Store the query asynchronously so we don't block.
(async () => {
return (
config.persistedQueries &&
config.persistedQueries.cache.set(`apq:${queryHash}`, query)
);
})().catch(error => {
console.warn(error);
});
Promise.resolve(
config.persistedQueries.cache.set(`apq:${queryHash}`, query),
).catch(console.warn);
}
} else if (query) {
// FIXME: We'll compute the APQ query hash to use as our cache key for
@ -179,13 +174,7 @@ export async function processGraphQLRequest<TContext>(
parsingDidEnd();
} catch (syntaxError) {
parsingDidEnd(syntaxError);
return sendResponse({
errors: [
fromGraphQLError(syntaxError, {
errorClass: SyntaxError,
}),
],
});
return sendErrorResponse(syntaxError, SyntaxError);
}
requestContext.document = document;
@ -197,19 +186,13 @@ export async function processGraphQLRequest<TContext>(
const validationErrors = validate(document);
if (validationErrors.length > 0) {
if (validationErrors.length === 0) {
validationDidEnd();
} else {
validationDidEnd(validationErrors);
return sendResponse({
errors: validationErrors.map(validationError =>
fromGraphQLError(validationError, {
errorClass: ValidationError,
}),
),
});
return sendErrorResponse(validationErrors, ValidationError);
}
validationDidEnd();
// FIXME: If we want to guarantee an operation has been set when invoking
// `willExecuteOperation` and executionDidStart`, we need to throw an
// error here and not leave this to `buildExecutionContext` in
@ -248,9 +231,7 @@ export async function processGraphQLRequest<TContext>(
executionDidEnd();
} catch (executionError) {
executionDidEnd(executionError);
return sendResponse({
errors: [fromGraphQLError(executionError)],
});
return sendErrorResponse(executionError);
}
const formattedExtensions = extensionStack.format();
@ -346,6 +327,27 @@ export async function processGraphQLRequest<TContext>(
return requestContext.response!;
}
function sendErrorResponse(
errorOrErrors: ReadonlyArray<GraphQLError> | GraphQLError,
errorClass?: typeof ApolloError,
) {
// If a single error is passed, it should still be encapsulated in an array.
const errors = Array.isArray(errorOrErrors)
? errorOrErrors
: [errorOrErrors];
return sendResponse({
errors: errors.map(err =>
fromGraphQLError(
err,
errorClass && {
errorClass,
},
),
),
});
}
function initializeRequestListenerDispatcher(): Dispatcher<
GraphQLRequestListener
> {

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-express",
"version": "2.3.0",
"version": "2.3.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.3.0",
"version": "2.3.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.3.0",
"version": "2.3.1",
"description": "Apollo Server Integrations testsuite",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -978,7 +978,7 @@ export function testApolloServer<AS extends ApolloServerBase>(
});
});
});
it('disables subscritpions when option set to false', done => {
it('disables subscriptions when option set to false', done => {
const typeDefs = gql`
type Query {
"graphql-js forces there to be a query type"
@ -1025,7 +1025,7 @@ export function testApolloServer<AS extends ApolloServerBase>(
}
const client = new SubscriptionClient(
`ws://localhost:${port}${server.subscriptionsPath}`,
`ws://localhost:${port}${server.subscriptionsPath || ''}`,
{},
WebSocket,
);

View file

@ -1,6 +1,6 @@
{
"name": "apollo-server-koa",
"version": "2.3.0",
"version": "2.3.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.3.0",
"version": "2.3.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.3.0",
"version": "2.3.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.2.0",
"version": "0.2.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.3.0",
"version": "2.3.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.3.0",
"version": "2.3.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.4.0",
"version": "0.4.1",
"description": "Add extensions to GraphQL servers",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",