Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(typegen): allow injecting groq parameter type with comments #7445

Draft
wants to merge 1 commit into
base: next
Choose a base branch
from
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
14 changes: 13 additions & 1 deletion packages/@sanity/codegen/src/safeParseQuery.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import {parse} from 'groq-js'

import {type QueryParameter} from './typescript/expressionResolvers'

/**
* 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) {
export function safeParseQuery(
query: string,
parameters: QueryParameter[] = [],
): ReturnType<typeof parse> {
const params: Record<string, unknown> = {}

for (const param of extractSliceParams(query)) {
params[param] = 0 // we don't care about the value, just the type
}
for (const param of parameters) {
if (param.typeNode.type === 'unknown') {
continue
}
params[param.name] = param.typeNode.value
}

return parse(query, {params})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,35 @@ describe('findQueries with the groq template', () => {
expect(queries[0].result).toBe('*[_type == "foo bar"]')
})

test('can infer parameters from comment', () => {
const source = `
import { groq } from "groq";

// @groq-parameter {string} $type - The type of the document
// @groq-parameter {string} $language - The wanted language
// @groq-parameter {string} $missing
const postQuery = groq\`*[_type == "foo"]{ "lang": languages[$language] } \`
`

const queries = findQueriesInSource(source, __filename, undefined)
expect(queries.length).toBe(1)
expect(queries[0].parameters.type).toStrictEqual({
name: 'type',
typeNode: {type: 'string', value: ''},
description: 'The type of the document',
})
expect(queries[0].parameters.language).toStrictEqual({
name: 'language',
typeNode: {type: 'string', value: ''},
description: 'The wanted language',
})
expect(queries[0].parameters.missing).toStrictEqual({
name: 'missing',
typeNode: {type: 'string', value: ''},
description: undefined,
})
})

test('will ignore declarations with ignore tag', () => {
const source = `
import { groq } from "groq";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ import {type TransformOptions} from '@babel/core'
import traverse, {type Scope} from '@babel/traverse'
import * as babelTypes from '@babel/types'
import createDebug from 'debug'
import {type PrimitiveTypeNode, type UnknownTypeNode} from 'groq-js'

import {parseSourceFile} from './parseSource'

const debug = createDebug('sanity:codegen:findQueries:debug')

type resolveExpressionReturnType = string

export type QueryParameter = {
name: string
description?: string
typeNode: PrimitiveTypeNode | UnknownTypeNode
}

/**
* NamedQueryResult is a result of a named query
*/
Expand All @@ -20,6 +27,8 @@ export interface NamedQueryResult {
name: string
/** result is a groq query */
result: resolveExpressionReturnType
/** list of parameter to infer types or values */
parameters: Record<string, QueryParameter>

/** location is the location of the query in the source */
location: {
Expand Down
43 changes: 41 additions & 2 deletions packages/@sanity/codegen/src/typescript/findQueriesInSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import {createRequire} from 'node:module'
import {type NodePath, type TransformOptions, traverse} from '@babel/core'
import {type Scope} from '@babel/traverse'
import * as babelTypes from '@babel/types'
import {type PrimitiveTypeNode, type UnknownTypeNode} from 'groq-js'

import {getBabelConfig} from '../getBabelConfig'
import {type NamedQueryResult, resolveExpression} from './expressionResolvers'
import {type NamedQueryResult, type QueryParameter, resolveExpression} from './expressionResolvers'
import {parseSourceFile} from './parseSource'

const require = createRequire(__filename)
Expand Down Expand Up @@ -84,14 +85,52 @@ export function findQueriesInSource(
}
: {}

queries.push({name: queryName, result: queryResult, location})
const parameters = declarationLeadingParameters(path)
queries.push({name: queryName, result: queryResult, location, parameters})
}
},
})

return queries
}

const groqParameterMatcher = /@groq-parameter\s*\{([^}]+)\}\s*\$(\S+)(?:\s*-\s*(.+))?$/
function declarationLeadingParameters(path: NodePath): Record<string, QueryParameter> {
const parameters: Record<string, QueryParameter> = {}

const variableDeclaration = path.find((node) => node.isVariableDeclaration())
if (!variableDeclaration) return parameters
if (!variableDeclaration.node.leadingComments) return parameters
for (const comment of variableDeclaration.node.leadingComments) {
const value = comment.value.trim()
const matches = value.match(groqParameterMatcher)
if (!matches) {
continue
}
const typeName = matches[1]
const name = matches[2]
const description = matches[3]
let typeNode: PrimitiveTypeNode | UnknownTypeNode = {type: 'unknown'}
switch (typeName) {
case 'string': {
typeNode = {type: 'string', value: ''}
break
}
case 'number': {
typeNode = {type: 'number', value: 0}
break
}
default: {
// do nothing
}
}

parameters[name] = {name, typeNode, description}
}

return parameters
}

function declarationLeadingCommentContains(path: NodePath, comment: string): boolean {
/*
* We have to consider these cases:
Expand Down
Loading