Skip to content

Instantly share code, notes, and snippets.

@chregu
Last active April 11, 2023 08:52
Show Gist options
  • Save chregu/8e3edf2a07330171a1ba217fdb07e96d to your computer and use it in GitHub Desktop.
Save chregu/8e3edf2a07330171a1ba217fdb07e96d to your computer and use it in GitHub Desktop.
Middleware Caching Vendure
import { caching } from 'cache-manager'
import redisStore from 'cache-manager-ioredis'
import crypto from 'crypto'
import { NextFunction, Request, Response, Send } from 'express'
import { DocumentNode, parse } from 'graphql'
import { DefinitionNode } from 'graphql/language/ast'
const redisCacheConfig = {
store: redisStore,
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6457,
password: process.env.REDIS_PASSWORD,
ttl: 60,
db: 2,
}
const cache = caching(redisCacheConfig)
type QueryConfig = {
ttl: number
}
const defaultConfig: QueryConfig = { ttl: 300 }
// only these queries will be cached
const config: Record<string, QueryConfig> = {
products: defaultConfig,
pagesBySection: defaultConfig,
}
// parse graphql query, from https://stackoverflow.com/a/59055312
const operationDefinitions = (ast: DocumentNode): DefinitionNode[] =>
ast.definitions.filter((d: any) => d.kind === 'OperationDefinition')
const fieldValueNamesFromOperation = (operationDefinitions: DefinitionNode[]): QueryNames[] => {
return operationDefinitions
.map((operationDefinition: any) => {
if ('selectionSet' in operationDefinition) {
return operationDefinition.selectionSet.selections.map((s: any) => s.name.value)
}
return []
})
.flat()
}
export const middlewareCaching = async (req: Request, res: Response, next: NextFunction) => {
// only cache post requests, if you do graphql via GET, change this
if (req.method !== 'POST') {
next()
return
}
// not sure, if this is the correct way to do it,so that the body is available later again
const body = req.body
req.body = body
// parse graphql query, from https://stackoverflow.com/a/59055312
const parsedQuery = parse(body.query)
const operations = operationDefinitions(parsedQuery)
if (operations.length === 0) {
next()
return
}
// don't cache, if there's a mutation in it
if (operations.find((o: any) => o.operation === 'mutation')) {
next()
return
}
const queryNames = fieldValueNamesFromOperation(operations)
//dont cache schema
if (queryNames.includes('__schema')) {
next()
return
}
// no queryNames found, don't cache
if (queryNames.length === 0) {
next()
return
}
for (const queryName of queryNames) {
// don't cache whole query, if one query is not in config
if (!config[queryName]) {
next()
return
}
}
const shasum = crypto.createHash('sha1')
shasum.update(JSON.stringify(body) + JSON.stringify(req.query))
const key = `graphql:${queryNames}:${shasum.digest('hex')}`
try {
const obj = await cache.get(key)
if (obj) {
res.send(obj)
return
}
type ResponseWithSend = Response & { sendResponse: Send }
const responseWithSend = res as ResponseWithSend
responseWithSend.sendResponse = res.send
const ttl = Math.min(...queryNames.map(queryName => config[queryName]?.ttl || defaultConfig.ttl))
res.send = body => {
console.log(`set cache for query ${queryNames} with key ${key} and ttl: ${ttl}`)
cache.set(key, body, { ttl: ttl })
return responseWithSend.sendResponse(body)
}
} catch (e) {
console.error(e)
}
next()
}
@chregu
Copy link
Author

chregu commented Apr 11, 2023

And put this into your vendure-config.ts

apiOptions: {

  middleware: [
            {
                handler: middlewareCaching,
                route: '/shop-api',
            },
        ],
...
}

This will cache some of your GraphQL Queries (the one defined in the config variable). Make sure you don't cache user specific stuff and also make sure to clear the cache, when something changes (like stocks, so basically you should clear the cache, whenever someone buys something and you do stock checking via the query)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment