Skip to content

Commit 7f8f128

Browse files
authored
feat(query): support complex SQL where conditions (#2878)
1 parent c325151 commit 7f8f128

File tree

9 files changed

+117
-45
lines changed

9 files changed

+117
-45
lines changed

package.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
},
4949
"dependencies": {
5050
"@nuxt/kit": "^3.14.1592",
51-
"@nuxtjs/mdc": "^0.9.5",
51+
"@nuxtjs/mdc": "npm:@nuxtjs/mdc-edge@latest",
5252
"@sqlite.org/sqlite-wasm": "3.47.0-build1",
5353
"better-sqlite3": "^11.5.0",
5454
"c12": "^2.0.1",
@@ -121,8 +121,7 @@
121121
},
122122
"resolutions": {
123123
"@nuxt/content": "workspace:*",
124-
"vue": "^3.5.12",
125-
"@nuxtjs/mdc": "npm:@nuxtjs/mdc-edge@latest"
124+
"vue": "^3.5.12"
126125
},
127126
"packageManager": "pnpm@9.14.2",
128127
"unbuild": {

pnpm-lock.yaml

+9-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/runtime/app.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Collections, PageCollections, CollectionQueryBuilder, SurroundOptions, SQLOperator } from '@nuxt/content'
1+
import type { Collections, PageCollections, CollectionQueryBuilder, SurroundOptions, SQLOperator, QueryGroupFunction } from '@nuxt/content'
22
import { collectionQureyBuilder } from './internal/query'
33
import { measurePerformance } from './internal/performance'
44
import { generateNavigationTree } from './internal/navigation'
@@ -9,6 +9,8 @@ import { tryUseNuxtApp } from '#imports'
99

1010
interface ChainablePromise<T extends keyof PageCollections, R> extends Promise<R> {
1111
where(field: keyof PageCollections[T] | string, operator: SQLOperator, value?: unknown): ChainablePromise<T, R>
12+
andWhere(groupFactory: QueryGroupFunction<PageCollections[T]>): ChainablePromise<T, R>
13+
orWhere(groupFactory: QueryGroupFunction<PageCollections[T]>): ChainablePromise<T, R>
1214
order(field: keyof PageCollections[T], direction: 'ASC' | 'DESC'): ChainablePromise<T, R>
1315
}
1416

@@ -59,6 +61,14 @@ function chainablePromise<T extends keyof PageCollections, Result>(collection: T
5961
queryBuilder.where(String(field), operator, value)
6062
return chainable
6163
},
64+
andWhere(groupFactory) {
65+
queryBuilder.andWhere(groupFactory)
66+
return chainable
67+
},
68+
orWhere(groupFactory) {
69+
queryBuilder.orWhere(groupFactory)
70+
return chainable
71+
},
6272
order(field, direction) {
6373
queryBuilder.order(String(field), direction)
6474
return chainable

src/runtime/internal/navigation.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import type { CollectionQueryBuilder } from '~/src/types'
88
export async function generateNavigationTree<T extends PageCollectionItemBase>(queryBuilder: CollectionQueryBuilder<T>, extraFields: Array<keyof T> = []) {
99
const collecitonItems = await queryBuilder
1010
.order('stem', 'ASC')
11-
.where('navigation', '<>', 'false')
11+
.orWhere(group => group
12+
.where('navigation', '<>', 'false')
13+
.where('navigation', 'IS NULL'),
14+
)
1215
.select('navigation', 'stem', 'path', 'title', 'meta', ...(extraFields || []))
1316
.all() as unknown as PageCollectionItemBase[]
1417

@@ -65,7 +68,7 @@ export async function generateNavigationTree<T extends PageCollectionItemBase>(q
6568
const dirConfig = configs[navItem.path]
6669

6770
// Drop item if current directory config has `navigation: false`
68-
if (typeof dirConfig?.navigation !== 'undefined' && !dirConfig?.navigation) {
71+
if (typeof dirConfig?.navigation !== 'undefined' && dirConfig?.navigation === false) {
6972
return nav
7073
}
7174

@@ -99,7 +102,7 @@ export async function generateNavigationTree<T extends PageCollectionItemBase>(q
99102
const conf = configs[currentPathPart]
100103

101104
// Drop childrens if .navigation.yml has `navigation: false`
102-
if (typeof conf?.navigation !== 'undefined' && !conf.navigation) {
105+
if (typeof conf?.navigation !== 'undefined' && conf.navigation === false) {
103106
return []
104107
}
105108

@@ -171,7 +174,7 @@ function pick(keys?: string[]) {
171174
}
172175

173176
function isObject(obj: unknown) {
174-
return Object.prototype.toString.call(obj) === '[object Object]'
177+
return obj !== null && Object.prototype.toString.call(obj) === '[object Object]'
175178
}
176179

177180
export const generateTitle = (path: string) => path.split(/[\s-]/g).map(pascalCase).join(' ')

src/runtime/internal/query.ts

+64-24
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
1-
import type { Collections, CollectionQueryBuilder, SQLOperator } from '@nuxt/content'
1+
import type { Collections, CollectionQueryBuilder, CollectionQueryGroup, QueryGroupFunction, SQLOperator } from '@nuxt/content'
22
import { withoutTrailingSlash } from 'ufo'
33
import { tables } from '#content/manifest'
44

5-
export const collectionQureyBuilder = <T extends keyof Collections>(collection: T, fetch: (collection: T, sql: string) => Promise<Collections[T][]>): CollectionQueryBuilder<Collections[T]> => {
6-
const params = {
7-
conditions: [] as Array<string>,
8-
selectedFields: [] as Array<keyof Collections[T]>,
9-
offset: 0,
10-
limit: 0,
11-
orderBy: [] as Array<string>,
12-
// Count query
13-
count: {
14-
field: '' as keyof Collections[T] | '*',
15-
distinct: false,
16-
},
17-
}
5+
const buildGroup = <T extends keyof Collections>(group: CollectionQueryGroup<Collections[T]>, type: 'and' | 'or') => {
6+
const conditions = (group as unknown as { _conditions: Array<string> })._conditions
7+
return conditions.length > 0 ? `(${conditions.join(` ${type} `)})` : ''
8+
}
189

19-
const query: CollectionQueryBuilder<Collections[T]> = {
20-
path(path: string) {
21-
return query.where('path', '=', withoutTrailingSlash(path))
22-
},
23-
skip(skip: number) {
24-
params.offset = skip
25-
return query
26-
},
27-
where(field: keyof Collections[T] | string, operator: SQLOperator, value?: unknown): CollectionQueryBuilder<Collections[T]> {
10+
export const collectionQureyGroup = <T extends keyof Collections>(collection: T): CollectionQueryGroup<Collections[T]> => {
11+
const conditions = [] as Array<string>
12+
13+
const query: CollectionQueryGroup<Collections[T]> = {
14+
// @ts-expect-error -- internal
15+
_conditions: conditions,
16+
where(field: keyof Collections[T] | string, operator: SQLOperator, value?: unknown): CollectionQueryGroup<Collections[T]> {
2817
let condition: string
2918

3019
switch (operator.toUpperCase()) {
@@ -62,7 +51,58 @@ export const collectionQureyBuilder = <T extends keyof Collections>(collection:
6251
default:
6352
condition = `"${String(field)}" ${operator} '${value}'`
6453
}
65-
params.conditions.push(`(${condition})`)
54+
conditions.push(`${condition}`)
55+
return query
56+
},
57+
andWhere(groupFactory: QueryGroupFunction<Collections[T]>) {
58+
const group = groupFactory(collectionQureyGroup(collection))
59+
conditions.push(buildGroup(group, 'and'))
60+
return query
61+
},
62+
orWhere(groupFactory: QueryGroupFunction<Collections[T]>) {
63+
const group = groupFactory(collectionQureyGroup(collection))
64+
conditions.push(buildGroup(group, 'or'))
65+
return query
66+
},
67+
}
68+
69+
return query
70+
}
71+
72+
export const collectionQureyBuilder = <T extends keyof Collections>(collection: T, fetch: (collection: T, sql: string) => Promise<Collections[T][]>): CollectionQueryBuilder<Collections[T]> => {
73+
const params = {
74+
conditions: [] as Array<string>,
75+
selectedFields: [] as Array<keyof Collections[T]>,
76+
offset: 0,
77+
limit: 0,
78+
orderBy: [] as Array<string>,
79+
// Count query
80+
count: {
81+
field: '' as keyof Collections[T] | '*',
82+
distinct: false,
83+
},
84+
}
85+
86+
const query: CollectionQueryBuilder<Collections[T]> = {
87+
andWhere(groupFactory: QueryGroupFunction<Collections[T]>) {
88+
const group = groupFactory(collectionQureyGroup(collection))
89+
params.conditions.push(buildGroup(group, 'and'))
90+
return query
91+
},
92+
orWhere(groupFactory: QueryGroupFunction<Collections[T]>) {
93+
const group = groupFactory(collectionQureyGroup(collection))
94+
params.conditions.push(buildGroup(group, 'or'))
95+
return query
96+
},
97+
path(path: string) {
98+
return query.where('path', '=', withoutTrailingSlash(path))
99+
},
100+
skip(skip: number) {
101+
params.offset = skip
102+
return query
103+
},
104+
where(field: keyof Collections[T] | string, operator: SQLOperator, value?: unknown): CollectionQueryBuilder<Collections[T]> {
105+
query.andWhere(group => group.where(String(field), operator, value))
66106
return query
67107
},
68108
limit(limit: number) {

src/runtime/nitro.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Collections, CollectionQueryBuilder, PageCollections, SurroundOptions, SQLOperator } from '@nuxt/content'
1+
import type { Collections, CollectionQueryBuilder, PageCollections, SurroundOptions, SQLOperator, QueryGroupFunction } from '@nuxt/content'
22
import type { H3Event } from 'h3'
33
import { collectionQureyBuilder } from './internal/query'
44
import { generateNavigationTree } from './internal/navigation'
@@ -8,6 +8,8 @@ import { fetchQuery } from './internal/api'
88

99
interface ChainablePromise<T extends keyof PageCollections, R> extends Promise<R> {
1010
where(field: keyof PageCollections[T] | string, operator: SQLOperator, value?: unknown): ChainablePromise<T, R>
11+
andWhere(groupFactory: QueryGroupFunction<PageCollections[T]>): ChainablePromise<T, R>
12+
orWhere(groupFactory: QueryGroupFunction<PageCollections[T]>): ChainablePromise<T, R>
1113
order(field: keyof PageCollections[T], direction: 'ASC' | 'DESC'): ChainablePromise<T, R>
1214
}
1315

@@ -35,6 +37,14 @@ function chainablePromise<T extends keyof PageCollections, Result>(event: H3Even
3537
queryBuilder.where(String(field), operator, value)
3638
return chainable
3739
},
40+
andWhere(groupFactory) {
41+
queryBuilder.andWhere(groupFactory)
42+
return chainable
43+
},
44+
orWhere(groupFactory) {
45+
queryBuilder.orWhere(groupFactory)
46+
return chainable
47+
},
3848
order(field, direction) {
3949
queryBuilder.order(String(field), direction)
4050
return chainable

src/types/content.ts

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface MarkdownOptions {
5454
export const ContentFileExtension = {
5555
Markdown: 'md',
5656
Yaml: 'yaml',
57+
Yml: 'yml',
5758
Json: 'json',
5859
Csv: 'csv',
5960
Xml: 'xml',

src/types/query.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
export type SQLOperator = '=' | '>' | '<' | '<>' | 'in' | 'BETWEEN' | 'NOT BETWEEN' | 'IS NULL' | 'IS NOT NULL' | 'LIKE' | 'NOT LIKE'
22

3+
export type QueryGroupFunction<T> = (group: CollectionQueryGroup<T>) => CollectionQueryGroup<T>
4+
35
export interface CollectionQueryBuilder<T> {
46
path(path: string): CollectionQueryBuilder<T>
5-
where(field: string, operator: SQLOperator, value?: unknown): CollectionQueryBuilder<T>
67
select<K extends keyof T>(...fields: K[]): CollectionQueryBuilder<Pick<T, K>>
78
order(field: keyof T, direction: 'ASC' | 'DESC'): CollectionQueryBuilder<T>
89
skip(skip: number): CollectionQueryBuilder<T>
910
limit(limit: number): CollectionQueryBuilder<T>
1011
all(): Promise<T[]>
1112
first(): Promise<T>
1213
count(field?: keyof T | '*', distinct?: boolean): Promise<number>
14+
where(field: string, operator: SQLOperator, value?: unknown): CollectionQueryBuilder<T>
15+
andWhere(groupFactory: QueryGroupFunction<T>): CollectionQueryBuilder<T>
16+
orWhere(groupFactory: QueryGroupFunction<T>): CollectionQueryBuilder<T>
17+
}
18+
19+
export interface CollectionQueryGroup<T> {
20+
where(field: string, operator: SQLOperator, value?: unknown): CollectionQueryGroup<T>
21+
andWhere(groupFactory: QueryGroupFunction<T>): CollectionQueryGroup<T>
22+
orWhere(groupFactory: QueryGroupFunction<T>): CollectionQueryGroup<T>
1323
}

src/utils/schema.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ export const pageSchema = z.object({
3232
description: z.string(),
3333
icon: z.string(),
3434
}),
35-
]).default(true),
35+
]).optional().default(true),
3636
})

0 commit comments

Comments
 (0)