Skip to content

Commit

Permalink
fix(query): fallback to default locale if query has no filter on `_lo…
Browse files Browse the repository at this point in the history
…cale` (#1748)
  • Loading branch information
farnabaz committed Dec 14, 2022
1 parent 9ec3906 commit 90b358a
Show file tree
Hide file tree
Showing 16 changed files with 250 additions and 184 deletions.
9 changes: 9 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ export default defineNuxtModule<ModuleOptions>({
const { resolve } = createResolver(import.meta.url)
const resolveRuntimeModule = (path: string) => resolveModule(path, { paths: resolve('./runtime') })

// Ensure default locale alway is the first item of locales
options.locales = Array.from(new Set([options.defaultLocale, ...options.locales].filter(Boolean))) as string[]

// Disable cache in dev mode
const buildIntegrity = nuxt.options.dev ? undefined : Date.now()

Expand Down Expand Up @@ -564,6 +567,8 @@ export default defineNuxtModule<ModuleOptions>({
contentContext.markdown = processMarkdownOptions(contentContext.markdown)

nuxt.options.runtimeConfig.public.content = defu(nuxt.options.runtimeConfig.public.content, {
locales: options.locales,
defaultLocale: contentContext.defaultLocale,
integrity: buildIntegrity,
clientDB: {
isSPA: options.experimental.clientDB && nuxt.options.ssr === false
Expand Down Expand Up @@ -679,6 +684,10 @@ interface ModulePublicRuntimeConfig {
integrity: number
}

defaultLocale: ModuleOptions['defaultLocale']

locales: ModuleOptions['locales']

tags: Record<string, string>

base: string;
Expand Down
11 changes: 11 additions & 0 deletions src/runtime/composables/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ export const fetchContentNavigation = async (queryBuilder?: QueryBuilder | Query
// 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

// Filter by locale if:
// - locales are defined
// - query doesn't already have a locale filter
if (content.locales.length) {
const queryLocale = params.where?.find(w => w._locale)?._locale
if (!queryLocale) {
params.where = params.where || []
params.where.push({ _locale: content.defaultLocale })
}
}

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

Expand Down
10 changes: 10 additions & 0 deletions src/runtime/composables/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ export const createQueryFetch = <T = ParsedContent>(path?: string) => async (que
query.sort({ _file: 1, $numeric: true })
}

// Filter by locale if:
// - locales are defined
// - query doesn't already have a locale filter
if (content.locales.length) {
const queryLocale = query.params().where?.find(w => w._locale)?._locale
if (!queryLocale) {
query.locale(content.defaultLocale)
}
}

const params = query.params()

const apiPath = withContentBase(process.dev ? '/query' : `/query/${hash(params)}.${content.integrity}.json`)
Expand Down
24 changes: 14 additions & 10 deletions src/runtime/server/content-index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import type { H3Event } from 'h3'
import type { ParsedContent, QueryBuilder } from '../types'
import { isPreview } from './preview'
import { cacheStorage, getContent, getContentsList, serverQueryContent } from './storage'
import { cacheStorage, getContent, getContentsList } from './storage'
import { useRuntimeConfig } from '#imports'

export async function getContentIndex (event: H3Event) {
let contentIndex = await cacheStorage.getItem('content-index.json') as Record<string, string>
const defaultLocale = useRuntimeConfig().content.defaultLocale
let contentIndex = await cacheStorage.getItem('content-index.json') as Record<string, string[]>
if (!contentIndex) {
// Fetch all content
const data = await serverQueryContent(event).find()
// Fetch all contents
const data = await getContentsList(event)

contentIndex = data.reduce((acc, item) => {
if (!acc[item._path!]) {
acc[item._path!] = item._id
} else if (item._id.startsWith('content:')) {
acc[item._path!] = item._id
acc[item._path!] = acc[item._path!] || []
if (item._locale === defaultLocale) {
acc[item._path!].unshift(item._id)
} else {
acc[item._path!].push(item._id)
}
return acc
}, {} as Record<string, string>)
}, {} as Record<string, string[]>)

await cacheStorage.setItem('content-index.json', contentIndex)
}
Expand All @@ -33,9 +36,10 @@ export async function getIndexedContentsList<T = ParsedContent> (event: H3Event,
const index = await getContentIndex(event)
const keys = Object.keys(index)
.filter(key => (path as any).test ? (path as any).test(key) : key === String(path))
.map(key => index[key])
.flatMap(key => index[key])

const contents = await Promise.all(keys.map(key => getContent(event, key)))

return contents as unknown as Promise<T[]>
}

Expand Down
10 changes: 10 additions & 0 deletions src/runtime/server/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ export const createServerQueryFetch = <T = ParsedContent>(event: H3Event, path?:
query.sort({ _file: 1, $numeric: true })
}

// Filter by locale if:
// - locales are defined
// - query doesn't already have a locale filter
if (contentConfig.locales.length) {
const queryLocale = query.params().where?.find(w => w._locale)?._locale
if (!queryLocale) {
query.locale(contentConfig.defaultLocale)
}
}

return createPipelineFetcher<T>(() => getIndexedContentsList<T>(event, query))(query)
}

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/transformers/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default defineTransformer({
})

async function importPlugins (plugins: Record<string, false | MarkdownPlugin> = {}) {
const resolvedPlugins = {}
const resolvedPlugins: Record<string, false | MarkdownPlugin & { instance: any }> = {}
for (const [name, plugin] of Object.entries(plugins)) {
if (plugin) {
resolvedPlugins[name] = {
Expand Down
38 changes: 38 additions & 0 deletions test/__snapshots__/basic.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
// Vitest Snapshot v1

exports[`Basic usage > Content Queries > Get contents index > basic-index-body 1`] = `
{
"children": [
{
"children": [
{
"type": "text",
"value": "Index",
},
],
"props": {
"id": "index",
},
"tag": "h1",
"type": "element",
},
{
"children": [
{
"type": "text",
"value": "Hello World",
},
],
"props": {},
"tag": "p",
"type": "element",
},
],
"toc": {
"depth": 2,
"links": [],
"searchDepth": 2,
"title": "",
},
"type": "root",
}
`;

exports[`Basic usage > Get contents index > basic-index-body 1`] = `
{
"children": [
Expand Down
38 changes: 38 additions & 0 deletions test/__snapshots__/custom-api-base.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
// Vitest Snapshot v1

exports[`Custom api baseURL > Content Queries > Get contents index > basic-index-body 1`] = `
{
"children": [
{
"children": [
{
"type": "text",
"value": "Index",
},
],
"props": {
"id": "index",
},
"tag": "h1",
"type": "element",
},
{
"children": [
{
"type": "text",
"value": "Hello World",
},
],
"props": {},
"tag": "p",
"type": "element",
},
],
"toc": {
"depth": 2,
"links": [],
"searchDepth": 2,
"title": "",
},
"type": "root",
}
`;

exports[`Custom api baseURL > Get contents index > basic-index-body 1`] = `
{
"children": [
Expand Down
89 changes: 4 additions & 85 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { fileURLToPath } from 'url'
import { assert, test, describe, expect, vi } from 'vitest'
import { test, describe, expect, vi } from 'vitest'
import { setup, $fetch } from '@nuxt/test-utils'
import { hash } from 'ohash'
import { testMarkdownParser } from './features/parser-markdown'
import { testPathMetaTransformer } from './features/transformer-path-meta'
import { testYamlParser } from './features/parser-yaml'
Expand All @@ -18,6 +17,7 @@ import { testHighlighter } from './features/highlighter'
import { testMarkdownRenderer } from './features/renderer-markdown'
import { testParserOptions } from './features/parser-options'
import { testComponents } from './features/components'
import { testLocales } from './features/locales'

const spyConsoleWarn = vi.spyOn(global.console, 'warn')

Expand All @@ -27,94 +27,11 @@ describe('Basic usage', async () => {
server: true
})

const QUERY_ENDPOINT = '/api/_content/query'
const fetchDocument = (_id: string) => {
const params = { first: true, where: { _id } }
const qid = hash(params)
return $fetch(`${QUERY_ENDPOINT}/${qid}`, {
params: { _params: JSON.stringify(params) }
})
}

test('List contents', async () => {
const params = { only: '_id' }
const qid = hash(params)
const docs = await $fetch(`${QUERY_ENDPOINT}/${qid}`, {
params: { _params: JSON.stringify(params) }
})

const ids = docs.map(doc => doc._id)

assert(ids.length > 0)
assert(ids.includes('content:index.md'))

// Ignored files should be listed
assert(ids.includes('content:.dot-ignored.md') === false, 'Ignored files with `.` should not be listed')
assert(ids.includes('content:-dash-ignored.md') === false, 'Ignored files with `-` should not be listed')

assert(ids.includes('fa-ir:fa:hello.md') === true, 'Files with `fa-ir` prefix should be listed')
})

test('Get contents index', async () => {
const index = await fetchDocument('content:index.md')

expect(index).toHaveProperty('body')

expect(index.body).toMatchSnapshot('basic-index-body')
})

test('Get ignored contents', async () => {
const ignored = await fetchDocument('content:.dot-ignored.md').catch(_err => null)

expect(ignored).toBeNull()
})

test('Search contents using `locale` helper', async () => {
const fa = await $fetch('/locale-fa')

expect(fa).toContain('fa-ir:fa:hello.md')
expect(fa).not.toContain('content:index.md')

const en = await $fetch('/locale-en')

expect(en).not.toContain('fa-ir:fa:hello.md')
expect(en).toContain('content:index.md')
})

test('Use default locale for unscoped contents', async () => {
const index = await fetchDocument('content:index.md')

expect(index).toMatchObject({
_locale: 'en'
})
})

test('Multi part path', async () => {
const html = await $fetch('/features/multi-part-path')
expect(html).contains('Persian')
})

test('Empty slot', async () => {
const html = await $fetch('/features/empty-slot')
expect(html).contains('Nullish Document!!!')
expect(html).contains('Empty Child!!!')
})

test('<ContentDoc> head management (if same path)', async () => {
const html = await $fetch('/head')
expect(html).contains('<title>Head overwritten</title>')
expect(html).contains('<meta property="og:image" content="https://picsum.photos/200/300">')
expect(html).contains('<meta name="description" content="Description overwritten">')
expect(html).contains('<meta property="og:image" content="https://picsum.photos/200/300">')
})

test('<ContentDoc> head management (not same path)', async () => {
const html = await $fetch('/bypass-head')
expect(html).not.contains('<title>Head overwritten</title>')
expect(html).not.contains('<meta property="og:image" content="https://picsum.photos/200/300">')
expect(html).not.contains('<meta name="description" content="Description overwritten"><meta property="og:image" content="https://picsum.photos/200/300">')
})

test('Partials specials chars', async () => {
const html = await $fetch('/_partial/content-(v2)')
expect(html).contains('Content (v2)')
Expand All @@ -131,6 +48,8 @@ describe('Basic usage', async () => {
expect(spyConsoleWarn).toHaveBeenCalledWith('Ignoring [content:with-\'invalid\'-char.md]. File name should not contain any of the following characters: \', ", ?, #, /')
})

testLocales()

testComponents()

testContentQuery()
Expand Down

0 comments on commit 90b358a

Please sign in to comment.