Skip to content

Commit

Permalink
feat(query): fully cacheable api
Browse files Browse the repository at this point in the history
  • Loading branch information
farnabaz committed Dec 14, 2022
1 parent f6ab8bc commit b46eaf2
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 39 deletions.
31 changes: 21 additions & 10 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,11 @@ export default defineNuxtModule<ModuleOptions>({

// Add server handlers
nitroConfig.handlers.push(
{
method: 'get',
route: `${options.api.baseURL}/query/:qid/**:params`,
handler: resolveRuntimeModule('./server/api/query')
},
{
method: 'get',
route: `${options.api.baseURL}/query/:qid`,
Expand Down Expand Up @@ -444,16 +449,22 @@ export default defineNuxtModule<ModuleOptions>({

nuxt.hook('nitro:config', (nitroConfig) => {
nitroConfig.handlers = nitroConfig.handlers || []
nitroConfig.handlers.push({
method: 'get',
route: `${options.api.baseURL}/navigation/:qid`,
handler: resolveRuntimeModule('./server/api/navigation')
})
nitroConfig.handlers.push({
method: 'get',
route: `${options.api.baseURL}/navigation`,
handler: resolveRuntimeModule('./server/api/navigation')
})
nitroConfig.handlers.push(
{
method: 'get',
route: `${options.api.baseURL}/navigation/:qid/**:params`,
handler: resolveRuntimeModule('./server/api/navigation')
}, {
method: 'get',
route: `${options.api.baseURL}/navigation/:qid`,
handler: resolveRuntimeModule('./server/api/navigation')
},
{
method: 'get',
route: `${options.api.baseURL}/navigation`,
handler: resolveRuntimeModule('./server/api/navigation')
}
)
})
} else {
addImports({ name: 'navigationDisabled', as: 'fetchContentNavigation', from: resolveRuntimeModule('./composables/utils') })
Expand Down
18 changes: 5 additions & 13 deletions src/runtime/composables/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { hash } from 'ohash'
import { useCookie, useRuntimeConfig } from '#app'
import { useRuntimeConfig } from '#app'
import type { NavItem, QueryBuilder, QueryBuilderParams } from '../types'
import { jsonStringify } from '../utils/json'
import { encodeQueryParams } from '../utils/query'
import { addPrerenderPath, shouldUseClientDB, withContentBase } from './utils'

export const fetchContentNavigation = async (queryBuilder?: QueryBuilder | QueryBuilderParams): Promise<Array<NavItem>> => {
const { content } = useRuntimeConfig().public

// When params is an instance of QueryBuilder then we need to pick the params explicitly
const params: QueryBuilderParams = typeof queryBuilder?.params === 'function' ? queryBuilder.params() : queryBuilder
const params: QueryBuilderParams = typeof queryBuilder?.params === 'function' ? queryBuilder.params() : queryBuilder || {}

// Filter by locale if:
// - locales are defined
Expand All @@ -21,8 +21,7 @@ export const fetchContentNavigation = async (queryBuilder?: QueryBuilder | Query
}
}

const _apiPath = params ? `/navigation/${hash(params)}` : '/navigation/'
const apiPath = withContentBase(process.dev ? _apiPath : `${_apiPath}.${content.integrity}.json`)
const apiPath = withContentBase(`/navigation/${process.dev ? '_' : `${hash(params)}.${content.integrity}`}/${encodeQueryParams(params)}.json`)

// Add `prefetch` to `<head>` in production
if (!process.dev && process.server) {
Expand All @@ -34,14 +33,7 @@ export const fetchContentNavigation = async (queryBuilder?: QueryBuilder | Query
return generateNavigation(params)
}

const data = await $fetch<NavItem[]>(apiPath, {
method: 'GET',
responseType: 'json',
params: {
_params: jsonStringify(params || {}),
previewToken: useCookie('previewToken').value
}
})
const data = await $fetch<NavItem[]>(apiPath, { method: 'GET', responseType: 'json' })

// On SSG, all url are redirected to `404.html` when not found, so we need to check the content type
// to know if the response is a valid JSON or not
Expand Down
15 changes: 4 additions & 11 deletions src/runtime/composables/query.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { joinURL, withLeadingSlash, withoutTrailingSlash } from 'ufo'
import { hash } from 'ohash'
import { useCookie, useRuntimeConfig } from '#app'
import { useRuntimeConfig } from '#app'
import { createQuery } from '../query/query'
import type { ParsedContent, QueryBuilder, QueryBuilderParams } from '../types'
import { jsonStringify } from '../utils/json'
import { encodeQueryParams } from '../utils/query'
import { addPrerenderPath, shouldUseClientDB, withContentBase } from './utils'

/**
Expand Down Expand Up @@ -37,7 +37,7 @@ export const createQueryFetch = <T = ParsedContent>(path?: string) => async (que

const params = query.params()

const apiPath = withContentBase(process.dev ? '/query' : `/query/${hash(params)}.${content.integrity}.json`)
const apiPath = withContentBase(`/query/${process.dev ? '_' : `${hash(params)}.${content.integrity}`}/${encodeQueryParams(params)}.json`)

// Prefetch the query
if (!process.dev && process.server) {
Expand All @@ -49,14 +49,7 @@ export const createQueryFetch = <T = ParsedContent>(path?: string) => async (que
return db.fetch(query as QueryBuilder<ParsedContent>)
}

const data = await $fetch(apiPath as any, {
method: 'GET',
responseType: 'json',
params: {
_params: jsonStringify(params),
previewToken: useCookie('previewToken').value
}
})
const data = await $fetch(apiPath as any, { method: 'GET', responseType: 'json' })

// On SSG, all url are redirected to `404.html` when not found, so we need to check the content type
// to know if the response is a valid JSON or not
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/utils/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function regExpReplacer (_key: string, value: any) {
/**
* A function that transforms RegExp string representation back to RegExp objects.
*/
function regExpReviver (_key, value) {
function regExpReviver (_key: string, value: any) {
const withOperator = (typeof value === 'string' && value.match(/^--([A-Z]+) (.+)$/)) || []

if (withOperator[1] === 'REGEX') {
Expand Down
35 changes: 31 additions & 4 deletions src/runtime/utils/query.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
import { getQuery, H3Event, createError } from 'h3'
import { QueryBuilderParams, QueryBuilderWhere } from '../types'
import { jsonParse } from './json'
import { jsonParse, jsonStringify } from './json'

const parseQueryParams = (body: string) => {
const parseJSONQueryParams = (body: string) => {
try {
return jsonParse(body)
} catch (e) {
throw createError({ statusCode: 400, message: 'Invalid _params query' })
}
}

export const encodeQueryParams = (params: QueryBuilderParams) => {
let encoded = jsonStringify(params)
encoded = typeof Buffer !== 'undefined' ? Buffer.from(encoded).toString('base64') : btoa(encoded)

encoded = encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')

// split to chunks of 100 chars
const chunks = encoded.match(/.{1,100}/g) || []
return chunks.join('/')
}

export const decodeQuryParams = (encoded: string) => {
// remove chunks
encoded = encoded.replace(/\//g, '')

// revert base64
encoded = encoded.replace(/-/g, '+').replace(/_/g, '/')
encoded = encoded.padEnd(encoded.length + (4 - (encoded.length % 4)) % 4, '=')

return parseJSONQueryParams(typeof Buffer !== 'undefined' ? Buffer.from(encoded, 'base64').toString() : atob(encoded))
}

const memory: Record<string, QueryBuilderParams> = {}
export const getContentQuery = (event: H3Event): QueryBuilderParams => {
const { params } = event.context.params || {}
if (params) {
return decodeQuryParams(params.replace(/.json$/, ''))
}

const qid = event.context.params.qid?.replace(/.json$/, '')
const query: any = getQuery(event) || {}

// Using /api/_content/query/:qid?_params=....
if (qid && query._params) {
memory[qid] = parseQueryParams(query._params)
memory[qid] = parseJSONQueryParams(query._params)

if (memory[qid].where && !Array.isArray(memory[qid].where)) {
memory[qid].where = [memory[qid].where as any as QueryBuilderWhere]
Expand All @@ -31,7 +58,7 @@ export const getContentQuery = (event: H3Event): QueryBuilderParams => {

// Using /api/_content/query?_params={{JSON_FORMAT}}
if (query._params) {
return parseQueryParams(query._params)
return parseJSONQueryParams(query._params)
}

// Using /api/_content/query?path=...&only=...
Expand Down

0 comments on commit b46eaf2

Please sign in to comment.