Skip to content

Commit

Permalink
Persisted Queries Plugin (#1137)
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>

* Persisted Queries Plugin

* Move docs to new home

* Use v3 API

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • Loading branch information
ardatan and renovate[bot] committed Jul 27, 2022
1 parent 3541a88 commit 3eebe6f
Show file tree
Hide file tree
Showing 6 changed files with 705 additions and 46 deletions.
8 changes: 8 additions & 0 deletions .changeset/long-ears-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@graphql-yoga/plugin-persisted-queries': major
---

New Persisted Queries Plugin

See the docs for more information;
https://www.graphql-yoga.com/docs/features/persisted-queries
68 changes: 68 additions & 0 deletions packages/plugins/persisted-queries/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"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/cjs/index.js",
"module": "dist/esm/index.js",
"scripts": {
"prepack": "bob prepack",
"check": "tsc --pretty --noEmit"
},
"author": "Arda TANRIKULU <ardatanrikulu@gmail.com>",
"license": "MIT",
"exports": {
".": {
"require": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
},
"default": {
"types": "./dist/typings/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"./*": {
"require": {
"types": "./dist/typings/*.d.ts",
"default": "./dist/cjs/*.js"
},
"import": {
"types": "./dist/typings/*.d.ts",
"default": "./dist/esm/*.js"
},
"default": {
"types": "./dist/typings/*.d.ts",
"default": "./dist/esm/*.js"
}
},
"./package.json": "./package.json"
},
"typings": "dist/typings/index.d.ts",
"typescript": {
"definition": "dist/typings/index.d.ts"
},
"publishConfig": {
"directory": "dist",
"access": "public"
},
"dependencies": {
"tiny-lru": "^8.0.2",
"tslib": "^2.3.1"
},
"peerDependencies": {
"graphql-yoga": "^2.13.4"
},
"devDependencies": {
"bob-the-bundler": "^1.5.1"
},
"type": "module"
}
94 changes: 94 additions & 0 deletions packages/plugins/persisted-queries/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Plugin, PromiseOrValue } from 'graphql-yoga'
import { GraphQLError } from 'graphql'
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 GraphQLError('PersistedQueryOnly')
}
if (persistedQueryData?.version === 1) {
if (params.query == null) {
const persistedQuery = await store.get(
persistedQueryData.sha256Hash,
)
if (persistedQuery == null) {
throw new GraphQLError('PersistedQueryNotFound')
}
setParams({
...params,
query: persistedQuery,
})
} else {
if (hash != null) {
const expectedHash = await hash(params.query)
if (persistedQueryData.sha256Hash !== expectedHash) {
throw new GraphQLError('PersistedQueryMismatch')
}
}
if (mode === PersistedQueriesMode.AUTOMATIC) {
await store.set!(persistedQueryData.sha256Hash, params.query)
}
}
}
},
}
},
}
}
190 changes: 190 additions & 0 deletions packages/plugins/persisted-queries/tests/persisted-queries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { createYoga, YogaServerInstance } from 'graphql-yoga'
import request from 'supertest'
import {
createInMemoryPersistedQueriesStore,
PersistedQueriesMode,
PersistedQueriesStore,
usePersistedQueries,
} from '../src'

describe('Persisted Queries', () => {
let yoga: YogaServerInstance<any, any, any>
let store: ReturnType<typeof createInMemoryPersistedQueriesStore>
beforeAll(async () => {
store = createInMemoryPersistedQueriesStore()
yoga = createYoga({
plugins: [
usePersistedQueries({
store,
}),
],
})
})
afterAll(() => {
store.clear()
})
it('should return not found error if persisted query is missing', async () => {
const response = await request(yoga)
.post('/graphql')
.send({
extensions: {
persistedQuery: {
version: 1,
sha256Hash:
'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38',
},
},
})

const body = JSON.parse(response.text)
expect(body.errors).toBeDefined()
expect(body.errors[0].message).toBe('PersistedQueryNotFound')
})
it('should load the persisted query when stored', async () => {
const persistedQueryEntry = {
version: 1,
sha256Hash:
'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38',
}
store.set(persistedQueryEntry.sha256Hash, '{__typename}')
const response = await request(yoga)
.post('/graphql')
.send({
extensions: {
persistedQuery: persistedQueryEntry,
},
})

const body = JSON.parse(response.text)
expect(body.errors).toBeUndefined()
expect(body.data.__typename).toBe('Query')
})
describe('Automatic', () => {
beforeAll(async () => {
store = createInMemoryPersistedQueriesStore()
yoga = createYoga({
plugins: [
usePersistedQueries({
store,
mode: PersistedQueriesMode.AUTOMATIC,
}),
],
})
})
it('should save the persisted query', async () => {
const persistedQueryEntry = {
version: 1,
sha256Hash:
'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38',
}
const query = `{__typename}`
const response = await request(yoga)
.post('/graphql')
.send({
query,
extensions: {
persistedQuery: persistedQueryEntry,
},
})

const entry = store.get(persistedQueryEntry.sha256Hash)
expect(entry).toBe(query)

const body = JSON.parse(response.text)
expect(body.errors).toBeUndefined()
expect(body.data.__typename).toBe('Query')
})
})
describe('Persisted Only', () => {
beforeAll(async () => {
store = createInMemoryPersistedQueriesStore()
yoga = createYoga({
plugins: [
usePersistedQueries({
store,
mode: PersistedQueriesMode.PERSISTED_ONLY,
}),
],
})
})
it('should not allow regular queries', async () => {
const query = `{__typename}`
const response = await request(yoga).post('/graphql').send({
query,
})

const body = JSON.parse(response.text)
expect(body.errors).toBeDefined()
expect(body.errors[0].message).toBe('PersistedQueryOnly')
})
it('should not save the persisted query', async () => {
const persistedQueryEntry = {
version: 1,
sha256Hash:
'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38',
}
const query = `{__typename}`
const response = await request(yoga)
.post('/graphql')
.send({
query,
extensions: {
persistedQuery: persistedQueryEntry,
},
})

const entry = store.get(persistedQueryEntry.sha256Hash)
expect(entry).toBeFalsy()

const body = JSON.parse(response.text)
expect(body.errors).toBeUndefined()
expect(body.data.__typename).toBe('Query')
})
})
describe('Manual', () => {
beforeAll(async () => {
store = createInMemoryPersistedQueriesStore()
yoga = createYoga({
plugins: [
usePersistedQueries({
store,
mode: PersistedQueriesMode.MANUAL,
}),
],
})
})
it('should allow regular queries', async () => {
const query = `{__typename}`
const response = await request(yoga).post('/graphql').send({
query,
})

const body = JSON.parse(response.text)
expect(body.errors).toBeUndefined()
expect(body.data.__typename).toBe('Query')
})
it('should not save the persisted query', async () => {
const persistedQueryEntry = {
version: 1,
sha256Hash:
'ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38',
}
const query = `{__typename}`
const response = await request(yoga)
.post('/graphql')
.send({
query,
extensions: {
persistedQuery: persistedQueryEntry,
},
})

const entry = store.get(persistedQueryEntry.sha256Hash)
expect(entry).toBeFalsy()

const body = JSON.parse(response.text)
expect(body.errors).toBeUndefined()
expect(body.data.__typename).toBe('Query')
})
})
})
Loading

0 comments on commit 3eebe6f

Please sign in to comment.