Skip to content

Commit

Permalink
fix: cache query results client-side
Browse files Browse the repository at this point in the history
  • Loading branch information
johannschopplich committed Dec 6, 2022
1 parent 69375ec commit c40b28e
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 32 deletions.
2 changes: 1 addition & 1 deletion docs/guide/faq-are-cors-issues-possible.md
Expand Up @@ -6,4 +6,4 @@ No.

## Detailed Answer

With the default module configuration, you won't have to deal with CORS issues. [`useKql`](/api/use-kql) and [`$kql`](/api/kql) pass given queries to the Nuxt server route `/api/__kql__`. The query is fetched from the Kirby instance on the server-side and then passed back to the client. Thus, no data is fetched from the Kirby instance on the client-side. It is proxied by the Nuxt server and no CORS issues will occur.
With the default module configuration, you won't have to deal with CORS issues. [`useKql`](/api/use-kql) and [`$kql`](/api/kql) pass given queries to the Nuxt server route `/api/__kql`. The query is fetched from the Kirby instance on the server-side and then passed back to the client. Thus, no data is fetched from the Kirby instance on the client-side. It is proxied by the Nuxt server and no CORS issues will occur.
4 changes: 2 additions & 2 deletions docs/guide/how-it-works.md
Expand Up @@ -2,11 +2,11 @@

## tl;dr

With a internal `/api/__kql__` server route proxying your query request.
With a internal `/api/__kql` server route proxying your query request.

## Detailed Answer

The [`useKql`](/api/use-kql) and [`$kql`](/api/kql) composables will initiate a POST request to the Nuxt server route `/api/__kql__` defined by this module. The KQL query will be encoded in the request body.
The [`useKql`](/api/use-kql) and [`$kql`](/api/kql) composables will initiate a POST request to the Nuxt server route `/api/__kql` defined by this module. The KQL query will be encoded in the request body.

The internal server route then fetches the actual data for a given query from the Kirby instance and passes the response back to the template/client. Thus, no KQL requests to the Kirby instance are initiated on the client-side. This proxy behaviour has the benefit of omitting CORS issues, since data is sent from server to server.

Expand Down
6 changes: 3 additions & 3 deletions src/module.ts
Expand Up @@ -3,7 +3,7 @@ import { defu } from 'defu'
import { pascalCase } from 'scule'
import { addImportsDir, addServerHandler, addTemplate, createResolver, defineNuxtModule } from '@nuxt/kit'
import type { KirbyQueryRequest } from 'kirby-fest'
import { kirbyApiRoute, kqlApiRoute } from './runtime/utils'
import { KIRBY_API_ROUTE, KQL_API_ROUTE } from './runtime/utils'
import { logger, prefetchQueries } from './utils'

export interface ModuleOptions {
Expand Down Expand Up @@ -142,14 +142,14 @@ export default defineNuxtModule<ModuleOptions>({

// Add KQL proxy endpoint to send queries server-side
addServerHandler({
route: kqlApiRoute,
route: KQL_API_ROUTE,
method: 'post',
handler: resolve('runtime/server/api/kql'),
})

// Add another proxy endpoint to fetch raw Kirby data server-side
addServerHandler({
route: kirbyApiRoute,
route: KIRBY_API_ROUTE,
method: 'post',
handler: resolve('runtime/server/api/kirby'),
})
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/composables/$kirby.ts
@@ -1,7 +1,7 @@
import { hash } from 'ohash'
import { joinURL } from 'ufo'
import type { FetchOptions } from 'ofetch'
import { clientErrorMessage, getAuthHeader, headersToObject, kirbyApiRoute } from '../utils'
import { DEFAULT_CLIENT_ERROR, KIRBY_API_ROUTE, getAuthHeader, headersToObject } from '../utils'
import type { ModuleOptions } from '../../module'
import { useNuxtApp, useRuntimeConfig } from '#imports'

Expand Down Expand Up @@ -29,7 +29,7 @@ export function $kirby<T = any>(
const { headers, client = false, ...fetchOptions } = opts

if (client && !kql.client)
throw new Error(clientErrorMessage)
throw new Error(DEFAULT_CLIENT_ERROR)

const promiseMap: Map<string, Promise<T>> = nuxt._promiseMap = nuxt._promiseMap || new Map()
const key = `$kirby${hash(uri)}`
Expand Down Expand Up @@ -59,7 +59,7 @@ export function $kirby<T = any>(
},
}

const request = $fetch(client ? joinURL(kql.url, uri) : kirbyApiRoute, {
const request = $fetch(client ? joinURL(kql.url, uri) : KIRBY_API_ROUTE, {
...fetchOptions,
...(client ? _publicFetchOptions : _fetchOptions),
}).then((response) => {
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/composables/$kql.ts
Expand Up @@ -2,7 +2,7 @@ import { hash } from 'ohash'
import { joinURL } from 'ufo'
import type { FetchOptions } from 'ofetch'
import type { KirbyQueryRequest, KirbyQueryResponse } from 'kirby-fest'
import { clientErrorMessage, getAuthHeader, headersToObject, kqlApiRoute } from '../utils'
import { DEFAULT_CLIENT_ERROR, KQL_API_ROUTE, getAuthHeader, headersToObject } from '../utils'
import type { ModuleOptions } from '../../module'
import { useNuxtApp, useRuntimeConfig } from '#imports'

Expand Down Expand Up @@ -34,7 +34,7 @@ export function $kql<T extends KirbyQueryResponse = KirbyQueryResponse>(
const { headers, language, client = false, ...fetchOptions } = opts

if (client && !kql.client)
throw new Error(clientErrorMessage)
throw new Error(DEFAULT_CLIENT_ERROR)

const promiseMap: Map<string, Promise<T>> = nuxt._promiseMap = nuxt._promiseMap || new Map()
const key = `$kql${hash(query)}`
Expand Down Expand Up @@ -71,7 +71,7 @@ export function $kql<T extends KirbyQueryResponse = KirbyQueryResponse>(
},
}

const request = $fetch(client ? joinURL(kql.url, kql.prefix) : kqlApiRoute, {
const request = $fetch(client ? joinURL(kql.url, kql.prefix) : KQL_API_ROUTE, {
...fetchOptions,
...(client ? _publicFetchOptions : _fetchOptions),
}).then((response) => {
Expand Down
35 changes: 26 additions & 9 deletions src/runtime/composables/useKirbyData.ts
@@ -1,11 +1,10 @@
import { computed, reactive } from 'vue'
import { hash } from 'ohash'
import { joinURL } from 'ufo'
import type { FetchError, FetchOptions } from 'ofetch'
import type { AsyncData, AsyncDataOptions } from 'nuxt/app'
import { resolveUnref } from '@vueuse/core'
import type { MaybeComputedRef } from '@vueuse/core'
import { clientErrorMessage, getAuthHeader, headersToObject, kirbyApiRoute } from '../utils'
import { DEFAULT_CLIENT_ERROR, KIRBY_API_ROUTE, getAuthHeader, headersToObject } from '../utils'
import type { ModuleOptions } from '../../module'
import { useAsyncData, useRuntimeConfig } from '#imports'

Expand Down Expand Up @@ -36,8 +35,10 @@ export function useKirbyData<T = any>(
uri: MaybeComputedRef<string>,
opts: UseKirbyDataOptions<T> = {},
) {
const nuxt = useNuxtApp()
const { kql } = useRuntimeConfig().public
const _uri = computed(() => resolveUnref(uri).replace(/^\//, ''))
const key = `$kirby${hash(_uri.value)}`

const {
server,
Expand All @@ -54,7 +55,7 @@ export function useKirbyData<T = any>(
console.error('[useKirbyData] Empty Kirby URI')

if (client && !kql.client)
throw new Error(clientErrorMessage)
throw new Error(DEFAULT_CLIENT_ERROR)

const asyncDataOptions: AsyncDataOptions<T> = {
server,
Expand All @@ -75,7 +76,8 @@ export function useKirbyData<T = any>(
},
})

const _publicFetchOptions = {
const _publicFetchOptions: FetchOptions = {
baseURL: kql.url,
headers: {
...headersToObject(headers),
...getAuthHeader({
Expand All @@ -89,16 +91,31 @@ export function useKirbyData<T = any>(
let controller: AbortController

return useAsyncData<T, FetchError>(
`$kirby${hash(_uri.value)}`,
() => {
key,
async () => {
controller?.abort?.()
controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController

return $fetch(client ? joinURL(kql.url, _uri.value) : kirbyApiRoute, {
if (key in nuxt.payload.data)
return Promise.resolve(nuxt.payload.data[key])

if (key in nuxt.static.data)
return Promise.resolve(nuxt.static.data[key])

controller = typeof AbortController !== 'undefined'
? new AbortController()
: ({} as AbortController)

const result = (await $fetch<T>(client ? _uri.value : KIRBY_API_ROUTE, {
...fetchOptions,
signal: controller.signal,
...(client ? _publicFetchOptions : _fetchOptions),
}) as Promise<T>
})) as T

// Workaround to persist response client-side
// https://github.com/nuxt/framework/issues/8917
nuxt.static.data[key] = result

return result
},
asyncDataOptions,
) as AsyncData<T, FetchError | null | true>
Expand Down
33 changes: 25 additions & 8 deletions src/runtime/composables/useKql.ts
@@ -1,12 +1,11 @@
import { computed, reactive } from 'vue'
import { hash } from 'ohash'
import { joinURL } from 'ufo'
import type { FetchError, FetchOptions } from 'ofetch'
import type { AsyncData, AsyncDataOptions } from 'nuxt/app'
import type { KirbyQueryRequest, KirbyQueryResponse } from 'kirby-fest'
import { resolveUnref } from '@vueuse/core'
import type { MaybeComputedRef } from '@vueuse/core'
import { clientErrorMessage, getAuthHeader, headersToObject, kqlApiRoute } from '../utils'
import { DEFAULT_CLIENT_ERROR, KQL_API_ROUTE, getAuthHeader, headersToObject } from '../utils'
import type { ModuleOptions } from '../../module'
import { useAsyncData, useRuntimeConfig } from '#imports'

Expand Down Expand Up @@ -41,8 +40,10 @@ export function useKql<
ResT extends KirbyQueryResponse = KirbyQueryResponse,
ReqT extends KirbyQueryRequest = KirbyQueryRequest,
>(query: MaybeComputedRef<ReqT>, opts: UseKqlOptions<ResT> = {}) {
const nuxt = useNuxtApp()
const { kql } = useRuntimeConfig().public
const _query = computed(() => resolveUnref(query))
const key = `$kql${hash(_query.value)}`

const {
server,
Expand All @@ -60,7 +61,7 @@ export function useKql<
console.error('[useKql] Empty KQL query')

if (client && !kql.client)
throw new Error(clientErrorMessage)
throw new Error(DEFAULT_CLIENT_ERROR)

const asyncDataOptions: AsyncDataOptions<ResT> = {
server,
Expand All @@ -87,6 +88,7 @@ export function useKql<
})

const _publicFetchOptions = reactive<FetchOptions>({
baseURL: kql.url,
method: 'POST',
body: _query,
headers: {
Expand All @@ -102,16 +104,31 @@ export function useKql<
let controller: AbortController

return useAsyncData<ResT, FetchError>(
`$kql${hash(_query.value)}`,
() => {
key,
async () => {
controller?.abort?.()
controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController

return $fetch(client ? joinURL(kql.url, kql.prefix) : kqlApiRoute, {
if (key in nuxt.payload.data)
return Promise.resolve(nuxt.payload.data[key])

if (key in nuxt.static.data)
return Promise.resolve(nuxt.static.data[key])

controller = typeof AbortController !== 'undefined'
? new AbortController()
: ({} as AbortController)

const result = (await $fetch<ResT>(client ? kql.prefix : KQL_API_ROUTE, {
...fetchOptions,
signal: controller.signal,
...(client ? _publicFetchOptions : _fetchOptions),
}) as Promise<ResT>
})) as ResT

// Workaround to persist response client-side
// https://github.com/nuxt/framework/issues/8917
nuxt.static.data[key] = result

return result
},
asyncDataOptions,
) as AsyncData<ResT, FetchError | null | true>
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/utils.ts
@@ -1,8 +1,8 @@
import type { ModuleOptions } from '../module'

export const kqlApiRoute = '/api/__kql__' as const
export const kirbyApiRoute = '/api/__kirby__' as const
export const clientErrorMessage = 'Fetching from Kirby client-side isn\'t allowed. Enable it by setting the module option "client" to "true" in your "nuxt.config.ts".'
export const KQL_API_ROUTE = '/api/__kql'
export const KIRBY_API_ROUTE = '/api/__kirby'
export const DEFAULT_CLIENT_ERROR = 'Fetching from Kirby client-side isn\'t allowed. Enable it by setting the module option "client" to "true" in your "nuxt.config.ts".'

export function headersToObject(headers: HeadersInit = {}): Record<string, string> {
// SSR compatibility for `Headers` prototype
Expand Down

0 comments on commit c40b28e

Please sign in to comment.