Skip to content

Instantly share code, notes, and snippets.

@luksak
Last active November 21, 2024 17:03
Show Gist options
  • Save luksak/def035c8ed37892d967a05186efa180a to your computer and use it in GitHub Desktop.
Save luksak/def035c8ed37892d967a05186efa180a to your computer and use it in GitHub Desktop.
Integration of nuxt-graphql-middleware with nuxt-multi-cache with full cache tag propagation
export default defineNuxtPlugin((NuxtApp) => {
const state = useGraphqlState()
const event = useRequestEvent()
if (!state) {
return
}
state.fetchOptions = {
onResponse ({ request, response, options }) {
if (response._data.__cacheTags) {
const cacheTags = response._data.__cacheTags
useCDNHeaders((helper) => {
helper
.public()
.setNumeric('maxAge', 3600)
.set('staleIfError', 86400)
.set('staleWhileRevalidate', 86400)
.set('sharedMaxAge', 2592000)
.addTags(cacheTags)
}, event)
}
}
}
})
import { H3Event } from 'h3'
import { defineGraphqlServerOptions } from 'nuxt-graphql-middleware/dist/runtime/serverOptions'
import { GraphqlMiddlewareOperation } from 'nuxt-graphql-middleware/dist/runtime/settings'
import type { GraphqlMiddlewareRuntimeConfig } from 'nuxt-graphql-middleware/dist/runtime/types'
// @todo: Mocking the missing functions from nuxt-graphql-middleware
import { getFetchOptions, onServerError, onServerResponse, getEndpoint } from '../utils/middlewareFunctions'
import serverOptions from '#graphql-middleware-server-options-build'
import { useDataCache, useCDNHeaders } from '#nuxt-multi-cache/composables'
import useAddCDNHeaders from '~/composables/useAddCDNHeaders'
export default defineGraphqlServerOptions({
/**
* Alter response from GraphQL server.
*/
onServerResponse (event, graphqlResponse) {
const surrogateKey = graphqlResponse.headers.get('Surrogate-Key')
const cacheTags = surrogateKey.split(' ')
return { ...graphqlResponse._data, __cacheTags: cacheTags }
},
/**
* Cache requests in nuxt-multi-cache
*/
async doGraphqlRequest ({
event,
operationName,
operationDocument,
variables
}) {
// The operation (either "query" or "mutation").
const operation = event.context?.params
?.operation as GraphqlMiddlewareOperation
// Determine the endpoint of the GraphQL server.
// Get the fetch options for this request.
const fetchOptions = await getFetchOptions(
serverOptions,
event,
operation,
operationName
)
// Get the runtime config.
const runtimeConfig = useRuntimeConfig()
.graphqlMiddleware as GraphqlMiddlewareRuntimeConfig
// Determine the endpoint of the GraphQL server.
const endpoint = await getEndpoint(
runtimeConfig,
serverOptions,
event,
operation,
operationName
)
const cacheKey = `${operationName}|${JSON.stringify(variables)}`
const { value: cachedValue, addToCache } = await useDataCache(cacheKey, event)
// If the data is already in cache, return it
if (cachedValue) {
useAddCDNHeaders(event, cachedValue.__cacheTags)
return cachedValue
}
// The data isn't available in cache. We have to fetch it.
const response = await $fetch
.raw(endpoint, {
// @todo: Remove any once https://github.com/unjs/nitro/pull/883 is released.
method: 'POST' as any,
body: {
query: operationDocument,
variables,
operationName
},
...fetchOptions
})
.then((response) => {
return onServerResponse(
serverOptions,
event,
response,
operation,
operationName
)
})
.catch((error) => {
return onServerError(
serverOptions,
event,
error,
operation,
operationName
)
})
useAddCDNHeaders(event, response.__cacheTags)
// Save data to the cache and return it.
addToCache(response)
return response
}
})
// This file only exists since nuxt-graphql-middleware doesn't export all functions needed in ../app/graphqlMiddleware.serverOptions.ts
import type { H3Event } from 'h3'
import type { GraphqlMiddlewareServerOptions, GraphqlMiddlewareRuntimeConfig } from 'nuxt-graphql-middleware/dist/runtime/types'
import { GraphqlMiddlewareOperation } from 'nuxt-graphql-middleware/dist/runtime/settings'
import type { FetchOptions, FetchResponse, FetchError } from 'ofetch'
export function getFetchOptions (
serverOptions: GraphqlMiddlewareServerOptions,
event: H3Event,
operation: GraphqlMiddlewareOperation,
operationName: string
): FetchOptions | Promise<FetchOptions> {
if (serverOptions.serverFetchOptions) {
return (
serverOptions.serverFetchOptions(event, operation, operationName) || {}
)
}
return {}
}
/**
* Handle GraphQL server response.
*/
export function onServerResponse (
serverOptions: GraphqlMiddlewareServerOptions,
event: H3Event,
response: FetchResponse<any>,
operation?: string,
operationName?: string
) {
if (serverOptions.onServerResponse) {
return serverOptions.onServerResponse(
event,
response,
operation,
operationName
)
}
return response._data
}
/**
* Handle GraphQL server errors.
*/
export function onServerError (
serverOptions: GraphqlMiddlewareServerOptions,
event: H3Event,
error: FetchError,
operation?: string,
operationName?: string
) {
if (serverOptions.onServerError) {
return serverOptions.onServerError(event, error, operation, operationName)
}
const message = error && 'message' in error ? error.message : ''
throw createError({
statusCode: 500,
statusMessage: "Couldn't execute GraphQL query: " + message,
data: error && 'message' in error ? error.message : error
})
}
/**
* Get the URL of the GraphQL endpoint.
*/
export function getEndpoint (
config: GraphqlMiddlewareRuntimeConfig,
serverOptions: GraphqlMiddlewareServerOptions,
event: H3Event,
operation: GraphqlMiddlewareOperation,
operationName: string
): string | Promise<string> {
// Check if a custom graphqlEndpoint method exists.
if (serverOptions.graphqlEndpoint) {
const result = serverOptions.graphqlEndpoint(
event,
operation,
operationName
)
// Only return if the method returned somethind. This way we fall back to
// config at build time.
if (result) {
return Promise.resolve(result)
}
}
if (config.graphqlEndpoint) {
return config.graphqlEndpoint
}
throw new Error('Failed to determine endpoint for GraphQL server.')
}
import { H3Event } from 'h3'
import { useCDNHeaders } from '#nuxt-multi-cache/composables'
export default function (event: H3Event, cacheTags: string[]) {
useCDNHeaders((helper) => {
helper
.public()
.setNumeric('maxAge', 3600)
.set('staleIfError', 86400)
.set('staleWhileRevalidate', 86400)
.set('sharedMaxAge', 2592000)
.addTags(cacheTags)
}, event)
}
@plcdnl
Copy link

plcdnl commented Nov 20, 2024

Thank you for the example @luksak ! Could you share also the middleware functions? I would like to use it for Craft CMS too and happy to contribute on your code.

import { getFetchOptions, onServerError, onServerResponse, getEndpoint } from '../utils/middlewareFunctions'

@luksak
Copy link
Author

luksak commented Nov 21, 2024

@plcdnl I just added the file. It actually just mocks the functions that nuxt-graphql-middleware didn't export. So nothing important there. I hope it helps!

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