Skip to content

Commit

Permalink
feat(codegen): attempt to parse groq queries with parameter in slices (
Browse files Browse the repository at this point in the history
  • Loading branch information
sgulseth authored Mar 26, 2024
1 parent 94d0934 commit 97c0b0c
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 2 deletions.
5 changes: 3 additions & 2 deletions packages/@sanity/cli/src/workers/typegenGenerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
getResolver,
readSchema,
registerBabel,
safeParseQuery,
TypeGenerator,
} from '@sanity/codegen'
import createDebug from 'debug'
import {parse, typeEvaluate, type TypeNode} from 'groq-js'
import {typeEvaluate, type TypeNode} from 'groq-js'

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

Expand Down Expand Up @@ -99,7 +100,7 @@ async function main() {
}[] = []
for (const {name: queryName, result: query} of result.queries) {
try {
const ast = parse(query)
const ast = safeParseQuery(query)
const queryTypes = typeEvaluate(ast, schema)

const type = typeGenerator.generateTypeNodeTypes(`${queryName}Result`, queryTypes)
Expand Down
47 changes: 47 additions & 0 deletions packages/@sanity/codegen/src/__tests__/safeParseQuery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {describe, expect, test} from '@jest/globals'

import {extractSliceParams, safeParseQuery} from '../safeParseQuery'

const variants = [
{
query: '*[_type == "author"][$from...$to]',
params: ['from', 'to'],
},
{
query: '*[_type == "author"][$from...5]',
params: ['from'],
},
{
query: '*[_type == "author"][5...$to]',
params: ['to'],
},
{
query: '*[_type == "author"][3...5]',
params: [],
},
{
query: '*[_type == "author"][3...5] { name, "foo": *[_type == "bar"][0...$limit] }',
params: ['limit'],
},
{
query: '*[_type == "author"][$from...$to] { name, "foo": *[_type == "bar"][0...$limit] }',
params: ['from', 'to', 'limit'],
},
]
describe('safeParseQuery', () => {
test.each(variants)('can extract: $query', async (variant) => {
const params = collectAll(extractSliceParams(variant.query))
expect(params).toStrictEqual(variant.params)
})
test.each(variants)('can parse: $query', async (variant) => {
safeParseQuery(variant.query)
})
})

function collectAll<T>(iterator: Generator<T>) {
const res = []
for (const item of iterator) {
res.push(item)
}
return res
}
1 change: 1 addition & 0 deletions packages/@sanity/codegen/src/_exports/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {type CodegenConfig, readConfig} from '../readConfig'
export {readSchema} from '../readSchema'
export {safeParseQuery} from '../safeParseQuery'
export {findQueriesInPath} from '../typescript/findQueriesInPath'
export {findQueriesInSource} from '../typescript/findQueriesInSource'
export {getResolver} from '../typescript/moduleResolver'
Expand Down
39 changes: 39 additions & 0 deletions packages/@sanity/codegen/src/safeParseQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {parse} from 'groq-js'

/**
* safeParseQuery parses a GROQ query string, but first attempts to extract any parameters used in slices. This method is _only_
* intended for use in type generation where we don't actually execute the parsed AST on a dataset, and should not be used elsewhere.
* @internal
*/
export function safeParseQuery(query: string) {
const params: Record<string, unknown> = {}

for (const param of extractSliceParams(query)) {
params[param] = 0 // we don't care about the value, just the type
}
return parse(query, {params})
}

/**
* Finds occurences of `[($start|{number})..($end|{number})]` in a query string and returns the start and end values, and return
* the names of the start and end variables.
* @internal
*/
export function* extractSliceParams(query: string): Generator<string> {
const sliceRegex = /\[(\$(\w+)|\d)\.\.\.?(\$(\w+)|\d)\]/g
const matches = query.matchAll(sliceRegex)
if (!matches) {
return
}
const params = new Set<string>()
for (const match of matches) {
const start = match[1] === `$${match[2]}` ? match[2] : null
if (start !== null) {
yield start
}
const end = match[3] === `$${match[4]}` ? match[4] : null
if (end !== null) {
yield end
}
}
}

0 comments on commit 97c0b0c

Please sign in to comment.