Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/app/components/PostPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ const { type } = defineProps({
const route = useRoute()
const siteConfig = useSiteConfig()

const { data } = await useAsyncData(route.path, () => Promise.all([
queryCollection('posts').path(route.path).first(),
queryCollectionItemSurroundings('posts', route.path, { fields: ['title', 'description'] })
const { data } = await useAsyncData(route.path, (_nuxtApp, { signal }) => Promise.all([
queryCollection('posts').path(route.path).first({ signal }),
queryCollectionItemSurroundings('posts', route.path, { fields: ['title', 'description'] }, { signal })
.where('path', 'LIKE', `/${type}%`)
.where('draft', '=', 0)
.order('date', 'DESC'),
Expand Down
2 changes: 1 addition & 1 deletion docs/app/components/example/ExampleFulltextFusejs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import Fuse from 'fuse.js'

const query = ref('')
const { data } = await useAsyncData('search-data', () => queryCollectionSearchSections('docs'))
const { data } = await useAsyncData('search-data', (_nuxtApp, { signal }) => queryCollectionSearchSections('docs', { signal }))

const fuse = new Fuse(data.value || [], {
keys: [
Expand Down
2 changes: 1 addition & 1 deletion docs/app/components/example/ExampleFulltextMiniSearch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import MiniSearch from 'minisearch'

const query = ref('')
const { data } = await useAsyncData('search-data', () => queryCollectionSearchSections('docs'))
const { data } = await useAsyncData('search-data', (_nuxtApp, { signal }) => queryCollectionSearchSections('docs', { signal }))

const miniSearch = new MiniSearch({
fields: ['title', 'content'],
Expand Down
6 changes: 3 additions & 3 deletions docs/app/pages/blog/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import { titleCase } from 'scule'

const siteConfig = useSiteConfig()

const { data: page } = await useAsyncData('blog-landing', () => queryCollection('landing').path('/blog').first())
const { data: page } = await useAsyncData('blog-landing', (_nuxtApp, { signal }) => queryCollection('landing').path('/blog').first({ signal }))
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}

const { data: posts } = await useAsyncData('blog-posts', () => queryCollection('posts')
const { data: posts } = await useAsyncData('blog-posts', (_nuxtApp, { signal }) => queryCollection('posts')
.where('path', 'LIKE', '/blog%')
.where('draft', '=', 0)
.order('date', 'DESC')
.all(),
.all({ signal }),
)

useSeoMeta({
Expand Down
6 changes: 3 additions & 3 deletions docs/app/pages/changelog/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ const { isScrolling, arrivedState } = useScroll(document)

const siteConfig = useSiteConfig()

const { data: page } = await useAsyncData('changelog-landing', () => queryCollection('landing').path('/changelog').first())
const { data: page } = await useAsyncData('changelog-landing', (_nuxtApp, { signal }) => queryCollection('landing').path('/changelog').first({ signal }))
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}

const { data: posts } = await useAsyncData('changelog-posts', () => queryCollection('posts')
const { data: posts } = await useAsyncData('changelog-posts', (_nuxtApp, { signal }) => queryCollection('posts')
.where('path', 'LIKE', '/changelog%')
.where('draft', '=', 0)
.order('date', 'DESC')
.all(),
.all({ signal }),
)

useSeoMeta({
Expand Down
2 changes: 1 addition & 1 deletion docs/app/pages/studio/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
const siteConfig = useSiteConfig()

const { data: page } = await useAsyncData('studio-landing', () => queryCollection('landing').path('/studio').first())
const { data: page } = await useAsyncData('studio-landing', (_nuxtApp, { signal }) => queryCollection('landing').path('/studio').first({ signal }))
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
Expand Down
2 changes: 1 addition & 1 deletion docs/app/pages/studio/pricing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { TableColumn } from '@nuxt/ui'
const siteConfig = useSiteConfig()
const UIcon = resolveComponent('UIcon')

const { data: page } = await useAsyncData('pricing-landing', () => queryCollection('pricing').path('/studio/pricing').first())
const { data: page } = await useAsyncData('pricing-landing', (_nuxtApp, { signal }) => queryCollection('pricing').path('/studio/pricing').first({ signal }))
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
Expand Down
2 changes: 1 addition & 1 deletion docs/app/pages/templates/[slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const route = useRoute()
const siteConfig = useSiteConfig()

const { data: template } = await useAsyncData(`template-${route.params.slug}`, () => queryCollection('templates').path(route.path).first())
const { data: template } = await useAsyncData(`template-${route.params.slug}`, (_nuxtApp, { signal }) => queryCollection('templates').path(route.path).first({ signal }))
if (!template.value) {
showError({ statusCode: 404, statusMessage: 'Template Not Found' })
}
Expand Down
4 changes: 2 additions & 2 deletions docs/app/pages/templates/index.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
const siteConfig = useSiteConfig()

const { data: page } = await useAsyncData('templates-landing', () => queryCollection('landing').path('/templates').first())
const { data: page } = await useAsyncData('templates-landing', (_nuxtApp, { signal }) => queryCollection('landing').path('/templates').first({ signal }))
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}

const { data: templates } = await useAsyncData('templates', () => queryCollection('templates').where('draft', '=', 0).all())
const { data: templates } = await useAsyncData('templates', (_nuxtApp, { signal }) => queryCollection('templates').where('draft', '=', 0).all({ signal }))

useSeoMeta({
title: page.value.seo?.title,
Expand Down
4 changes: 2 additions & 2 deletions examples/basic/app/pages/[...slug].vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script setup lang="ts">
const route = useRoute()

const { data: page } = await useAsyncData('page-' + route.path, () => {
return queryCollection('content').path(route.path).first()
const { data: page } = await useAsyncData('page-' + route.path, (_nuxtApp, { signal }) => {
return queryCollection('content').path(route.path).first({ signal })
})
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
Expand Down
4 changes: 2 additions & 2 deletions examples/blog/app/pages/[...slug].vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script lang="ts" setup>
const route = useRoute()
const pageId = computed(() => 'blog-' + route.path)
const { data: post } = await useAsyncData(pageId, () => {
const { data: post } = await useAsyncData(pageId, (_nuxtApp, { signal }) => {
return queryCollection('blog')
.path(route.path)
.first()
.first({ signal })
})
</script>

Expand Down
4 changes: 2 additions & 2 deletions examples/i18n/app/pages/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ const route = useRoute()
const { locale, localeProperties } = useI18n()
const slug = computed(() => withLeadingSlash(String(route.params.slug)))

const { data: page } = await useAsyncData('page-' + slug.value, async () => {
const { data: page } = await useAsyncData('page-' + slug.value, async (_nuxtApp, { signal }) => {
const collection = ('content_' + locale.value) as keyof Collections
const content = await queryCollection(collection).path(slug.value).first()
const content = await queryCollection(collection).path(slug.value).first({ signal })

// Possibly fallback to default locale if content is missing in non-default locale

Expand Down
4 changes: 2 additions & 2 deletions examples/ui/app/pages/[...slug].vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script setup lang="ts">
const route = useRoute()

const { data: page } = await useAsyncData('page-' + route.path, () => {
return queryCollection('content').path(route.path).first()
const { data: page } = await useAsyncData('page-' + route.path, (_nuxtApp, { signal }) => {
return queryCollection('content').path(route.path).first({ signal })
})
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
Expand Down
9 changes: 5 additions & 4 deletions playground/pages/[...slug].vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<script setup lang="ts">
const route = useRoute()

const { data: navigation } = await useAsyncData('contents-list', () => queryCollectionNavigation('content'))
const { data } = await useAsyncData(() => 'posts' + route.path, async () => {
return await queryCollection('content').path(route.path).first()
const { data: navigation } = await useAsyncData('contents-list', (_nuxtApp, { signal }) => queryCollectionNavigation('content', [], { signal }))
const { data } = await useAsyncData(() => 'posts' + route.path, async (_nuxtApp, { signal }) => {
return await queryCollection('content').path(route.path).first({ signal })
})

const { data: surround } = await useAsyncData(() => 'content-surround' + route.path, () => {
const { data: surround } = await useAsyncData(() => 'content-surround' + route.path, (_nuxtApp, { signal }) => {
return queryCollectionItemSurroundings('content', route.path, {
before: 1,
after: 1,
fields: ['title', 'description'],
signal,
})
})
</script>
Expand Down
9 changes: 5 additions & 4 deletions playground/pages/content-v2/[...slug].vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<script setup lang="ts">
const route = useRoute()

const { data: navigation } = await useAsyncData('contents-v2-list', () => queryCollectionNavigation('contentV2'))
const { data } = await useAsyncData('posts' + route.path, async () => {
return await queryCollection('contentV2').path(route.path).first()
const { data: navigation } = await useAsyncData('contents-v2-list', (_nuxtApp, { signal }) => queryCollectionNavigation('contentV2', { signal }))
const { data } = await useAsyncData('posts' + route.path, async (_nuxtApp, { signal }) => {
return await queryCollection('contentV2').path(route.path).first({ signal })
})

const { data: surround } = await useAsyncData('content-v2-docs-surround' + route.path, () => {
const { data: surround } = await useAsyncData('content-v2-docs-surround' + route.path, (_nuxtApp, { signal }) => {
return queryCollectionItemSurroundings('contentV2', route.path, {
before: 1,
after: 1,
fields: ['title', 'description'],
signal,
})
})

Expand Down
2 changes: 1 addition & 1 deletion playground/pages/data/[...slug].vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts" setup>
const route = useRoute()
const { data } = await useAsyncData('data', () => queryCollection('data').path(route.path).first())
const { data } = await useAsyncData('data', (_nuxtApp, { signal }) => queryCollection('data').path(route.path).first({ signal }))
</script>

<template>
Expand Down
6 changes: 3 additions & 3 deletions playground/pages/hackernews/[...slug].vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script setup lang="ts">
const route = useRoute()

const { data } = await useAsyncData('news' + route.path, async () => {
const { data } = await useAsyncData('news' + route.path, async (_nuxtApp, { signal }) => {
if (route.path === '/hackernews') {
return await queryCollection('hackernews').all()
return await queryCollection('hackernews').all({ signal })
}

return await queryCollection('hackernews').where('id', '=', route.params.slug).first()
return await queryCollection('hackernews').where('id', '=', route.params.slug).first({ signal })
})
</script>

Expand Down
9 changes: 5 additions & 4 deletions playground/pages/nuxt/[...slug].vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<script setup lang="ts">
const route = useRoute()

const { data: navigation } = await useAsyncData('nuxt-contents-list', () => queryCollectionNavigation('nuxt'))
const { data } = await useAsyncData('posts' + route.path, async () => {
return await queryCollection('nuxt').path(route.path).first()
const { data: navigation } = await useAsyncData('nuxt-contents-list', (_nuxtApp, { signal }) => queryCollectionNavigation('nuxt', [], { signal }))
const { data } = await useAsyncData('posts' + route.path, async (_nuxtApp, { signal }) => {
return await queryCollection('nuxt').path(route.path).first({ signal })
})

const { data: surround } = await useAsyncData('nuxt-docs-surround' + route.path, () => {
const { data: surround } = await useAsyncData('nuxt-docs-surround' + route.path, (_nuxtApp, { signal }) => {
return queryCollectionItemSurroundings('nuxt', route.path, {
before: 1,
after: 1,
fields: ['title', 'description'],
signal,
})
})
</script>
Expand Down
9 changes: 5 additions & 4 deletions playground/pages/vue/[...slug].vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<script setup lang="ts">
const route = useRoute()

const { data: navigation } = await useAsyncData('vue-contents-list', () => queryCollectionNavigation('vue'))
const { data } = await useAsyncData('posts' + route.path, async () => {
return await queryCollection('vue').path(route.path).first()
const { data: navigation } = await useAsyncData('vue-contents-list', (_nuxtApp, { signal }) => queryCollectionNavigation('vue', [], { signal }))
const { data } = await useAsyncData('posts' + route.path, async (_nuxtApp, { signal }) => {
return await queryCollection('vue').path(route.path).first({ signal })
})

const { data: surround } = await useAsyncData('vue-docs-surround' + route.path, () => {
const { data: surround } = await useAsyncData('vue-docs-surround' + route.path, (_nuxtApp, { signal }) => {
return queryCollectionItemSurroundings('vue', route.path, {
before: 1,
after: 1,
fields: ['title', 'description'],
signal,
})
})
</script>
Expand Down
47 changes: 33 additions & 14 deletions src/runtime/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,55 @@ interface ChainablePromise<T extends keyof PageCollections, R> extends Promise<R

export const queryCollection = <T extends keyof Collections>(collection: T): CollectionQueryBuilder<Collections[T]> => {
const event = tryUseNuxtApp()?.ssrContext?.event
return collectionQueryBuilder<T>(collection, (collection, sql) => executeContentQuery(event, collection, sql))
return collectionQueryBuilder<T>(collection, (collection, sql, opts) => executeContentQuery(event, collection, sql, { signal: opts?.signal }))
}

export function queryCollectionNavigation<T extends keyof PageCollections>(collection: T, fields?: Array<keyof PageCollections[T]>): ChainablePromise<T, ContentNavigationItem[]> {
return chainablePromise(collection, qb => generateNavigationTree(qb, fields))
export interface QueryCollectionNavigationOptions {
signal?: AbortSignal
}

export function queryCollectionItemSurroundings<T extends keyof PageCollections>(collection: T, path: string, opts?: SurroundOptions<keyof PageCollections[T]>): ChainablePromise<T, ContentNavigationItem[]> {
export function queryCollectionNavigation<T extends keyof PageCollections>(collection: T, fields?: Array<keyof PageCollections[T]>, { signal }: QueryCollectionNavigationOptions = {}): ChainablePromise<T, ContentNavigationItem[]> {
return chainablePromise(collection, qb => generateNavigationTree(qb, fields, { signal }))
}

export interface QueryCollectionItemSurroundingsOptions<F> extends SurroundOptions<F> {
signal?: AbortSignal
}

export function queryCollectionItemSurroundings<T extends keyof PageCollections>(collection: T, path: string, opts?: QueryCollectionItemSurroundingsOptions<keyof PageCollections[T]>): ChainablePromise<T, ContentNavigationItem[]> {
return chainablePromise(collection, qb => generateItemSurround(qb, path, opts))
}

export function queryCollectionSearchSections(collection: keyof Collections, opts?: { ignoredTags: string[] }) {
export interface QueryCollectionSearchSectionsOptions {
ignoredTags?: string[]
signal?: AbortSignal
}

export function queryCollectionSearchSections(collection: keyof Collections, opts?: QueryCollectionSearchSectionsOptions) {
return chainablePromise(collection, qb => generateSearchSections(qb, opts))
}

async function executeContentQuery<T extends keyof Collections, Result = Collections[T]>(event: H3Event | undefined, collection: T, sql: string) {
async function executeContentQuery<T extends keyof Collections, Result = Collections[T]>(event: H3Event | undefined, collection: T, sql: string, { signal }: { signal?: AbortSignal } = {}): Promise<Result[]> {
if (import.meta.client && window.WebAssembly) {
return queryContentSqlClientWasm<T, Result>(collection, sql) as Promise<Result[]>
return queryContentSqlClientWasm<T, Result>(collection, sql, { signal }) as Promise<Result[]>
}
else {
return fetchQuery(event, String(collection), sql) as Promise<Result[]>
return fetchQuery(event, String(collection), sql, { signal })
}
}

async function queryContentSqlClientWasm<T extends keyof Collections, Result = Collections[T]>(collection: T, sql: string) {
const rows = await import('./internal/database.client')
.then(m => m.loadDatabaseAdapter(collection))
.then(db => db.all<Result>(sql))

return rows as Result[]
async function queryContentSqlClientWasm<T extends keyof Collections, Result = Collections[T]>(collection: T, sql: string, { signal }: { signal?: AbortSignal } = {}): Promise<Result[]> {
return new Promise<Result[]>((resolve, reject) => {
// todo: explore aborting wasm queries with signal
signal?.addEventListener('abort', () => {
reject(new DOMException('Aborted', 'AbortError'))
})
import('./internal/database.client')
.then(m => m.loadDatabaseAdapter(collection))
.then(db => db.all<Result>(sql))
.then(resolve)
.catch(reject)
})
}

function chainablePromise<T extends keyof PageCollections, Result>(collection: T, fn: (qb: CollectionQueryBuilder<PageCollections[T]>) => Promise<Result>) {
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/internal/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export async function fetchDatabase(event: H3Event | undefined, collection: stri
})
}

export async function fetchQuery<Item>(event: H3Event | undefined, collection: string, sql: string): Promise<Item[]> {
export interface FetchQueryOptions {
signal?: AbortSignal
}

export async function fetchQuery<Item>(event: H3Event | undefined, collection: string, sql: string, { signal }: FetchQueryOptions = {}): Promise<Item[]> {
return await $fetch(`/__nuxt_content/${collection}/query`, {
context: event ? { cloudflare: event.context.cloudflare } : {},
headers: {
Expand All @@ -25,5 +29,6 @@ export async function fetchQuery<Item>(event: H3Event | undefined, collection: s
body: {
sql,
},
signal,
})
}
8 changes: 4 additions & 4 deletions src/runtime/internal/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@ import type { ContentNavigationItem, PageCollectionItemBase, CollectionQueryBuil
/**
* Create NavItem array to be consumed from runtime plugin.
*/
export async function generateNavigationTree<T extends PageCollectionItemBase>(queryBuilder: CollectionQueryBuilder<T>, extraFields: Array<keyof T> = []) {
export async function generateNavigationTree<T extends PageCollectionItemBase>(queryBuilder: CollectionQueryBuilder<T>, extraFields: Array<keyof T> = [], { signal }: { signal?: AbortSignal } = {}): Promise<ContentNavigationItem[]> {
// @ts-expect-error -- internal
const params = queryBuilder.__params
if (!params?.orderBy?.length) {
queryBuilder = queryBuilder.order('stem', 'ASC')
}

const collecitonItems = await queryBuilder
const collectionItems = await queryBuilder
.orWhere(group => group
.where('navigation', '<>', 'false')
.where('navigation', 'IS NULL'),
)
.select('navigation', 'stem', 'path', 'title', 'meta', ...(extraFields || []))
.all() as unknown as PageCollectionItemBase[]
.all({ signal }) as unknown as PageCollectionItemBase[]

const { contents, configs } = collecitonItems.reduce((acc, c) => {
const { contents, configs } = collectionItems.reduce((acc, c) => {
if (String(c.stem).split('/').pop() === '.navigation') {
c.title = c.title?.toLowerCase() === 'navigation' ? '' : c.title
const key = c.path!.split('/').slice(0, -1).join('/') || '/'
Expand Down
Loading
Loading