Skip to content

Commit

Permalink
Persisted Queries Plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed May 9, 2022
1 parent cc4e454 commit 4fd66f3
Show file tree
Hide file tree
Showing 7 changed files with 424 additions and 85 deletions.
5 changes: 5 additions & 0 deletions .changeset/long-ears-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-yoga/plugin-persisted-queries': major
---

New Persisted Queries Plugin
2 changes: 1 addition & 1 deletion packages/common/src/processRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function processRequest<TContext, TRootValue = {}>({
ReadableStream,
}: RequestProcessContext<TContext, TRootValue>): Promise<Response> {
function getErrorResponse({
status = 500,
status = 500, // default status code
headers,
errors,
isEventStream,
Expand Down
167 changes: 84 additions & 83 deletions packages/common/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
FetchEvent,
FetchAPI,
GraphQLParams,
GraphQLYogaError,
} from './types'
import {
OnRequestParseDoneHook,
Expand Down Expand Up @@ -60,87 +61,87 @@ export type YogaServerOptions<
TServerContext extends Record<string, any>,
TUserContext extends Record<string, any>,
TRootValue,
> = {
/**
* Enable/disable logging or provide a custom logger.
* @default true
*/
logging?: boolean | YogaLogger
/**
* Prevent leaking unexpected errors to the client. We highly recommend enabling this in production.
* If you throw `GraphQLYogaError`/`EnvelopError` within your GraphQL resolvers then that error will be sent back to the client.
*
* You can lean more about this here:
* @see https://graphql-yoga.vercel.app/docs/features/error-masking
*
* Default: `true`
*/
maskedErrors?: boolean | UseMaskedErrorsOpts
/**
* Context
*/
context?:
> = {
/**
* Enable/disable logging or provide a custom logger.
* @default true
*/
logging?: boolean | YogaLogger
/**
* Prevent leaking unexpected errors to the client. We highly recommend enabling this in production.
* If you throw `GraphQLYogaError`/`EnvelopError` within your GraphQL resolvers then that error will be sent back to the client.
*
* You can lean more about this here:
* @see https://graphql-yoga.vercel.app/docs/features/error-masking
*
* Default: `true`
*/
maskedErrors?: boolean | UseMaskedErrorsOpts
/**
* Context
*/
context?:
| ((
initialContext: YogaInitialContext & TServerContext,
) => Promise<TUserContext> | TUserContext)
initialContext: YogaInitialContext & TServerContext,
) => Promise<TUserContext> | TUserContext)
| Promise<TUserContext>
| TUserContext
cors?:
cors?:
| ((
request: Request,
...args: {} extends TServerContext
? [serverContext?: TServerContext | undefined]
: [serverContext: TServerContext]
) => CORSOptions)
request: Request,
...args: {} extends TServerContext
? [serverContext?: TServerContext | undefined]
: [serverContext: TServerContext]
) => CORSOptions)
| CORSOptions
| boolean

/**
* GraphQL endpoint
*/
endpoint?: string

/**
* GraphiQL options
*
* Default: `true`
*/
graphiql?:
/**
* GraphQL endpoint
*/
endpoint?: string

/**
* GraphiQL options
*
* Default: `true`
*/
graphiql?:
| GraphiQLOptions
| ((
request: Request,
...args: {} extends TServerContext
? [serverContext?: TServerContext | undefined]
: [serverContext: TServerContext]
) => GraphiQLOptions | boolean)
request: Request,
...args: {} extends TServerContext
? [serverContext?: TServerContext | undefined]
: [serverContext: TServerContext]
) => GraphiQLOptions | boolean)
| boolean

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

schema?:
schema?:
| GraphQLSchema
| {
typeDefs: TypeSource
resolvers?:
| IResolvers<
TRootValue,
TUserContext & TServerContext & YogaInitialContext
>
| Array<
IResolvers<
TRootValue,
TUserContext & TServerContext & YogaInitialContext
>
>
}
typeDefs: TypeSource
resolvers?:
| IResolvers<
TRootValue,
TUserContext & TServerContext & YogaInitialContext
>
| Array<
IResolvers<
TRootValue,
TUserContext & TServerContext & YogaInitialContext
>
>
}

parserCache?: boolean | ParserCacheOptions
validationCache?: boolean | ValidationCache
fetchAPI?: FetchAPI
multipart?: boolean
} & Partial<
OptionsWithPlugins<TUserContext & TServerContext & YogaInitialContext>
>
parserCache?: boolean | ParserCacheOptions
validationCache?: boolean | ValidationCache
fetchAPI?: FetchAPI
multipart?: boolean
} & Partial<
OptionsWithPlugins<TUserContext & TServerContext & YogaInitialContext>
>

export function getDefaultSchema() {
return makeExecutableSchema({
Expand Down Expand Up @@ -185,7 +186,7 @@ export class YogaServer<
TServerContext extends Record<string, any>,
TUserContext extends Record<string, any>,
TRootValue,
> {
> {
/**
* Instance of envelop
*/
Expand Down Expand Up @@ -233,9 +234,9 @@ export class YogaServer<
? isSchema(options.schema)
? options.schema
: makeExecutableSchema({
typeDefs: options.schema.typeDefs,
resolvers: options.schema.resolvers,
})
typeDefs: options.schema.typeDefs,
resolvers: options.schema.resolvers,
})
: getDefaultSchema()

const logger = options?.logging != null ? options.logging : true
Expand All @@ -244,11 +245,11 @@ export class YogaServer<
? logger === true
? defaultYogaLogger
: {
debug: () => {},
error: () => {},
warn: () => {},
info: () => {},
}
debug: () => { },
error: () => { },
warn: () => { },
info: () => { },
}
: logger

const maskedErrors = options?.maskedErrors ?? true
Expand Down Expand Up @@ -554,8 +555,8 @@ export class YogaServer<
error instanceof GraphQLError
? error
: {
message: error.message,
},
message: error.message,
},
],
}),
{
Expand Down Expand Up @@ -643,17 +644,17 @@ export class YogaServer<

export type YogaServerInstance<TServerContext, TUserContext, TRootValue> =
YogaServer<TServerContext, TUserContext, TRootValue> &
(
| WindowOrWorkerGlobalScope['fetch']
| ((context: { request: Request }) => Promise<Response>)
)
(
| WindowOrWorkerGlobalScope['fetch']
| ((context: { request: Request }) => Promise<Response>)
)

export function createServer<
TServerContext extends Record<string, any> = {},
TUserContext extends Record<string, any> = {},
TRootValue = {},
>(
options?: YogaServerOptions<TServerContext, TUserContext, TRootValue>,
>(
options?: YogaServerOptions<TServerContext, TUserContext, TRootValue>,
): YogaServerInstance<TServerContext, TUserContext, TRootValue> {
const server = new YogaServer<TServerContext, TUserContext, TRootValue>(
options,
Expand Down
49 changes: 49 additions & 0 deletions packages/plugins/persisted-queries/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@graphql-yoga/plugin-persisted-queries",
"version": "0.0.0",
"description": "",
"repository": {
"type": "git",
"url": "https://github.com/dotansimha/graphql-yoga.git",
"directory": "packages/plugins/persisted-queries"
},
"main": "dist/index.js",
"module": "dist/index.mjs",
"scripts": {
"prepack": "bob prepack",
"check": "tsc --pretty --noEmit"
},
"author": "Arda TANRIKULU <[email protected]>",
"license": "MIT",
"buildOptions": {
"input": "./src/index.ts"
},
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./*": {
"require": "./dist/*.js",
"import": "./dist/*.mjs"
}
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
},
"publishConfig": {
"directory": "dist",
"access": "public"
},
"dependencies": {
"tiny-lru": "^8.0.2",
"tslib": "^2.3.1"
},
"peerDependencies": {
"@graphql-yoga/common": "^2.3.0"
},
"devDependencies": {
"bob-the-bundler": "^1.5.1"
}
}
93 changes: 93 additions & 0 deletions packages/plugins/persisted-queries/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { GraphQLYogaError, Plugin, PromiseOrValue } from '@graphql-yoga/common'
import lru from 'tiny-lru'

export interface PersistedQueriesStoreOptions {
max?: number
ttl?: number
}

export function createInMemoryPersistedQueriesStore(
options: PersistedQueriesStoreOptions = {},
) {
return lru<string>(options.max ?? 1000, options.ttl ?? 36000)
}

export interface PersistedQueriesOptions {
store?: PersistedQueriesStore
mode?: PersistedQueriesMode
hash?: (str: string) => PromiseOrValue<string>
}

export enum PersistedQueriesMode {
AUTOMATIC = 'AUTOMATIC',
MANUAL = 'MANUAL',
PERSISTED_ONLY = 'PERSISTED_ONLY',
}

export interface PersistedQueriesStore {
get(key: string): PromiseOrValue<string | null | undefined>
set?(key: string, query: string): PromiseOrValue<any>
}

export interface PersistedQueryExtension {
version: 1
sha256Hash: string
}

export function usePersistedQueries<TPluginContext>(
options: PersistedQueriesOptions = {},
): Plugin<TPluginContext> {
const {
mode = PersistedQueriesMode.AUTOMATIC,
store = createInMemoryPersistedQueriesStore(),
hash,
} = options
if (mode === PersistedQueriesMode.AUTOMATIC && store.set == null) {
throw new Error(
`Automatic Persisted Queries require "set" method to be implemented`,
)
}
return {
onRequestParse() {
return {
onRequestParseDone: async function persistedQueriesOnRequestParseDone({
params,
setParams,
}) {
const persistedQueryData: PersistedQueryExtension =
params.extensions?.persistedQuery
if (
mode === PersistedQueriesMode.PERSISTED_ONLY &&
persistedQueryData == null
) {
throw new GraphQLYogaError('PersistedQueryOnly')
}
if (persistedQueryData?.version === 1) {
if (params.query == null) {
const persistedQuery = await store.get(
persistedQueryData.sha256Hash,
)
if (persistedQuery == null) {
throw new GraphQLYogaError('PersistedQueryNotFound')
}
setParams({
...params,
query: persistedQuery,
})
} else {
if (hash != null) {
const expectedHash = await hash(params.query)
if (persistedQueryData.sha256Hash !== expectedHash) {
throw new GraphQLYogaError('PersistedQueryMismatch')
}
}
if (mode === PersistedQueriesMode.AUTOMATIC) {
await store.set!(persistedQueryData.sha256Hash, params.query)
}
}
}
},
}
},
}
}
Loading

0 comments on commit 4fd66f3

Please sign in to comment.