Skip to content

Commit e8c5508

Browse files
committed
feat: add PostGIS, pgvector, and Unified Search field sections to all generated docs
1 parent c749764 commit e8c5508

3 files changed

Lines changed: 202 additions & 11 deletions

File tree

graphql/codegen/src/core/codegen/cli/docs-generator.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {
77
cleanTypeName,
88
getEditableFields,
99
getSearchFields,
10+
categorizeSpecialFields,
11+
buildSpecialFieldsMarkdown,
12+
buildSpecialFieldsPlain,
1013
getReadmeHeader,
1114
getReadmeFooter,
1215
gqlTypeToJsonSchemaType,
@@ -147,10 +150,8 @@ export function generateReadme(
147150
if (requiredCreate.length === 0 && optionalCreate.length === 0) {
148151
lines.push(`**Create fields:** ${editableFields.map((f) => `\`${f.name}\``).join(', ')}`);
149152
}
150-
const searchFields = getSearchFields(table, registry);
151-
if (searchFields.length > 0) {
152-
lines.push(`**Search API fields (computed, read-only):** ${searchFields.map((f) => `\`${f.name}\``).join(', ')}`);
153-
}
153+
const specialGroups = categorizeSpecialFields(table, registry);
154+
lines.push(...buildSpecialFieldsMarkdown(specialGroups));
154155
lines.push('');
155156
}
156157
}
@@ -347,6 +348,14 @@ export function generateAgentsDocs(
347348
lines.push(` ${f.name}: ${cleanTypeName(f.type.gqlType)}${optLabel}`);
348349
}
349350
lines.push('');
351+
const agentSpecialGroups = categorizeSpecialFields(table, registry);
352+
const agentSpecialLines = buildSpecialFieldsPlain(agentSpecialGroups);
353+
if (agentSpecialLines.length > 0) {
354+
for (const sl of agentSpecialLines) {
355+
lines.push(sl);
356+
}
357+
lines.push('');
358+
}
350359
lines.push('OUTPUT: JSON');
351360
lines.push(` list: [{ ${scalarFields.map((f) => f.name).join(', ')} }]`);
352361
lines.push(` get: { ${scalarFields.map((f) => f.name).join(', ')} }`);
@@ -822,11 +831,17 @@ export function generateSkills(
822831

823832
referenceNames.push(kebab);
824833

834+
const skillSpecialGroups = categorizeSpecialFields(table, registry);
835+
const skillSpecialDesc = skillSpecialGroups.length > 0
836+
? `CRUD operations for ${table.name} records via ${toolName} CLI\n\n` +
837+
skillSpecialGroups.map((g) => `**${g.label}:** ${g.fields.map((f) => `\`${f.name}\``).join(', ')}\n${g.description}`).join('\n\n')
838+
: `CRUD operations for ${table.name} records via ${toolName} CLI`;
839+
825840
files.push({
826841
fileName: `${skillName}/references/${kebab}.md`,
827842
content: buildSkillReference({
828843
title: singularName,
829-
description: `CRUD operations for ${table.name} records via ${toolName} CLI`,
844+
description: skillSpecialDesc,
830845
usage: [
831846
`${toolName} ${kebab} list`,
832847
`${toolName} ${kebab} get --${pk.name} <value>`,
@@ -1156,10 +1171,8 @@ export function generateMultiTargetReadme(
11561171
if (requiredCreate.length === 0 && optionalCreate.length === 0) {
11571172
lines.push(`**Create fields:** ${editableFields.map((f) => `\`${f.name}\``).join(', ')}`);
11581173
}
1159-
const searchFields = getSearchFields(table, registry);
1160-
if (searchFields.length > 0) {
1161-
lines.push(`**Search API fields (computed, read-only):** ${searchFields.map((f) => `\`${f.name}\``).join(', ')}`);
1162-
}
1174+
const mtSpecialGroups = categorizeSpecialFields(table, registry);
1175+
lines.push(...buildSpecialFieldsMarkdown(mtSpecialGroups));
11631176
lines.push('');
11641177
}
11651178

@@ -1407,6 +1420,14 @@ export function generateMultiTargetAgentsDocs(
14071420
lines.push(` ${f.name}: ${cleanTypeName(f.type.gqlType)}${optLabel}`);
14081421
}
14091422
lines.push('');
1423+
const mtAgentSpecialGroups = categorizeSpecialFields(table, registry);
1424+
const mtAgentSpecialLines = buildSpecialFieldsPlain(mtAgentSpecialGroups);
1425+
if (mtAgentSpecialLines.length > 0) {
1426+
for (const sl of mtAgentSpecialLines) {
1427+
lines.push(sl);
1428+
}
1429+
lines.push('');
1430+
}
14101431
lines.push('OUTPUT: JSON');
14111432
lines.push(` list: [{ ${scalarFields.map((f) => f.name).join(', ')} }]`);
14121433
lines.push(` get: { ${scalarFields.map((f) => f.name).join(', ')} }`);
@@ -1960,11 +1981,17 @@ export function generateMultiTargetSkills(
19601981

19611982
tgtReferenceNames.push(kebab);
19621983

1984+
const mtSkillSpecialGroups = categorizeSpecialFields(table, registry);
1985+
const mtSkillSpecialDesc = mtSkillSpecialGroups.length > 0
1986+
? `CRUD operations for ${table.name} records via ${toolName} CLI (${tgt.name} target)\n\n` +
1987+
mtSkillSpecialGroups.map((g) => `**${g.label}:** ${g.fields.map((f) => `\`${f.name}\``).join(', ')}\n${g.description}`).join('\n\n')
1988+
: `CRUD operations for ${table.name} records via ${toolName} CLI (${tgt.name} target)`;
1989+
19631990
files.push({
19641991
fileName: `${tgtSkillName}/references/${kebab}.md`,
19651992
content: buildSkillReference({
19661993
title: singularName,
1967-
description: `CRUD operations for ${table.name} records via ${toolName} CLI (${tgt.name} target)`,
1994+
description: mtSkillSpecialDesc,
19681995
usage: [
19691996
`${toolName} ${cmd} list`,
19701997
`${toolName} ${cmd} get --${pk.name} <value>`,

graphql/codegen/src/core/codegen/docs-utils.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,150 @@ export function getSearchFields(table: CleanTable, typeRegistry?: TypeRegistry):
137137
);
138138
}
139139

140+
// ---------------------------------------------------------------------------
141+
// Special field categorization — PostGIS, pgvector, Unified Search
142+
// ---------------------------------------------------------------------------
143+
144+
export interface SpecialFieldGroup {
145+
/** Category key */
146+
category: 'geospatial' | 'embedding' | 'search';
147+
/** Human-readable label */
148+
label: string;
149+
/** One-line description of this category */
150+
description: string;
151+
/** Fields belonging to this category */
152+
fields: CleanField[];
153+
}
154+
155+
function isPostGISField(f: CleanField): boolean {
156+
const pgType = f.type.pgType?.toLowerCase();
157+
if (pgType === 'geometry' || pgType === 'geography') return true;
158+
const gql = f.type.gqlType;
159+
if (/^(GeoJSON|GeographyPoint|GeographyLineString|GeographyPolygon|GeometryPoint|GeometryLineString|GeometryPolygon|GeographyMulti|GeometryMulti|GeometryCollection|GeographyCollection)/i.test(gql)) return true;
160+
return false;
161+
}
162+
163+
function isEmbeddingField(f: CleanField): boolean {
164+
const pgType = f.type.pgType?.toLowerCase();
165+
if (pgType === 'vector') return true;
166+
if (/embedding$/i.test(f.name) && f.type.isArray && f.type.gqlType === 'Float') return true;
167+
return false;
168+
}
169+
170+
function isTsvectorField(f: CleanField): boolean {
171+
const pgType = f.type.pgType?.toLowerCase();
172+
return pgType === 'tsvector';
173+
}
174+
175+
function isSearchComputedField(f: CleanField): boolean {
176+
if (f.name === 'searchScore') return true;
177+
if (/TrgmSimilarity$/.test(f.name)) return true;
178+
if (/TsvectorRank$/.test(f.name)) return true;
179+
if (/Bm25Score$/.test(f.name)) return true;
180+
return false;
181+
}
182+
183+
/**
184+
* Categorize "special" fields on a table into PostGIS, pgvector, and
185+
* Unified Search groups. Returns only non-empty groups.
186+
*
187+
* The function inspects ALL scalar fields (not just computed ones) so that
188+
* real columns (geometry, vector, tsvector) are also surfaced with
189+
* descriptive context in generated docs.
190+
*/
191+
export function categorizeSpecialFields(
192+
table: CleanTable,
193+
typeRegistry?: TypeRegistry,
194+
): SpecialFieldGroup[] {
195+
const allFields = getScalarFields(table);
196+
const computedFields = getSearchFields(table, typeRegistry);
197+
const computedSet = new Set(computedFields.map((f) => f.name));
198+
199+
const geospatial: CleanField[] = [];
200+
const embedding: CleanField[] = [];
201+
const search: CleanField[] = [];
202+
203+
for (const f of allFields) {
204+
if (isPostGISField(f)) {
205+
geospatial.push(f);
206+
} else if (isEmbeddingField(f)) {
207+
embedding.push(f);
208+
} else if (isTsvectorField(f)) {
209+
search.push(f);
210+
} else if (computedSet.has(f.name) && isSearchComputedField(f)) {
211+
search.push(f);
212+
}
213+
}
214+
215+
const groups: SpecialFieldGroup[] = [];
216+
217+
if (geospatial.length > 0) {
218+
groups.push({
219+
category: 'geospatial',
220+
label: 'PostGIS geospatial fields',
221+
description:
222+
'Geographic/geometric columns managed by PostGIS. Supports spatial queries (distance, containment, intersection) via the Unified Search API PostGIS adapter.',
223+
fields: geospatial,
224+
});
225+
}
226+
227+
if (embedding.length > 0) {
228+
groups.push({
229+
category: 'embedding',
230+
label: 'pgvector embedding fields',
231+
description:
232+
'High-dimensional vector columns for semantic similarity search. Query via the Unified Search API pgvector adapter using cosine, L2, or inner-product distance.',
233+
fields: embedding,
234+
});
235+
}
236+
237+
if (search.length > 0) {
238+
groups.push({
239+
category: 'search',
240+
label: 'Unified Search API fields',
241+
description:
242+
'Fields provided by the Unified Search plugin. Includes full-text search (tsvector/BM25), trigram similarity scores, and the combined searchScore. Computed fields are read-only and cannot be set in create/update operations.',
243+
fields: search,
244+
});
245+
}
246+
247+
return groups;
248+
}
249+
250+
/**
251+
* Build markdown lines describing special fields for README-style docs.
252+
* Returns empty array when there are no special fields.
253+
*/
254+
export function buildSpecialFieldsMarkdown(groups: SpecialFieldGroup[]): string[] {
255+
if (groups.length === 0) return [];
256+
const lines: string[] = [];
257+
for (const g of groups) {
258+
const fieldList = g.fields.map((f) => `\`${f.name}\``).join(', ');
259+
lines.push(`> **${g.label}:** ${fieldList}`);
260+
lines.push(`> ${g.description}`);
261+
lines.push('');
262+
}
263+
return lines;
264+
}
265+
266+
/**
267+
* Build plain-text lines describing special fields for AGENTS-style docs.
268+
* Returns empty array when there are no special fields.
269+
*/
270+
export function buildSpecialFieldsPlain(groups: SpecialFieldGroup[]): string[] {
271+
if (groups.length === 0) return [];
272+
const lines: string[] = [];
273+
lines.push('SPECIAL FIELDS:');
274+
for (const g of groups) {
275+
lines.push(` [${g.label}]`);
276+
lines.push(` ${g.description}`);
277+
for (const f of g.fields) {
278+
lines.push(` ${f.name}: ${cleanTypeName(f.type.gqlType)}`);
279+
}
280+
}
281+
return lines;
282+
}
283+
140284
/**
141285
* Represents a flattened argument for docs/skills generation.
142286
* INPUT_OBJECT args are expanded to dot-notation fields.

graphql/codegen/src/core/codegen/orm/docs-generator.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {
66
buildSkillReference,
77
formatArgType,
88
getEditableFields,
9+
categorizeSpecialFields,
10+
buildSpecialFieldsMarkdown,
11+
buildSpecialFieldsPlain,
912
getReadmeHeader,
1013
getReadmeFooter,
1114
gqlTypeToJsonSchemaType,
@@ -104,6 +107,8 @@ export function generateOrmReadme(
104107
);
105108
lines.push('```');
106109
lines.push('');
110+
const ormSpecialGroups = categorizeSpecialFields(table);
111+
lines.push(...buildSpecialFieldsMarkdown(ormSpecialGroups));
107112
}
108113
}
109114

@@ -228,6 +233,14 @@ export function generateOrmAgentsDocs(
228233
lines.push(` ${f.name}: ${fieldTypeToTs(f.type)}`);
229234
}
230235
lines.push('');
236+
const ormAgentSpecialGroups = categorizeSpecialFields(table);
237+
const ormAgentSpecialLines = buildSpecialFieldsPlain(ormAgentSpecialGroups);
238+
if (ormAgentSpecialLines.length > 0) {
239+
for (const sl of ormAgentSpecialLines) {
240+
lines.push(sl);
241+
}
242+
lines.push('');
243+
}
231244
lines.push('OUTPUT: Promise<JSON>');
232245
lines.push(
233246
` findMany: [{ ${scalarFields.map((f) => f.name).join(', ')} }]`,
@@ -468,11 +481,18 @@ export function generateOrmSkills(
468481
const refName = toKebabCase(singularName);
469482
referenceNames.push(refName);
470483

484+
const ormSkillSpecialGroups = categorizeSpecialFields(table);
485+
const ormSkillBaseDesc = table.description || `ORM operations for ${table.name} records`;
486+
const ormSkillSpecialDesc = ormSkillSpecialGroups.length > 0
487+
? ormSkillBaseDesc + '\n\n' +
488+
ormSkillSpecialGroups.map((g) => `**${g.label}:** ${g.fields.map((f) => `\`${f.name}\``).join(', ')}\n${g.description}`).join('\n\n')
489+
: ormSkillBaseDesc;
490+
471491
files.push({
472492
fileName: `${skillName}/references/${refName}.md`,
473493
content: buildSkillReference({
474494
title: singularName,
475-
description: table.description || `ORM operations for ${table.name} records`,
495+
description: ormSkillSpecialDesc,
476496
language: 'typescript',
477497
usage: [
478498
`db.${modelName}.findMany({ select: { id: true } }).execute()`,

0 commit comments

Comments
 (0)