Apollo Server 2:Automatic Persisted Queries (#1149)

This commit is contained in:
Evans Hauser 2018-06-11 15:44:20 -07:00 committed by David Glasser
parent ff90e5cf2f
commit a7cd3a43e8
17 changed files with 558 additions and 69 deletions

View file

@ -16,6 +16,7 @@ sidebar_categories:
Features:
- features/mocking
- features/errors
- features/apq
- features/logging
- features/scalars-enums
- features/unions-interfaces

BIN
docs/sketch/APQs.sketch Normal file

Binary file not shown.

View file

@ -0,0 +1,86 @@
---
title: Automatic Persisted Queries
description: Reduce the size of GraphQL requests over the wire
---
The size of individual GraphQL query strings can be a major pain point. Apollo Server allows implements Automatic Persisted Queries(APQ), a technique that greatly improves network performance for GraphQL with zero build-time configuration. A persisted query is a ID or hash that can be sent to the server instead of the entire GraphQL query string. This smaller signature reduces bandwidth utilization and speeds up client loading times. Persisted queries are especially nice paired with GET requests, enabling the browser cache and [integration with a CDN](#get).
With Apollo Persisted Queries, the ID is a deterministic hash of the input query, so we don't need a complex build step to share the ID between clients and servers. If a server doesn't know about a given hash, the client can expand the query for it; Apollo Server caches that mapping.
<h2 id="setup">Setup</h2>
To get started with APQ, add the [Automatic Persisted Queries Link](https://github.com/apollographql/apollo-link-persisted-queries) to the client codebase with `npm install apollo-link-persisted-queries`. Next incorporate the APQ link with Apollo Client's link chain before the HTTP link:
```js
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import ApolloClient from "apollo-client";
const link = createPersistedQueryLink().concat(createHttpLink({ uri: "/graphql" }));
const client = new ApolloClient({
cache: new InMemoryCache(),
link: link,
});
```
> Note: using `apollo-link-persisted-query` requires migrating from [apollo-boost](https://www.apollographql.com/docs/react/advanced/boost-migration.html):
Inside Apollo Server, the query registry is stored in a user-configurable cache. By default, Apollo Server uses a in-memory cache.
<h2 id="verify">Verify</h2>
Apollo Server's persisted queries configuration can be tested from the command-line. The following examples assume Apollo Server is running at `localhost:4000/`.
This example persists a dummy query of `{__typename}`, using its sha256 hash: `ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38`.
1. Request a persisted query:
```bash
curl -g 'http://localhost:4000/?extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'
```
Expect a response of: `{"errors": [{"message": "PersistedQueryNotFound", "extensions": {...}}]}`.
2. Store the query to the cache:
```bash
curl -g 'http://localhost:4000/?query={__typename}&extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'
```
Expect a response of `{"data": {"__typename": "Query"}}"`.
3. Request the persisted query again:
```bash
curl -g 'http://localhost:4000/?extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'
```
Expect a response of `{"data": {"__typename": "Query"}}"`, as the query string is loaded from the cache.
<h2 id="get">Using GET requests with APQ to enable CDNs</h2>
A great application for APQ is running Apollo Server behind a CDN. Many CDNs only cache GET requests, but many GraphQL queries are too long to fit comfortably in a cacheable GET request. When the APQ link is created with `createPersistedQueryLink({useGETForHashedQueries: true})`, Apollo Client automatically sends the short hashed queries as GET requests allowing a CDN to serve those request. For full-length queries and for all mutations, Apollo Client will continue to use POST requests. For more about this pattern, read about how [Apollo Server provides cache information to CDNs]().
<h2 id="how-it-works">How it works</h2>
The mechanism is based on a lightweight protocol extension between Apollo Client and Apollo Server. It works as follows:
- When the client makes a query, it will optimistically send a short (64-byte) cryptographic hash instead of the full query text.
- *Optimized Path:* If a request containing a persisted query hash is detected, Apollo Server will look it up to find a corresponding query in its registry. Upon finding a match, Apollo Server will expand the request with the full text of the query and execute it.
- *New Query Path:* In the unlikely event that the query is not already in the Apollo Server registry (this only happens the very first time that Apollo Server sees a query), it will ask the client to resend the request using the full text of the query. At that point Apollo Server will store the query / hash mapping in the registry for all subsequent requests to benefit from.
<div align="center">
<h3 id="Optimized-Path">**Optimized Path**</h3>
</div>
![Optimized path](../img/persistedQueries.optPath.png)
<div align="center">
<h3 id="New-Query-Path">**New Query Path**</h3>
</div>
![New query path](../img/persistedQueries.newPath.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -1,6 +1,8 @@
# Changelog
### vNEXT
* `apollo-server-core`: Add persisted queries [PR#1149](https://github.com/apollographql/apollo-server/pull/1149)
* `apollo-server-core`: added `BadUserInputError`
* `apollo-server-core`: **breaking** gql is exported from gql-tag and ApolloServer requires a DocumentNode [PR#1146](https://github.com/apollographql/apollo-server/pull/1146)
* `apollo-server-core`: accept `Request` object in `runQuery` [PR#1108](https://github.com/apollographql/apollo-server/pull/1108)

View file

@ -30,12 +30,18 @@
"devDependencies": {
"@types/fibers": "0.0.30",
"@types/graphql": "^0.13.1",
"@types/keyv": "^3.0.1",
"@types/node-fetch": "^2.1.1",
"@types/quick-lru": "^1.1.0",
"@types/ws": "^5.1.2",
"apollo-engine": "^1.1.2",
"apollo-fetch": "^0.7.0",
"apollo-link": "^1.2.2",
"apollo-link-http": "^1.5.4",
"apollo-link-persisted-queries": "^0.2.0",
"fibers": "2.0.2",
"graphql-tag": "^2.9.2",
"js-sha256": "^0.9.0",
"meteor-promise": "0.8.6",
"mock-req": "^0.2.0"
},
@ -53,7 +59,10 @@
"graphql-extensions": "0.1.0-beta.13",
"graphql-subscriptions": "^0.5.8",
"graphql-tools": "^3.0.2",
"hash.js": "^1.1.3",
"keyv": "^3.0.0",
"node-fetch": "^2.1.2",
"quick-lru": "^1.1.0",
"subscriptions-transport-ws": "^0.9.9",
"ws": "^5.2.0"
}

View file

@ -3,7 +3,9 @@ import { expect } from 'chai';
import { stub } from 'sinon';
import http from 'http';
import net from 'net';
import url from 'url';
import 'mocha';
import { sha256 } from 'js-sha256';
import {
GraphQLSchema,
@ -18,6 +20,13 @@ import { PubSub } from 'graphql-subscriptions';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import WebSocket from 'ws';
import { execute } from 'apollo-link';
import { createHttpLink } from 'apollo-link-http';
import {
createPersistedQueryLink as createPersistedQuery,
VERSION,
} from 'apollo-link-persisted-queries';
import { createApolloFetch } from 'apollo-fetch';
import { ApolloServerBase } from './ApolloServer';
import { AuthenticationError } from './errors';
@ -68,11 +77,13 @@ function createHttpServer(server) {
body = Buffer.concat(body).toString();
// At this point, we have the headers, method, url and body, and can now
// do whatever we need to in order to respond to this request.
runHttpQuery([req, res], {
method: req.method,
options: server.graphQLServerOptionsForRequest(req as any),
query: JSON.parse(body),
query:
req.method.toUpperCase() === 'GET'
? url.parse(req.url, true)
: JSON.parse(body),
request: convertNodeHttpToRequest(req),
})
.then(gqlResponse => {
@ -85,6 +96,13 @@ function createHttpServer(server) {
res.end();
})
.catch(error => {
if (error.headers) {
Object.keys(error.headers).forEach(header => {
res.setHeader(header, error.headers[header]);
});
}
res.statusCode = error.statusCode;
res.write(error.message);
res.end();
});
@ -866,4 +884,135 @@ describe('ApolloServerBase', () => {
.catch(done);
});
});
describe('Persisted Queries', () => {
let server;
const query = gql`
${TEST_STRING_QUERY}
`;
const hash = sha256
.create()
.update(TEST_STRING_QUERY)
.hex();
const extensions = {
persistedQuery: {
version: VERSION,
sha256Hash: hash,
},
};
let uri: string;
beforeEach(async () => {
server = new ApolloServerBase({
schema,
introspection: false,
persistedQueries: {
cache: new Map<string, string>() as any,
},
});
const httpServer = createHttpServer(server);
server.use({
getHttp: () => httpServer,
path: '/graphql',
});
uri = (await server.listen()).url;
});
afterEach(async () => {
await server.stop();
});
it('returns PersistedQueryNotFound on the first try', async () => {
const apolloFetch = createApolloFetch({ uri });
const result = await apolloFetch({
extensions,
} as any);
expect(result.data).not.to.exist;
expect(result.errors.length).to.equal(1);
expect(result.errors[0].message).to.equal('PersistedQueryNotFound');
expect(result.errors[0].extensions.code).to.equal(
'PERSISTED_QUERY_NOT_FOUND',
);
});
it('returns result on the second try', async () => {
const apolloFetch = createApolloFetch({ uri });
await apolloFetch({
extensions,
} as any);
const result = await apolloFetch({
extensions,
query: TEST_STRING_QUERY,
} as any);
expect(result.data).to.deep.equal({ testString: 'test string' });
expect(result.errors).not.to.exist;
});
it('returns result on the persisted query', async () => {
const apolloFetch = createApolloFetch({ uri });
await apolloFetch({
extensions,
} as any);
await apolloFetch({
extensions,
query: TEST_STRING_QUERY,
} as any);
const result = await apolloFetch({
extensions,
} as any);
expect(result.data).to.deep.equal({ testString: 'test string' });
expect(result.errors).not.to.exist;
});
it('returns error when hash does not match', async () => {
const apolloFetch = createApolloFetch({ uri });
try {
await apolloFetch({
extensions: {
persistedQuery: {
version: VERSION,
sha:
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
},
},
query: TEST_STRING_QUERY,
} as any);
expect.fail();
} catch (e) {
expect(e.response.status).to.equal(400);
expect(e.response.raw).to.match(/does not match query/);
}
});
it('returns correct result for persisted query link', done => {
const variables = { id: 1 };
const link = createPersistedQuery().concat(
createHttpLink({ uri, fetch } as any),
);
execute(link, { query, variables } as any).subscribe(result => {
expect(result.data).to.deep.equal({ testString: 'test string' });
done();
}, done);
});
it('returns correct result for persisted query link using get request', done => {
const variables = { id: 1 };
const link = createPersistedQuery({
useGETForHashedQueries: true,
}).concat(createHttpLink({ uri, fetch } as any));
execute(link, { query, variables } as any).subscribe(result => {
expect(result.data).to.deep.equal({ testString: 'test string' });
done();
}, done);
});
});
});

View file

@ -27,8 +27,15 @@ import {
ExecutionParams,
} from 'subscriptions-transport-ws';
//use as default persisted query store
import Keyv = require('keyv');
import QuickLru = require('quick-lru');
import { formatApolloErrors } from './errors';
import { GraphQLServerOptions as GraphQLOptions } from './graphqlOptions';
import {
GraphQLServerOptions as GraphQLOptions,
PersistedQueryOptions,
} from './graphqlOptions';
import { LogFunction } from './logging';
import {
@ -108,7 +115,22 @@ export class ApolloServerBase<Request = RequestInit> {
: noIntro;
}
this.requestOptions = requestOptions;
if (requestOptions.persistedQueries !== false) {
if (!requestOptions.persistedQueries) {
//maxSize is the number of elements that can be stored inside of the cache
//https://github.com/withspectrum/spectrum has about 200 instances of gql`
//300 queries seems reasonable
const lru = new QuickLru({ maxSize: 300 });
requestOptions.persistedQueries = {
cache: new Keyv({ store: lru }),
};
}
} else {
//the user does not want to use persisted queries, so we remove the field
delete requestOptions.persistedQueries;
}
this.requestOptions = requestOptions as GraphQLOptions;
this.context = context;
if (
@ -380,7 +402,9 @@ const typeDefs = gql\`${startSchema}\`
try {
context =
typeof this.context === 'function'
? await this.context({ req: request })
? await this.context({
req: request,
})
: context;
} catch (error) {
//Defer context error resolution to inside of runQuery
@ -397,6 +421,8 @@ const typeDefs = gql\`${startSchema}\`
// avoid a bad side effect of the otherwise useful noUnusedLocals option
// (https://github.com/Microsoft/TypeScript/issues/21673).
logFunction: this.requestOptions.logFunction as LogFunction,
persistedQueries: this.requestOptions
.persistedQueries as PersistedQueryOptions,
fieldResolver: this.requestOptions.fieldResolver as GraphQLFieldResolver<
any,
any

View file

@ -0,0 +1,4 @@
export interface PersistedQueryCache {
set(key: string, data: string): Promise<any>;
get(key: string): Promise<string | null>;
}

View file

@ -184,6 +184,32 @@ export class ForbiddenError extends ApolloError {
}
}
export class PersistedQueryNotFoundError extends ApolloError {
constructor() {
super('PersistedQueryNotFound', 'PERSISTED_QUERY_NOT_FOUND');
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, PersistedQueryNotFoundError.prototype);
Object.defineProperty(this, 'name', {
value: 'PersistedQueryNotFoundError',
});
}
}
export class PersistedQueryNotSupportedError extends ApolloError {
constructor() {
super('PersistedQueryNotSupported', 'PERSISTED_QUERY_NOT_SUPPORTED');
// Set the prototype explicitly.
// https://stackoverflow.com/a/41102306
Object.setPrototypeOf(this, PersistedQueryNotSupportedError.prototype);
Object.defineProperty(this, 'name', {
value: 'PersistedQueryNotSupportedError',
});
}
}
export class BadUserInputError extends ApolloError {
constructor(message: string, properties?: Record<string, any>) {
super(message, 'BAD_USER_INPUT', properties);

View file

@ -4,6 +4,7 @@ import {
GraphQLFieldResolver,
} from 'graphql';
import { LogFunction } from './logging';
import { PersistedQueryCache } from './caching';
import { GraphQLExtension } from 'graphql-extensions';
/*
@ -41,6 +42,11 @@ export interface GraphQLServerOptions<
// cacheControl?: boolean | CacheControlExtensionOptions;
cacheControl?: boolean | any;
extensions?: Array<() => GraphQLExtension>;
persistedQueries?: PersistedQueryOptions;
}
export interface PersistedQueryOptions {
cache: PersistedQueryCache;
}
export default GraphQLServerOptions;

View file

@ -4,6 +4,7 @@ export { runHttpQuery, HttpQueryRequest, HttpQueryError } from './runHttpQuery';
export {
default as GraphQLOptions,
resolveGraphqlOptions,
PersistedQueryOptions,
} from './graphqlOptions';
export {
ApolloError,

View file

@ -1,10 +1,17 @@
import { ExecutionResult } from 'graphql';
import sha256 from 'hash.js/lib/hash/sha/256';
import { runQuery, QueryOptions } from './runQuery';
import {
default as GraphQLOptions,
resolveGraphqlOptions,
} from './graphqlOptions';
import { formatApolloErrors } from './errors';
import {
formatApolloErrors,
PersistedQueryNotSupportedError,
PersistedQueryNotFoundError,
} from './errors';
import { LogAction, LogStep } from './logging';
export interface HttpQueryRequest {
method: string;
@ -20,6 +27,11 @@ export interface HttpQueryRequest {
request: Pick<Request, 'url' | 'method' | 'headers'>;
}
//The result of a curl does not appear well in the terminal, so we add an extra new line
function prettyJSONStringify(toStringfy) {
return JSON.stringify(toStringfy) + '\n';
}
export class HttpQueryError extends Error {
public statusCode: number;
public isGraphQLError: boolean;
@ -39,6 +51,27 @@ export class HttpQueryError extends Error {
}
}
function throwHttpGraphQLError(
statusCode,
errors: Array<Error>,
optionsObject,
) {
throw new HttpQueryError(
statusCode,
prettyJSONStringify({
errors: formatApolloErrors(errors, {
debug: optionsObject.debug,
formatter: optionsObject.formatError,
logFunction: optionsObject.logFunction,
}),
}),
true,
{
'Content-Type': 'application/json',
},
);
}
export async function runHttpQuery(
handlerArguments: Array<any>,
request: HttpQueryRequest,
@ -62,19 +95,11 @@ export async function runHttpQuery(
if (!debugDefault) {
e.warning = `To remove the stacktrace, set the NODE_ENV environment variable to production if the options creation can fail`;
}
throw new HttpQueryError(
500,
JSON.stringify({
errors: formatApolloErrors([e], { debug: debugDefault }),
}),
true,
{
'Content-Type': 'application/json',
},
);
throwHttpGraphQLError(500, [e], { debug: debugDefault });
}
if (optionsObject.debug === undefined) {
optionsObject.debug = debugDefault;
}
const debug =
optionsObject.debug !== undefined ? optionsObject.debug : debugDefault;
let requestPayload;
switch (request.method) {
@ -118,7 +143,7 @@ export async function runHttpQuery(
const requests = requestPayload.map(async requestParams => {
try {
const queryString: string | undefined = requestParams.query;
let queryString: string | undefined = requestParams.query;
let extensions = requestParams.extensions;
if (isGetRequest && extensions) {
@ -132,31 +157,65 @@ export async function runHttpQuery(
}
}
if (
queryString === undefined &&
extensions &&
extensions.persistedQuery
) {
// It looks like we've received an Apollo Persisted Query. Apollo Server
// does not support persisted queries out of the box, so we should fail
// fast with a clear error saying that we don't support APQs. (A future
// version of Apollo Server may support APQs directly.)
throw new HttpQueryError(
if (extensions && extensions.persistedQuery) {
// It looks like we've received an Apollo Persisted Query. Check if we
// support them. In an ideal world, we always would, however since the
// middleware options are created every request, it does not make sense
// to create a default cache here and save a referrence to use across
// requests
if (
!optionsObject.persistedQueries ||
!optionsObject.persistedQueries.cache
) {
// Return 200 to simplify processing: we want this to be intepreted by
// the client as data worth interpreting, not an error.
200,
JSON.stringify({
errors: [
{
message: 'PersistedQueryNotSupported',
},
],
}),
true,
{
'Content-Type': 'application/json',
},
);
throwHttpGraphQLError(
200,
[new PersistedQueryNotSupportedError()],
optionsObject,
);
} else if (extensions.persistedQuery.version !== 1) {
throw new HttpQueryError(400, 'Unsupported persisted query version');
}
const sha = extensions.persistedQuery.sha256Hash;
if (queryString === undefined) {
queryString = await optionsObject.persistedQueries.cache.get(sha);
if (!queryString) {
throwHttpGraphQLError(
200,
[new PersistedQueryNotFoundError()],
optionsObject,
);
}
} else {
const calculatedSha = sha256()
.update(queryString)
.digest('hex');
if (sha !== calculatedSha) {
throw new HttpQueryError(400, 'provided sha does not match query');
}
//Do the store completely asynchronously
Promise.resolve()
.then(() => {
//We do not wait on the cache storage to complete
return optionsObject.persistedQueries.cache.set(sha, queryString);
})
.catch(error => {
if (optionsObject.logFunction) {
optionsObject.logFunction({
action: LogAction.setup,
step: LogStep.status,
key: 'error',
data: error,
});
} else {
console.warn(error);
}
});
}
}
//We ensure that there is a queryString or parsedQuery after formatParams
@ -216,20 +275,7 @@ export async function runHttpQuery(
context = await context();
} catch (e) {
e.message = `Context creation failed: ${e.message}`;
throw new HttpQueryError(
500,
JSON.stringify({
errors: formatApolloErrors([e], {
formatter: optionsObject.formatError,
debug,
logFunction: optionsObject.logFunction,
}),
}),
true,
{
'Content-Type': 'application/json',
},
);
throwHttpGraphQLError(500, [e], optionsObject);
}
} else if (isBatch) {
context = Object.assign(
@ -279,7 +325,7 @@ export async function runHttpQuery(
return {
errors: formatApolloErrors([e], {
formatter: optionsObject.formatError,
debug,
debug: optionsObject.debug,
logFunction: optionsObject.logFunction,
}),
};
@ -293,12 +339,12 @@ export async function runHttpQuery(
//This code is run on parse/validation errors and any other error that
//doesn't reach GraphQL execution
if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') {
throw new HttpQueryError(400, JSON.stringify(gqlResponse), true, {
throw new HttpQueryError(400, prettyJSONStringify(gqlResponse), true, {
'Content-Type': 'application/json',
});
}
return JSON.stringify(gqlResponse);
return prettyJSONStringify(gqlResponse);
}
return JSON.stringify(responses);
return prettyJSONStringify(responses);
}

View file

@ -6,7 +6,10 @@ import { ListenOptions as HttpListenOptions } from 'net';
import { GraphQLExtension } from 'graphql-extensions';
import { EngineReportingOptions } from 'apollo-engine-reporting';
import { GraphQLServerOptions as GraphQLOptions } from './graphqlOptions';
import {
GraphQLServerOptions as GraphQLOptions,
PersistedQueryOptions,
} from './graphqlOptions';
export type Context<T = any> = T;
export type ContextFunction<T = any> = (
@ -35,7 +38,6 @@ export interface Config
| 'validationRules'
| 'formatResponse'
| 'fieldResolver'
| 'debug'
| 'cacheControl'
| 'tracing'
> {
@ -48,6 +50,7 @@ export interface Config
mocks?: boolean | IMocks;
engine?: boolean | EngineReportingOptions;
extensions?: Array<() => GraphQLExtension>;
persistedQueries?: PersistedQueryOptions | false;
}
// XXX export these directly from apollo-engine-js

View file

@ -32,6 +32,8 @@
"definition": "dist/index.d.ts"
},
"devDependencies": {
"graphql-tag": "^2.9.2"
"apollo-link-persisted-queries": "^0.2.0",
"graphql-tag": "^2.9.2",
"js-sha256": "^0.9.0"
}
}

View file

@ -2,6 +2,10 @@ import { expect } from 'chai';
import { stub } from 'sinon';
import 'mocha';
//persisted query tests
import { sha256 } from 'js-sha256';
import { VERSION } from 'apollo-link-persisted-queries';
import {
GraphQLSchema,
GraphQLObjectType,
@ -419,9 +423,11 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body).to.deep.equal({
errors: [{ message: 'PersistedQueryNotSupported' }],
});
expect(res.body.errors).to.exist;
expect(res.body.errors.length).to.equal(1);
expect(res.body.errors[0].message).to.equal(
'PersistedQueryNotSupported',
);
});
});
@ -440,9 +446,11 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
});
return req.then(res => {
expect(res.status).to.equal(200);
expect(res.body).to.deep.equal({
errors: [{ message: 'PersistedQueryNotSupported' }],
});
expect(res.body.errors).to.exist;
expect(res.body.errors.length).to.equal(1);
expect(res.body.errors[0].message).to.equal(
'PersistedQueryNotSupported',
);
});
});
@ -1105,5 +1113,125 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
});
});
});
describe('Persisted Queries', () => {
const query = '{testString}';
const hash = sha256
.create()
.update(query)
.hex();
const extensions = {
persistedQuery: {
version: VERSION,
sha256Hash: hash,
},
};
beforeEach(async () => {
const map = new Map<string, string>();
const cache = {
set: async (key, val) => {
await map.set(key, val);
},
get: async key => map.get(key),
};
app = await createApp({
graphqlOptions: {
schema,
persistedQueries: {
cache,
},
},
});
});
it('returns PersistedQueryNotFound on the first try', async () => {
const result = await request(app)
.post('/graphql')
.send({
extensions,
});
expect(result.body.data).not.to.exist;
expect(result.body.errors.length).to.equal(1);
expect(result.body.errors[0].message).to.equal(
'PersistedQueryNotFound',
);
expect(result.body.errors[0].extensions.code).to.equal(
'PERSISTED_QUERY_NOT_FOUND',
);
});
it('returns result on the second try', async () => {
await request(app)
.post('/graphql')
.send({
extensions,
});
const result = await request(app)
.post('/graphql')
.send({
extensions,
query,
});
expect(result.body.data).to.deep.equal({ testString: 'it works' });
expect(result.body.errors).not.to.exist;
});
it('returns result on the persisted query', async () => {
await request(app)
.post('/graphql')
.send({
extensions,
});
await request(app)
.post('/graphql')
.send({
extensions,
query,
});
const result = await request(app)
.post('/graphql')
.send({
extensions,
});
expect(result.body.data).to.deep.equal({ testString: 'it works' });
expect(result.body.errors).not.to.exist;
});
it('returns error when hash does not match', async () => {
const response = await request(app)
.post('/graphql')
.send({
extensions: {
persistedQuery: {
version: VERSION,
sha:
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
},
},
query,
});
expect(response.status).to.equal(400);
expect(response.error.text).to.match(/does not match query/);
});
it('returns correct result using get request', async () => {
await request(app)
.post('/graphql')
.send({
extensions,
query,
});
const result = await request(app)
.get('/graphql')
.query({
extensions: JSON.stringify(extensions),
});
expect(result.body.data).to.deep.equal({ testString: 'it works' });
});
});
});
};