Skip to content

Commit

Permalink
feat(typegen): allow injecting groq parameter type with comments
Browse files Browse the repository at this point in the history
  • Loading branch information
sgulseth committed Aug 30, 2024
1 parent c259119 commit 3b1ca47
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 3 deletions.
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

0 comments on commit 3b1ca47

Please sign in to comment.