Skip to content

Commit

Permalink
Persisted Queries
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Apr 18, 2022
1 parent d18b449 commit f7bb70f
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 98 deletions.
5 changes: 5 additions & 0 deletions .changeset/tricky-forks-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-yoga/common': minor
---

Persisted Queries
1 change: 0 additions & 1 deletion benchmark/hello-world/start-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const { createServer } = require('@graphql-yoga/node')
const server = createServer({
logging: false,
hostname: '127.0.0.1',
healthCheckPath: false,
})

server.start()
2 changes: 1 addition & 1 deletion examples/error-handling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"dependencies": {
"@graphql-yoga/node": "2.3.0",
"cross-undici-fetch": "^0.2.5",
"cross-undici-fetch": "^0.3.0",
"graphql": "^16.1.0",
"ts-node": "10.4.0",
"typescript": "^4.4.4"
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
"contributors": [
"Johannes Schickling <[email protected]>",
"Saihajpreet Singh <[email protected]> (https://github.com/saihaj)",
"Dotan Simha <[email protected]>"
"Dotan Simha <[email protected]>",
"Arda Tanrikulu <[email protected]>",
"Laurin Quast <[email protected]>"
],
"license": "MIT",
"bugs": {
Expand Down
1 change: 1 addition & 0 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@graphql-yoga/subscription": "2.0.0",
"cross-undici-fetch": "^0.3.0",
"dset": "^3.1.1",
"tiny-lru": "^8.0.1",
"tslib": "^2.3.1"
},
"devDependencies": {
Expand Down
12 changes: 3 additions & 9 deletions packages/common/src/getGraphQLParameters.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { dset } from 'dset'

type GraphQLRequestPayload = {
operationName?: string
query?: string
variables?: Record<string, unknown>
extensions?: Record<string, unknown>
}
import { GraphQLParams } from './types'

type RequestParser = {
is: (request: Request) => boolean
parse: (request: Request) => Promise<GraphQLRequestPayload>
parse: (request: Request) => Promise<GraphQLParams>
}

export const GETRequestParser: RequestParser = {
Expand Down Expand Up @@ -72,7 +66,7 @@ export const POSTMultipartFormDataRequestParser: RequestParser = {
export function buildGetGraphQLParameters(parsers: Array<RequestParser>) {
return async function getGraphQLParameters(
request: Request,
): Promise<GraphQLRequestPayload> {
): Promise<GraphQLParams> {
for (const parser of parsers) {
if (parser.is(request)) {
return parser.parse(request)
Expand Down
90 changes: 64 additions & 26 deletions packages/common/src/processRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import {
ExecutionArgs,
ExecutionResult,
GraphQLError,
print,
} from 'graphql'
import { isAsyncIterable } from '@graphql-tools/utils'
import { ExecutionPatchResult, RequestProcessContext } from './types'
import { encodeString } from './encodeString'
import { crypto } from 'cross-undici-fetch'

interface ErrorResponseParams {
status?: number
Expand All @@ -34,7 +36,7 @@ function getExecutableOperation(
return operation
}

export async function processRequest<TContext, TRootValue = {}>({
export async function processRequest<TContext>({
contextFactory,
execute,
operationName,
Expand All @@ -46,9 +48,11 @@ export async function processRequest<TContext, TRootValue = {}>({
validate,
variables,
extraHeaders,
extensions,
persistedQueryStore,
Response,
ReadableStream,
}: RequestProcessContext<TContext, TRootValue>): Promise<Response> {
}: RequestProcessContext<TContext>): Promise<Response> {
function getErrorResponse({
status = 500,
headers,
Expand Down Expand Up @@ -207,21 +211,71 @@ export async function processRequest<TContext, TRootValue = {}>({
})
}

if (query == null) {
if (extensions?.persistedQuery != null && persistedQueryStore == null) {
return getErrorResponse({
status: 400,
errors: [new GraphQLError('Must provide query string.')],
status: 500,
errors: [new GraphQLError('PersistedQueryNotSupported')],
isEventStream,
headers: extraHeaders,
})
}

try {
if (typeof query !== 'string' && query.kind === 'Document') {
document = query
if (query == null) {
if (
extensions?.persistedQuery?.version === 1 &&
extensions?.persistedQuery?.sha256Hash != null &&
persistedQueryStore != null
) {
const persistedQuery = await persistedQueryStore.get(
extensions?.persistedQuery?.sha256Hash,
)
if (persistedQuery == null) {
return getErrorResponse({
status: 404,
errors: [new GraphQLError('PersistedQueryNotFound')],
isEventStream,
headers: extraHeaders,
})
} else {
query = persistedQuery
}
} else {
document = parse(query)
return getErrorResponse({
status: 400,
errors: [new GraphQLError('Must provide query string.')],
isEventStream,
headers: extraHeaders,
})
}
} else if (
extensions?.persistedQuery?.version === 1 &&
extensions?.persistedQuery?.sha256Hash != null &&
persistedQueryStore != null
) {
if (crypto) {
const encodedQuery = encodeString(query)
const hashArrayBuffer = await crypto.subtle.digest(
'SHA-256',
encodedQuery,
)
const hashTypedArray = new Uint8Array(hashArrayBuffer)
const expectedHashString = [...hashTypedArray]
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
if (extensions.persistedQuery.sha256Hash !== expectedHashString) {
return getErrorResponse({
status: 400,
errors: [new GraphQLError('PersistedQueryInvalidHash')],
isEventStream,
headers: extraHeaders,
})
}
}
await persistedQueryStore.set(extensions.persistedQuery.sha256Hash, query)
}

try {
document = parse(query)
} catch (e: unknown) {
return getErrorResponse({
status: 400,
Expand Down Expand Up @@ -259,29 +313,13 @@ export async function processRequest<TContext, TRootValue = {}>({
})
}

let variableValues: { [name: string]: any } | undefined

try {
if (variables) {
variableValues =
typeof variables === 'string' ? JSON.parse(variables) : variables
}
} catch (_error) {
return getErrorResponse({
errors: [new GraphQLError('Variables are invalid JSON.')],
status: 400,
isEventStream,
headers: extraHeaders,
})
}

contextValue = await contextFactory()

const executionArgs: ExecutionArgs = {
schema,
document,
contextValue,
variableValues,
variableValues: variables,
operationName,
}

Expand Down
66 changes: 53 additions & 13 deletions packages/common/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GraphQLSchema, isSchema, print } from 'graphql'
import { GraphQLError, GraphQLSchema, isSchema, print } from 'graphql'
import {
Plugin,
GetEnvelopedFn,
Expand All @@ -25,6 +25,7 @@ import {
YogaInitialContext,
FetchEvent,
FetchAPI,
PersistedQueriesCache,
} from './types'
import {
GraphiQLOptions,
Expand All @@ -35,6 +36,7 @@ import * as crossUndiciFetch from 'cross-undici-fetch'
import { getGraphQLParameters } from './getGraphQLParameters'
import { processRequest } from './processRequest'
import { defaultYogaLogger, YogaLogger } from './logger'
import lru from 'tiny-lru'

interface OptionsWithPlugins<TContext> {
/**
Expand Down Expand Up @@ -128,6 +130,13 @@ export type YogaServerOptions<
parserCache?: boolean | ParserCacheOptions
validationCache?: boolean | ValidationCache
fetchAPI?: FetchAPI
persistedQueries?:
| boolean
| PersistedQueriesCache
| {
max?: number
ttl?: number
}
} & Partial<
OptionsWithPlugins<TUserContext & TServerContext & YogaInitialContext>
>
Expand Down Expand Up @@ -202,6 +211,7 @@ export class YogaServer<
fetch: typeof fetch
ReadableStream: typeof ReadableStream
}
protected persistedQueryStore?: PersistedQueriesCache

renderGraphiQL: (options?: GraphiQLOptions) => PromiseOrValue<BodyInit>

Expand Down Expand Up @@ -237,8 +247,6 @@ export class YogaServer<
}
: logger

const maskedErrors = options?.maskedErrors ?? true

this.getEnveloped = envelop({
plugins: [
// Use the schema provided by the user
Expand Down Expand Up @@ -297,9 +305,11 @@ export class YogaServer<
),
...(options?.plugins ?? []),
enableIf(
!!maskedErrors,
options?.maskedErrors !== false,
useMaskedErrors(
typeof maskedErrors === 'object' ? maskedErrors : undefined,
typeof options?.maskedErrors === 'object'
? options?.maskedErrors
: undefined,
),
),
],
Expand Down Expand Up @@ -329,6 +339,24 @@ export class YogaServer<
this.renderGraphiQL = options?.renderGraphiQL || renderGraphiQL

this.endpoint = options?.endpoint

if (options?.persistedQueries !== false) {
if (typeof options?.persistedQueries === 'object') {
if ('get' in options.persistedQueries) {
this.persistedQueryStore = options.persistedQueries
} else if (
'max' in options.persistedQueries ||
'ttl' in options.persistedQueries
) {
this.persistedQueryStore = lru<string>(
options.persistedQueries.max ?? 1000,
options.persistedQueries.ttl ?? 36000,
)
}
} else {
this.persistedQueryStore = lru<string>(1000, 36000)
}
}
}

getCORSResponseHeaders(
Expand Down Expand Up @@ -519,7 +547,9 @@ export class YogaServer<

this.logger.debug(`Processing Request`)

const corsHeaders = this.getCORSResponseHeaders(request, initialContext)
const corsHeaders = request.headers.get('origin')
? this.getCORSResponseHeaders(request, initialContext)
: {}
const response = await processRequest({
request,
query,
Expand All @@ -532,17 +562,29 @@ export class YogaServer<
contextFactory,
schema,
extraHeaders: corsHeaders,
extensions,
persistedQueryStore: this.persistedQueryStore,
Response: this.fetchAPI.Response,
ReadableStream: this.fetchAPI.ReadableStream,
})
return response
} catch (err: any) {
this.logger.error(err.message, err.stack, err)
const response = new this.fetchAPI.Response(err.message, {
} catch (error: any) {
if (error instanceof GraphQLError) {
return new this.fetchAPI.Response(
JSON.stringify({
errors: [error],
}),
{
status: 200,
},
)
}
const errorMessage = error.stack || JSON.stringify(error)
this.logger.error(errorMessage)
return new this.fetchAPI.Response(errorMessage, {
status: 500,
statusText: 'Internal Server Error',
})
return response
}
}

Expand Down Expand Up @@ -573,9 +615,7 @@ export class YogaServer<
method: 'POST',
headers,
body: JSON.stringify({
query:
document &&
(typeof document === 'string' ? document : print(document)),
query: typeof document === 'string' ? document : print(document),
variables,
operationName,
}),
Expand Down
Loading

0 comments on commit f7bb70f

Please sign in to comment.