Skip to content

Commit 97c0b0c

Browse files
authoredMar 26, 2024··
feat(codegen): attempt to parse groq queries with parameter in slices (#6117)
1 parent 94d0934 commit 97c0b0c

File tree

4 files changed

+90
-2
lines changed

4 files changed

+90
-2
lines changed
 

‎packages/@sanity/cli/src/workers/typegenGenerate.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import {
55
getResolver,
66
readSchema,
77
registerBabel,
8+
safeParseQuery,
89
TypeGenerator,
910
} from '@sanity/codegen'
1011
import createDebug from 'debug'
11-
import {parse, typeEvaluate, type TypeNode} from 'groq-js'
12+
import {typeEvaluate, type TypeNode} from 'groq-js'
1213

1314
const $info = createDebug('sanity:codegen:generate:info')
1415

@@ -99,7 +100,7 @@ async function main() {
99100
}[] = []
100101
for (const {name: queryName, result: query} of result.queries) {
101102
try {
102-
const ast = parse(query)
103+
const ast = safeParseQuery(query)
103104
const queryTypes = typeEvaluate(ast, schema)
104105

105106
const type = typeGenerator.generateTypeNodeTypes(`${queryName}Result`, queryTypes)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {describe, expect, test} from '@jest/globals'
2+
3+
import {extractSliceParams, safeParseQuery} from '../safeParseQuery'
4+
5+
const variants = [
6+
{
7+
query: '*[_type == "author"][$from...$to]',
8+
params: ['from', 'to'],
9+
},
10+
{
11+
query: '*[_type == "author"][$from...5]',
12+
params: ['from'],
13+
},
14+
{
15+
query: '*[_type == "author"][5...$to]',
16+
params: ['to'],
17+
},
18+
{
19+
query: '*[_type == "author"][3...5]',
20+
params: [],
21+
},
22+
{
23+
query: '*[_type == "author"][3...5] { name, "foo": *[_type == "bar"][0...$limit] }',
24+
params: ['limit'],
25+
},
26+
{
27+
query: '*[_type == "author"][$from...$to] { name, "foo": *[_type == "bar"][0...$limit] }',
28+
params: ['from', 'to', 'limit'],
29+
},
30+
]
31+
describe('safeParseQuery', () => {
32+
test.each(variants)('can extract: $query', async (variant) => {
33+
const params = collectAll(extractSliceParams(variant.query))
34+
expect(params).toStrictEqual(variant.params)
35+
})
36+
test.each(variants)('can parse: $query', async (variant) => {
37+
safeParseQuery(variant.query)
38+
})
39+
})
40+
41+
function collectAll<T>(iterator: Generator<T>) {
42+
const res = []
43+
for (const item of iterator) {
44+
res.push(item)
45+
}
46+
return res
47+
}

‎packages/@sanity/codegen/src/_exports/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export {type CodegenConfig, readConfig} from '../readConfig'
22
export {readSchema} from '../readSchema'
3+
export {safeParseQuery} from '../safeParseQuery'
34
export {findQueriesInPath} from '../typescript/findQueriesInPath'
45
export {findQueriesInSource} from '../typescript/findQueriesInSource'
56
export {getResolver} from '../typescript/moduleResolver'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {parse} from 'groq-js'
2+
3+
/**
4+
* safeParseQuery parses a GROQ query string, but first attempts to extract any parameters used in slices. This method is _only_
5+
* intended for use in type generation where we don't actually execute the parsed AST on a dataset, and should not be used elsewhere.
6+
* @internal
7+
*/
8+
export function safeParseQuery(query: string) {
9+
const params: Record<string, unknown> = {}
10+
11+
for (const param of extractSliceParams(query)) {
12+
params[param] = 0 // we don't care about the value, just the type
13+
}
14+
return parse(query, {params})
15+
}
16+
17+
/**
18+
* Finds occurences of `[($start|{number})..($end|{number})]` in a query string and returns the start and end values, and return
19+
* the names of the start and end variables.
20+
* @internal
21+
*/
22+
export function* extractSliceParams(query: string): Generator<string> {
23+
const sliceRegex = /\[(\$(\w+)|\d)\.\.\.?(\$(\w+)|\d)\]/g
24+
const matches = query.matchAll(sliceRegex)
25+
if (!matches) {
26+
return
27+
}
28+
const params = new Set<string>()
29+
for (const match of matches) {
30+
const start = match[1] === `$${match[2]}` ? match[2] : null
31+
if (start !== null) {
32+
yield start
33+
}
34+
const end = match[3] === `$${match[4]}` ? match[4] : null
35+
if (end !== null) {
36+
yield end
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)
Please sign in to comment.