diff --git a/src/runtime/composables/query.ts b/src/runtime/composables/query.ts index dd05ac9b4..b2adb688b 100644 --- a/src/runtime/composables/query.ts +++ b/src/runtime/composables/query.ts @@ -9,7 +9,21 @@ import { withContentBase } from './utils' /** * Query fetcher */ -export const queryFetch = (params: QueryBuilderParams) => { +export const createQueryFetch = (path?: string) => (query: QueryBuilder) => { + if (path) { + if (query.params().first) { + query.where({ _path: withoutTrailingSlash(path) }) + } else { + query.where({ _path: new RegExp(`^${path.replace(/[-[\]{}()*+.,^$\s/]/g, '\\$&')}`) }) + } + } + // Provide default sort order + if (!query.params().sort?.length) { + query.sort({ _file: 1, $numeric: true }) + } + + const params = query.params() + const apiPath = withContentBase(process.dev ? '/query' : `/query/${hash(params)}`) // Prefetch the query @@ -39,12 +53,8 @@ export function queryContent(query: string, ...pathParts: str export function queryContent (query: QueryBuilderParams): QueryBuilder; export function queryContent (query?: string | QueryBuilderParams, ...pathParts: string[]) { if (typeof query === 'string') { - let path = withLeadingSlash(withoutTrailingSlash(joinURL(query, ...pathParts))) - // escape regex special chars - path = path.replace(/[-[\]{}()*+.,^$\s]/g, '\\$&') - - return createQuery(queryFetch).where({ _path: new RegExp(`^${path}`) }) + return createQuery(createQueryFetch(withLeadingSlash(joinURL(query, ...pathParts)))) } - return createQuery(queryFetch, query) + return createQuery(createQueryFetch(), query) } diff --git a/src/runtime/query/match/pipeline.ts b/src/runtime/query/match/pipeline.ts index acab570ba..fc66fb96f 100644 --- a/src/runtime/query/match/pipeline.ts +++ b/src/runtime/query/match/pipeline.ts @@ -1,4 +1,4 @@ -import type { QueryBuilderParams, QueryPipe } from '../../types' +import type { QueryBuilder, QueryBuilderParams, QueryPipe } from '../../types' import { apply, ensureArray, sortList, withoutKeys, withKeys } from './utils' import { createMatch } from '.' @@ -40,9 +40,9 @@ export function createPipelineFetcher (getContentsList: () => Promise) { (data, params) => params.first ? data[0] : data ] - return async (params: QueryBuilderParams): Promise => { + return async (query: QueryBuilder): Promise => { const data = await getContentsList() - return pipelines.reduce(($data: Array, pipe: any) => pipe($data, params) || $data, data) + return pipelines.reduce(($data: Array, pipe: any) => pipe($data, query.params()) || $data, data) } } diff --git a/src/runtime/query/query.ts b/src/runtime/query/query.ts index 0fcb95f5a..167b75685 100644 --- a/src/runtime/query/query.ts +++ b/src/runtime/query/query.ts @@ -6,15 +6,15 @@ const arrayParams = ['sort', 'where', 'only', 'without'] export const createQuery = ( fetcher: DatabaseFetcher, - queryParams?: QueryBuilderParams + intitialParams?: QueryBuilderParams ): QueryBuilder => { - const params = { - ...queryParams + const queryParams = { + ...intitialParams } as QueryBuilderParams for (const key of arrayParams) { - if (params[key]) { - params[key] = ensureArray(params[key]) + if (queryParams[key]) { + queryParams[key] = ensureArray(queryParams[key]) } } @@ -23,23 +23,29 @@ export const createQuery = ( */ const $set = (key: string, fn: (...values: any[]) => any = v => v) => { return (...values: []) => { - params[key] = fn(...values) + queryParams[key] = fn(...values) return query } } const query: QueryBuilder = { - params: () => Object.freeze(params), + params: () => queryParams, only: $set('only', ensureArray) as () => ReturnType['only']>, without: $set('without', ensureArray), - where: $set('where', (q: any) => [...ensureArray(params.where), q]), - sort: $set('sort', (sort: SortOptions) => [...ensureArray(params.sort), ...ensureArray(sort)]), + where: $set('where', (q: any) => [...ensureArray(queryParams.where), q]), + sort: $set('sort', (sort: SortOptions) => [...ensureArray(queryParams.sort), ...ensureArray(sort)]), limit: $set('limit', v => parseInt(String(v), 10)), skip: $set('skip', v => parseInt(String(v), 10)), // find - findOne: () => fetcher({ ...params, first: true }) as Promise, - find: () => fetcher(params) as Promise>, - findSurround: (query, options) => fetcher({ ...params, surround: { query, ...options } }) as Promise>, + find: () => fetcher(query) as Promise>, + findOne: () => { + queryParams.first = true + return fetcher(query) as Promise + }, + findSurround: (surroundQuery, options) => { + queryParams.surround = { query: surroundQuery, ...options } + return fetcher(query) as Promise> + }, // locale locale: (_locale: string) => query.where({ _locale }) } diff --git a/src/runtime/server/storage.ts b/src/runtime/server/storage.ts index e0880436c..5c65587bb 100644 --- a/src/runtime/server/storage.ts +++ b/src/runtime/server/storage.ts @@ -1,5 +1,5 @@ import { prefixStorage } from 'unstorage' -import { joinURL, withLeadingSlash } from 'ufo' +import { joinURL, withLeadingSlash, withoutTrailingSlash } from 'ufo' import { hash as ohash } from 'ohash' import type { CompatibilityEvent } from 'h3' import type { QueryBuilderParams, ParsedContent, QueryBuilder } from '../types' @@ -118,6 +118,25 @@ export const getContent = async (event: CompatibilityEvent, id: string): Promise return parsed } +export const createServerQueryFetch = (event: CompatibilityEvent, path?: string) => (query: QueryBuilder) => { + if (path) { + if (query.params().first) { + query.where({ _path: withoutTrailingSlash(path) }) + } else { + query.where({ _path: new RegExp(`^${path.replace(/[-[\]{}()*+.,^$\s/]/g, '\\$&')}`) }) + } + } + + // Provide default sort order + if (!query.params().sort?.length) { + query.sort({ _file: 1, $numeric: true }) + } + + return createPipelineFetcher( + () => getContentsList(event) as unknown as Promise + )(query) +} + /** * Query contents */ @@ -125,24 +144,10 @@ export function serverQueryContent(event: CompatibilityEvent) export function serverQueryContent(event: CompatibilityEvent, params?: QueryBuilderParams): QueryBuilder; export function serverQueryContent(event: CompatibilityEvent, path?: string, ...pathParts: string[]): QueryBuilder; export function serverQueryContent (event: CompatibilityEvent, path?: string | QueryBuilderParams, ...pathParts: string[]) { - let params = (path || {}) as QueryBuilderParams if (typeof path === 'string') { path = withLeadingSlash(joinURL(path, ...pathParts)) - // escape regex special chars - path = path.replace(/[-[\]{}()*+.,^$\s]/g, '\\$&') - - params = { - where: [{ _path: new RegExp(`^${path}`) }] - } - } - const pipelineFetcher = createPipelineFetcher( - () => getContentsList(event) as unknown as Promise - ) - - // Provide default sort order - if (!params.sort?.length) { - params.sort = [{ _file: 1, $numeric: true }] + return createQuery(createServerQueryFetch(event, path)) } - return createQuery(pipelineFetcher, params) + return createQuery(createServerQueryFetch(event), path || {}) } diff --git a/src/runtime/types.d.ts b/src/runtime/types.d.ts index f039f90ea..3ffe57f7d 100644 --- a/src/runtime/types.d.ts +++ b/src/runtime/types.d.ts @@ -262,7 +262,7 @@ export interface QueryBuilder { export type QueryPipe = (data: Array, param: QueryBuilderParams) => Array | void -export type DatabaseFetcher = (params: QueryBuilderParams) => Promise | T> +export type DatabaseFetcher = (quey: QueryBuilder) => Promise | T> export type QueryMatchOperator = (item: any, condition: any) => boolean diff --git a/test/basic.test.ts b/test/basic.test.ts index 2064225f6..547f755b8 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -12,6 +12,7 @@ import { testCSVParser } from './features/parser-csv' import { testRegex } from './features/regex' import { testMarkdownParserExcerpt } from './features/parser-markdown-excerpt' import { testParserHooks } from './features/parser-hooks' +import { testContentQuery } from './features/content-query' describe('fixtures:basic', async () => { await setup({ @@ -44,7 +45,7 @@ describe('fixtures:basic', async () => { 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:index.md') === true, 'Files with `fa-ir` prefix should be listed') + assert(ids.includes('fa-ir:fa:hello.md') === true, 'Files with `fa-ir` prefix should be listed') }) test('Get contents index', async () => { @@ -64,12 +65,12 @@ describe('fixtures:basic', async () => { test('Search contents using `locale` helper', async () => { const fa = await $fetch('/locale-fa') - expect(fa).toContain('fa-ir:fa:index.md') + 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:index.md') + expect(en).not.toContain('fa-ir:fa:hello.md') expect(en).toContain('content:index.md') }) @@ -104,6 +105,8 @@ describe('fixtures:basic', async () => { expect(html).contains('Content (v2)') }) + testContentQuery() + testNavigation() testMarkdownParser() diff --git a/test/features/content-query.ts b/test/features/content-query.ts new file mode 100644 index 000000000..31f317290 --- /dev/null +++ b/test/features/content-query.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from 'vitest' +import { $fetch } from '@nuxt/test-utils' + +export const testContentQuery = () => { + describe('content-query', () => { + test('Find index', async () => { + const content = await $fetch('/') + + // Normal Prop + expect(content).includes('Index') + }) + + test('exact match foo not found', async () => { + const content = await $fetch('/features/query-content?path=/foo&findOne=1') + + // empty + expect(content).includes('$$$$') + }) + + test('exact match foo/bar found', async () => { + const content = await $fetch('/features/query-content?path=/foo/bar&findOne=1') + + // empty + expect(content).includes('prefix:foo:bar.md$$') + }) + + test('prefix queries', async () => { + const content = await $fetch('/features/query-content?path=/foo') + + expect(content).includes('prefix:foo:bar.md') + expect(content).includes('prefix:foo:baz.md') + expect(content).includes('prefix:foobarbaz.md') + }) + + test('directory listing', async () => { + const content = await $fetch('/features/query-content?path=/foo/') + + expect(content).includes('prefix:foo:bar.md') + expect(content).includes('prefix:foo:baz.md') + expect(content).not.includes('prefix:foobarbaz.md') + }) + }) +} diff --git a/test/features/parser-markdown.ts b/test/features/parser-markdown.ts index bf9aaec1e..a6200e2d5 100644 --- a/test/features/parser-markdown.ts +++ b/test/features/parser-markdown.ts @@ -78,9 +78,12 @@ export const testMarkdownParser = () => { content: [ ':hello', // valid ':hello,', // valid - ':hello-world', // valid but with different name + ':hello :hello', // valid ':hello{}-world', // valid + ':hello:hello', // invalid + ':hello-world', // valid but with different name ':hello:', // invalid + '`:hello`', // code ':rocket:' // emoji ].join('\n') } @@ -90,7 +93,14 @@ export const testMarkdownParser = () => { visit(parsed.body, node => (node as any).tag === 'hello', () => { compComponentCount += 1 }) - expect(compComponentCount).toEqual(3) + expect(compComponentCount).toEqual(5) + + const paragraph = parsed.body.children[0] + expect(paragraph.children[0].tag).toEqual('hello') + expect(paragraph.children[1].tag).toEqual('hello') + expect(paragraph.children[3].tag).toEqual('hello') + expect(paragraph.children[5].tag).toEqual('hello') + expect(paragraph.children[6].tag).toEqual('hello') // Check conflict between inline compoenent and emoji expect(parsed.body.children[0].children.pop().value).toContain('🚀') diff --git a/test/fixtures/basic/content-fa/index.md b/test/fixtures/basic/content-fa/hello.md similarity index 100% rename from test/fixtures/basic/content-fa/index.md rename to test/fixtures/basic/content-fa/hello.md diff --git a/test/fixtures/basic/content/_partial/prefix/foo/bar.md b/test/fixtures/basic/content/_partial/prefix/foo/bar.md new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixtures/basic/content/_partial/prefix/foo/baz.md b/test/fixtures/basic/content/_partial/prefix/foo/baz.md new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixtures/basic/content/_partial/prefix/foobarbaz.md b/test/fixtures/basic/content/_partial/prefix/foobarbaz.md new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixtures/basic/pages/features/query-content.vue b/test/fixtures/basic/pages/features/query-content.vue new file mode 100644 index 000000000..3b937d13f --- /dev/null +++ b/test/fixtures/basic/pages/features/query-content.vue @@ -0,0 +1,20 @@ + + +