Skip to content
Merged
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
18 changes: 6 additions & 12 deletions graphql/codegen/src/core/codegen/cli/docs-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
getSearchFields,
categorizeSpecialFields,
buildSpecialFieldsMarkdown,
buildSearchExamples,
buildSearchExamplesMarkdown,
getReadmeHeader,
getReadmeFooter,
gqlTypeToJsonSchemaType,
Expand Down Expand Up @@ -160,6 +162,7 @@ export function generateReadme(
}
const specialGroups = categorizeSpecialFields(table, registry);
lines.push(...buildSpecialFieldsMarkdown(specialGroups));
lines.push(...buildSearchExamplesMarkdown(specialGroups, toolName, kebab));
lines.push('');
}
}
Expand Down Expand Up @@ -430,12 +433,7 @@ export function generateSkills(
description: `List ${singularName} records with filtering and ordering`,
code: [`${toolName} ${kebab} list --where.${pk.name}.equalTo <value> --orderBy ${pk.name.replace(/([A-Z])/g, '_$1').toUpperCase()}_ASC`],
},
...(skillSpecialGroups.some((g) => g.category === 'search' || g.category === 'embedding')
? [{
description: `Search ${singularName} records`,
code: [`${toolName} ${kebab} search "query text" --limit 10 --fields id,searchScore`],
}]
: []),
...buildSearchExamples(skillSpecialGroups, toolName, kebab),
{
description: `Create a ${singularName}`,
code: [
Expand Down Expand Up @@ -763,6 +761,7 @@ export function generateMultiTargetReadme(
}
const mtSpecialGroups = categorizeSpecialFields(table, registry);
lines.push(...buildSpecialFieldsMarkdown(mtSpecialGroups));
lines.push(...buildSearchExamplesMarkdown(mtSpecialGroups, toolName, `${tgt.name}:${kebab}`));
lines.push('');
}

Expand Down Expand Up @@ -1096,12 +1095,7 @@ export function generateMultiTargetSkills(
description: `List ${singularName} records with filtering and ordering`,
code: [`${toolName} ${cmd} list --where.${pk.name}.equalTo <value> --orderBy ${pk.name.replace(/([A-Z])/g, '_$1').toUpperCase()}_ASC`],
},
...(mtSkillSpecialGroups.some((g) => g.category === 'search' || g.category === 'embedding')
? [{
description: `Search ${singularName} records`,
code: [`${toolName} ${cmd} search "query text" --limit 10 --fields id,searchScore`],
}]
: []),
...buildSearchExamples(mtSkillSpecialGroups, toolName, cmd),
{
description: `Create a ${singularName}`,
code: [
Expand Down
139 changes: 139 additions & 0 deletions graphql/codegen/src/core/codegen/docs-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,145 @@ export function buildSpecialFieldsPlain(groups: SpecialFieldGroup[]): string[] {
return lines;
}

// ---------------------------------------------------------------------------
// Search-specific CLI examples for generated docs
// ---------------------------------------------------------------------------

export interface SearchExample {
description: string;
code: string[];
}

/**
* Build concrete, field-specific CLI examples for tables with search fields.
* Uses the same field-name derivation logic as buildSearchHandler in
* table-command-generator.ts so the examples match the actual generated code.
*
* Returns an empty array when the table has no search/embedding fields.
*/
export function buildSearchExamples(
specialGroups: SpecialFieldGroup[],
toolName: string,
cmd: string,
): SearchExample[] {
const examples: SearchExample[] = [];
const scoreFields: string[] = [];

for (const group of specialGroups) {
for (const field of group.fields) {
// tsvector (FullText scalar) — where input uses the column name directly
if (field.type.gqlType === 'FullText' && !field.type.isArray) {
examples.push({
description: `Full-text search via tsvector (\`${field.name}\`)`,
code: [
`${toolName} ${cmd} list --where.${field.name} "search query" --fields title,tsvRank`,
],
});
scoreFields.push('tsvRank');
}

// BM25 computed score — bodyBm25Score → bm25Body
if (/Bm25Score$/.test(field.name)) {
const baseName = field.name.replace(/Bm25Score$/, '');
const inputName = `bm25${baseName.charAt(0).toUpperCase()}${baseName.slice(1)}`;
examples.push({
description: `BM25 keyword search via \`${inputName}\``,
code: [
`${toolName} ${cmd} list --where.${inputName}.query "search query" --fields title,${field.name}`,
],
});
scoreFields.push(field.name);
}

// Trigram similarity — titleTrgmSimilarity → trgmTitle
if (/TrgmSimilarity$/.test(field.name)) {
const baseName = field.name.replace(/TrgmSimilarity$/, '');
const inputName = `trgm${baseName.charAt(0).toUpperCase()}${baseName.slice(1)}`;
examples.push({
description: `Fuzzy search via trigram similarity (\`${inputName}\`)`,
code: [
`${toolName} ${cmd} list --where.${inputName}.value "approximate query" --where.${inputName}.threshold 0.3 --fields title,${field.name}`,
],
});
scoreFields.push(field.name);
}

// pgvector embedding — uses column name, note about CLI limitation
if (group.category === 'embedding') {
examples.push({
description: `Vector similarity search via \`${field.name}\` (requires JSON array)`,
code: [
`# Note: vector arrays must be passed as JSON strings via dot-notation`,
`${toolName} ${cmd} list --where.${field.name}.vector '[0.1,0.2,0.3]' --where.${field.name}.distance 1.0 --fields title,${field.name}VectorDistance`,
],
});
}

// searchScore — composite blend field, useful for ordering
if (field.name === 'searchScore') {
scoreFields.push('searchScore');
}
}
}

// Composite fullTextSearch example (dispatches to all text adapters)
const hasTextSearch = specialGroups.some(
(g) => g.category === 'search' && g.fields.some(
(f) => f.type.gqlType === 'FullText' || /TrgmSimilarity$/.test(f.name) || /Bm25Score$/.test(f.name),
),
);
if (hasTextSearch) {
const fieldsArg = scoreFields.length > 0
? `title,${[...new Set(scoreFields)].join(',')}`
: 'title';
examples.push({
description: 'Composite search (fullTextSearch dispatches to all text adapters)',
code: [
`${toolName} ${cmd} list --where.fullTextSearch "search query" --fields ${fieldsArg}`,
],
});
}

// Combined search + pagination + ordering example
if (examples.length > 0) {
examples.push({
description: 'Search with pagination and field projection',
code: [
`${toolName} ${cmd} list --where.fullTextSearch "query" --limit 10 --fields id,title,searchScore`,
`${toolName} ${cmd} search "query" --limit 10 --fields id,title,searchScore`,
],
});
}

return examples;
}

/**
* Build markdown lines for search-specific examples in README-style docs.
* Returns empty array when there are no search examples.
*/
export function buildSearchExamplesMarkdown(
specialGroups: SpecialFieldGroup[],
toolName: string,
cmd: string,
): string[] {
const examples = buildSearchExamples(specialGroups, toolName, cmd);
if (examples.length === 0) return [];
const lines: string[] = [];
lines.push('**Search Examples:**');
lines.push('');
for (const ex of examples) {
lines.push(`*${ex.description}:*`);
lines.push('```bash');
for (const c of ex.code) {
lines.push(c);
}
lines.push('```');
lines.push('');
}
return lines;
}

/**
* Represents a flattened argument for docs/skills generation.
* INPUT_OBJECT args are expanded to dot-notation fields.
Expand Down
Loading
Loading