Skip to content

Commit

Permalink
Response Cache plugin (#1359)
Browse files Browse the repository at this point in the history
* chore(deps): update dependency vite to v3 (master) (#1444)

* chore(deps): update actions/checkout action to v3 (#1431)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency ioredis to v5.2.2 (#1450)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Replace cross-undici-fetch with @whatwg-node/fetch

* chore(deps): update dependency vite to v3

* Fix GraphiQL build

* Go

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>

* Response Cache plugin

* Fixes

* chore: upgrade to latest envelop/response-cache

* no need to graphql parameters

* add docs

* white      space     ok

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Laurin Quast <laurinquast@googlemail.com>
  • Loading branch information
3 people committed Jul 28, 2022
1 parent f325429 commit 5629a5c
Show file tree
Hide file tree
Showing 14 changed files with 620 additions and 104 deletions.
9 changes: 9 additions & 0 deletions .changeset/chilly-rats-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@graphql-yoga/common': minor
---

New `setResult` helper is available in `onRequestParseDone` hook to set `ExecutionResult` before any GraphQL specific process.

You can check `@graphql-yoga/plugin-response-cache`'s implementation to see how it can be useful.

Also now `onResultProcess` and `useResultProcessor` hooks use generics to get more type-safety.
18 changes: 18 additions & 0 deletions .changeset/twenty-poets-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@graphql-yoga/plugin-response-cache': major
---

New Response Cache Plugin!!!

On top of [`@envelop/response-cache`](https://www.envelop.dev/plugins/use-response-cache), this new plugin allows you to skip execution phase even before all the GraphQL execution phases immediately after the GraphQL request parameters is parsed by Yoga.

Also it doesn't need to have `documentString` stored in somewhere in order to get it back during the execution to generate the cache key.

All the features of the same except for the following:

- `session` factory function takes `GraphQLParams` and `Request` objects instead of GraphQL context as arguments.

- `type SessionIdFactory = (params: GraphQLParams, request: Request) => Maybe<string>`

- `enabled` function takes `GraphQLParams` and `Request` objects instead of GraphQL context as arguments.
- `type EnabledFn = (params: GraphQLParams, request: Request) => boolean`
12 changes: 7 additions & 5 deletions packages/graphql-yoga/src/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type OnRequestParseDoneHook = (
export interface OnRequestParseDoneEventPayload {
params: GraphQLParams
setParams: (params: GraphQLParams) => void
setResult: (result: ResultProcessorInput) => void
}

export type OnResultProcess = (
Expand All @@ -73,16 +74,17 @@ export type ResultProcessorInput = PromiseOrValue<
ExecutionResult | AsyncIterable<ExecutionResult | ExecutionPatchResult>
>

export type ResultProcessor = (
result: ResultProcessorInput,
fetchAPI: FetchAPI,
) => PromiseOrValue<Response>
export type ResultProcessor<
TResult extends ResultProcessorInput = ResultProcessorInput,
> = (result: TResult, fetchAPI: FetchAPI) => PromiseOrValue<Response>

export interface OnResultProcessEventPayload {
request: Request
result: ResultProcessorInput
resultProcessor?: ResultProcessor
setResultProcessor(resultProcessor: ResultProcessor): void
setResultProcessor<TResult extends ResultProcessorInput>(
resultProcessor: ResultProcessor<TResult>,
): void
}

export type OnResponseHook<TServerContext> = (
Expand Down
18 changes: 10 additions & 8 deletions packages/graphql-yoga/src/plugins/useResultProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { Plugin, ResultProcessor, ResultProcessorInput } from './types.js'

export interface ResultProcessorPluginOptions {
processResult: ResultProcessor
match?(request: Request, result: ResultProcessorInput): boolean
export interface ResultProcessorPluginOptions<
TResult extends ResultProcessorInput,
> {
processResult: ResultProcessor<TResult>
match?(request: Request, result: ResultProcessorInput): result is TResult
}

export function useResultProcessor(
options: ResultProcessorPluginOptions,
): Plugin {
const isMatch = options.match || (() => true)
export function useResultProcessor<
TResult extends ResultProcessorInput = ResultProcessorInput,
>(options: ResultProcessorPluginOptions<TResult>): Plugin {
const matchFn = options.match || (() => true)
return {
onResultProcess({ request, result, setResultProcessor }) {
if (isMatch(request, result)) {
if (matchFn(request, result)) {
setResultProcessor(options.processResult)
}
},
Expand Down
78 changes: 50 additions & 28 deletions packages/graphql-yoga/src/processRequest.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,57 @@
import { getOperationAST, ExecutionArgs } from 'graphql'
import { RequestProcessContext } from './types.js'
import { ResultProcessor } from './plugins/types.js'
import { FetchAPI, GraphQLParams } from './types.js'
import {
OnResultProcess,
ResultProcessor,
ResultProcessorInput,
} from './plugins/types.js'
import { GetEnvelopedFn } from '@envelop/core'

export async function processRequest<TContext>({
export async function processResult({
request,
params,
enveloped,
result,
fetchAPI,
onResultProcessHooks,
}: RequestProcessContext<TContext>): Promise<Response> {
}: {
request: Request
result: ResultProcessorInput
fetchAPI: FetchAPI
/**
* Response Hooks
*/
onResultProcessHooks: OnResultProcess[]
}) {
let resultProcessor: ResultProcessor<any> | undefined

for (const onResultProcessHook of onResultProcessHooks) {
await onResultProcessHook({
request,
result,
resultProcessor,
setResultProcessor(newResultProcessor) {
resultProcessor = newResultProcessor
},
})
}

// If no result processor found for this result, return an error
if (!resultProcessor) {
return new fetchAPI.Response(null, {
status: 406,
statusText: 'Not Acceptable',
})
}

return resultProcessor(result, fetchAPI)
}

export async function processRequest<TContext>({
params,
enveloped,
}: {
params: GraphQLParams
enveloped: ReturnType<GetEnvelopedFn<TContext>>
}): Promise<ResultProcessorInput> {
// Parse GraphQLParams
const document = enveloped.parse(params.query!)

Expand Down Expand Up @@ -38,26 +81,5 @@ export async function processRequest<TContext>({
// Get the result to be processed
const result = await executeFn(executionArgs)

let resultProcessor: ResultProcessor | undefined

for (const onResultProcessHook of onResultProcessHooks) {
await onResultProcessHook({
request,
result,
resultProcessor,
setResultProcessor(newResultProcessor) {
resultProcessor = newResultProcessor
},
})
}

// If no result processor found for this result, return an error
if (!resultProcessor) {
return new fetchAPI.Response(null, {
status: 406,
statusText: 'Not Acceptable',
})
}

return resultProcessor(result, fetchAPI)
return result
}
49 changes: 34 additions & 15 deletions packages/graphql-yoga/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ import {
Plugin,
RequestParser,
ResultProcessor,
ResultProcessorInput,
} from './plugins/types.js'
import { createFetch } from '@whatwg-node/fetch'
import { ServerAdapter, createServerAdapter } from '@whatwg-node/server'
import { processRequest as processGraphQLParams } from './processRequest.js'
import {
processRequest as processGraphQLParams,
processResult,
} from './processRequest.js'
import { defaultYogaLogger, titleBold, YogaLogger } from './logger.js'
import { CORSPluginOptions, useCORS } from './plugins/useCORS.js'
import { useHealthCheck } from './plugins/useHealthCheck.js'
Expand Down Expand Up @@ -143,6 +147,7 @@ export type YogaServerOptions<
* Whether the landing page should be shown.
*/
landingPage?: boolean

/**
* GraphiQL options
*
Expand Down Expand Up @@ -383,15 +388,15 @@ export class YogaServer<
// Middlewares after the GraphQL execution
useResultProcessor({
match: isRegularResult,
processResult: processRegularResult as ResultProcessor,
processResult: processRegularResult,
}),
useResultProcessor({
match: isPushResult,
processResult: processPushResult as ResultProcessor,
processResult: processPushResult,
}),
useResultProcessor({
match: isMultipartResult,
processResult: processMultipartResult as ResultProcessor,
processResult: processMultipartResult,
}),
...(options?.plugins ?? []),

Expand Down Expand Up @@ -491,34 +496,48 @@ export class YogaServer<

let params = await requestParser(request)

let result: ResultProcessorInput | undefined

for (const onRequestParseDone of onRequestParseDoneList) {
await onRequestParseDone({
params,
setParams(newParams: GraphQLParams) {
params = newParams
},
setResult(earlyResult: ResultProcessorInput) {
result = earlyResult
},
})
if (result) {
break
}
}

const initialContext = {
request,
...params,
...serverContext,
}
if (result == null) {
const initialContext = {
request,
...params,
...serverContext,
}

const enveloped = this.getEnveloped(initialContext)

const enveloped = this.getEnveloped(initialContext)
this.logger.debug(`Processing GraphQL Parameters`)

this.logger.debug(`Processing GraphQL Parameters`)
result = await processGraphQLParams({
params,
enveloped,
})
}

const result = await processGraphQLParams({
const response = await processResult({
request,
params,
enveloped,
result,
fetchAPI: this.fetchAPI,
onResultProcessHooks: this.onResultProcessHooks,
})

return result
return response
} catch (error: unknown) {
const finalResponseInit = {
status: 200,
Expand Down
71 changes: 23 additions & 48 deletions packages/graphql-yoga/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import type {
DocumentNode,
ExecutionResult,
GraphQLError,
OperationDefinitionNode,
} from 'graphql'
import type { DocumentNode, GraphQLError } from 'graphql'
import type { TypedDocumentNode } from '@graphql-typed-document-node/core'
import { GetEnvelopedFn, PromiseOrValue } from '@envelop/core'
import { OnResultProcess } from './plugins/types.js'
import { PromiseOrValue } from '@envelop/core'
import { createFetch } from '@whatwg-node/fetch'

export interface ExecutionPatchResult<
TData = { [key: string]: any },
TExtensions = { [key: string]: any },
> {
> {
errors?: ReadonlyArray<GraphQLError>
data?: TData | null
path?: ReadonlyArray<string | number>
Expand All @@ -24,21 +18,13 @@ export interface ExecutionPatchResult<
export interface GraphQLParams<
TVariables = Record<string, any>,
TExtensions = Record<string, any>,
> {
> {
operationName?: string
query?: string
variables?: TVariables
extensions?: TExtensions
}

export interface FormatPayloadParams<TContext, TRootValue> {
payload: ExecutionResult | ExecutionPatchResult
context?: TContext
document?: DocumentNode
operation?: OperationDefinitionNode
rootValue?: TRootValue
}

export interface YogaInitialContext {
/**
* A Document containing GraphQL Operations and Fragments to execute.
Expand All @@ -62,44 +48,33 @@ export interface YogaInitialContext {
extensions?: Record<string, any>
}

export interface RequestProcessContext<TContext> {
request: Request
enveloped: ReturnType<GetEnvelopedFn<TContext>>
params: GraphQLParams
fetchAPI: FetchAPI
/**
* Response Hooks
*/
onResultProcessHooks: OnResultProcess[]
}

export type CORSOptions =
| {
origin?: string[] | string
methods?: string[]
allowedHeaders?: string[]
exposedHeaders?: string[]
credentials?: boolean
maxAge?: number
}
origin?: string[] | string
methods?: string[]
allowedHeaders?: string[]
exposedHeaders?: string[]
credentials?: boolean
maxAge?: number
}
| false

export type GraphQLServerInject<
TData = any,
TVariables = Record<string, any>,
TServerContext extends Record<string, any> = Record<string, any>,
> = {
/** GraphQL Operation to execute */
document: string | TypedDocumentNode<TData, TVariables>
/** Variables for GraphQL Operation */
variables?: TVariables
/** Name for GraphQL Operation */
operationName?: string
/** Set any headers for the GraphQL request */
headers?: HeadersInit
} & ({} extends TServerContext
? { serverContext?: TServerContext }
: { serverContext: TServerContext })
> = {
/** GraphQL Operation to execute */
document: string | TypedDocumentNode<TData, TVariables>
/** Variables for GraphQL Operation */
variables?: TVariables
/** Name for GraphQL Operation */
operationName?: string
/** Set any headers for the GraphQL request */
headers?: HeadersInit
} & ({} extends TServerContext
? { serverContext?: TServerContext }
: { serverContext: TServerContext })

export { EnvelopError as GraphQLYogaError } from '@envelop/core'

Expand Down
3 changes: 3 additions & 0 deletions packages/plugins/response-cache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @graphql-yoga/plugin-response-cache

For the documentation check `http://graphql-yoga.com/docs/response-cache`
Loading

0 comments on commit 5629a5c

Please sign in to comment.