diff --git a/packages/@sanity/codegen/src/safeParseQuery.ts b/packages/@sanity/codegen/src/safeParseQuery.ts index 37bcc2637f8..610c607c62c 100644 --- a/packages/@sanity/codegen/src/safeParseQuery.ts +++ b/packages/@sanity/codegen/src/safeParseQuery.ts @@ -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 { const params: Record = {} 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}) } diff --git a/packages/@sanity/codegen/src/typescript/__tests__/findQueriesInSource.test.ts b/packages/@sanity/codegen/src/typescript/__tests__/findQueriesInSource.test.ts index bd77c2f3047..831a93b88db 100644 --- a/packages/@sanity/codegen/src/typescript/__tests__/findQueriesInSource.test.ts +++ b/packages/@sanity/codegen/src/typescript/__tests__/findQueriesInSource.test.ts @@ -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"; diff --git a/packages/@sanity/codegen/src/typescript/expressionResolvers.ts b/packages/@sanity/codegen/src/typescript/expressionResolvers.ts index 05ad68e7f7f..13bf4bdfddb 100644 --- a/packages/@sanity/codegen/src/typescript/expressionResolvers.ts +++ b/packages/@sanity/codegen/src/typescript/expressionResolvers.ts @@ -5,6 +5,7 @@ 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' @@ -12,6 +13,12 @@ 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 */ @@ -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 /** location is the location of the query in the source */ location: { diff --git a/packages/@sanity/codegen/src/typescript/findQueriesInSource.ts b/packages/@sanity/codegen/src/typescript/findQueriesInSource.ts index 784b2d47370..f5f22dee4ed 100644 --- a/packages/@sanity/codegen/src/typescript/findQueriesInSource.ts +++ b/packages/@sanity/codegen/src/typescript/findQueriesInSource.ts @@ -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) @@ -84,7 +85,8 @@ export function findQueriesInSource( } : {} - queries.push({name: queryName, result: queryResult, location}) + const parameters = declarationLeadingParameters(path) + queries.push({name: queryName, result: queryResult, location, parameters}) } }, }) @@ -92,6 +94,43 @@ export function findQueriesInSource( return queries } +const groqParameterMatcher = /@groq-parameter\s*\{([^}]+)\}\s*\$(\S+)(?:\s*-\s*(.+))?$/ +function declarationLeadingParameters(path: NodePath): Record { + const parameters: Record = {} + + 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: