@constructive-io/graphql-codegen 4.26.0 → 4.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/core/codegen/cli/command-map-generator.js +9 -9
  2. package/core/codegen/cli/custom-command-generator.js +2 -2
  3. package/core/codegen/cli/docs-generator.js +23 -31
  4. package/core/codegen/cli/index.d.ts +3 -1
  5. package/core/codegen/cli/index.js +16 -1
  6. package/core/codegen/cli/table-command-generator.d.ts +2 -0
  7. package/core/codegen/cli/table-command-generator.js +250 -36
  8. package/core/codegen/cli/utils-generator.d.ts +8 -0
  9. package/core/codegen/cli/utils-generator.js +14 -0
  10. package/core/codegen/docs-utils.d.ts +17 -0
  11. package/core/codegen/docs-utils.js +127 -0
  12. package/core/codegen/hooks-docs-generator.js +3 -3
  13. package/core/codegen/index.js +1 -1
  14. package/core/codegen/orm/docs-generator.js +3 -3
  15. package/core/codegen/orm/index.js +1 -1
  16. package/core/codegen/orm/input-types-generator.js +1 -1
  17. package/core/codegen/orm/model-generator.js +1 -1
  18. package/core/codegen/queries.js +1 -1
  19. package/core/codegen/templates/cli-utils.ts +11 -11
  20. package/core/codegen/templates/embedder.ts +175 -0
  21. package/core/generate.js +2 -0
  22. package/esm/core/codegen/cli/command-map-generator.js +1 -1
  23. package/esm/core/codegen/cli/custom-command-generator.js +1 -1
  24. package/esm/core/codegen/cli/docs-generator.js +7 -15
  25. package/esm/core/codegen/cli/index.d.ts +3 -1
  26. package/esm/core/codegen/cli/index.js +16 -2
  27. package/esm/core/codegen/cli/table-command-generator.d.ts +2 -0
  28. package/esm/core/codegen/cli/table-command-generator.js +250 -36
  29. package/esm/core/codegen/cli/utils-generator.d.ts +8 -0
  30. package/esm/core/codegen/cli/utils-generator.js +13 -0
  31. package/esm/core/codegen/docs-utils.d.ts +17 -0
  32. package/esm/core/codegen/docs-utils.js +125 -0
  33. package/esm/core/codegen/hooks-docs-generator.js +1 -1
  34. package/esm/core/codegen/index.js +1 -1
  35. package/esm/core/codegen/orm/docs-generator.js +1 -1
  36. package/esm/core/codegen/orm/index.js +1 -1
  37. package/esm/core/codegen/orm/input-types-generator.js +1 -1
  38. package/esm/core/codegen/orm/model-generator.js +1 -1
  39. package/esm/core/codegen/queries.js +1 -1
  40. package/esm/core/generate.js +2 -0
  41. package/package.json +8 -9
@@ -1,7 +1,7 @@
1
1
  import * as t from '@babel/types';
2
- import { toKebabCase } from 'komoji';
2
+ import { toKebabCase } from 'inflekt';
3
3
  import { generateCode } from '../babel-ast';
4
- import { getGeneratedFileHeader, getPrimaryKeyInfo, getScalarFields, getSelectableScalarFields, getTableNames, getWritableFieldNames, resolveInnerInputType, ucFirst, lcFirst, toPascalCase, getCreateInputTypeName, getPatchTypeName, } from '../utils';
4
+ import { getGeneratedFileHeader, getPrimaryKeyInfo, getScalarFields, getSelectableScalarFields, getTableNames, getWritableFieldNames, resolveInnerInputType, ucFirst, lcFirst, toPascalCase, getCreateInputTypeName, getPatchTypeName, getFilterTypeName, getOrderByTypeName, getConditionTypeName, } from '../utils';
5
5
  import { categorizeSpecialFields } from '../docs-utils';
6
6
  function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
7
7
  const specifiers = namedImports.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)));
@@ -165,7 +165,146 @@ function buildSubcommandSwitch(subcommands, handlerPrefix, usageVarName) {
165
165
  ]));
166
166
  return t.switchStatement(t.identifier('subcommand'), cases);
167
167
  }
168
- function buildListHandler(table, targetName, typeRegistry) {
168
+ /**
169
+ * Build an auto-embed block that resolves the embedder and converts text
170
+ * values in the where clause to vector embeddings.
171
+ *
172
+ * Generates code equivalent to:
173
+ * if (argv['auto-embed']) {
174
+ * const embedder = resolveEmbedder();
175
+ * if (!embedder) {
176
+ * console.error('--auto-embed requires an embedder. Set EMBEDDER_PROVIDER=ollama');
177
+ * process.exit(1);
178
+ * }
179
+ * await autoEmbedWhere(findManyArgs.where ?? {}, ['fieldA', 'fieldB'], embedder);
180
+ * }
181
+ *
182
+ * @param whereExpr - The expression to access the where clause (e.g. findManyArgs.where, searchWhere)
183
+ * @param vectorFieldNames - Names of vector embedding fields detected at codegen time
184
+ * @param assignBack - If true, assigns the result back to findManyArgs.where
185
+ */
186
+ function buildAutoEmbedBlock(whereExpr, vectorFieldNames, assignBack = false) {
187
+ const fieldNamesArray = t.arrayExpression(vectorFieldNames.map((n) => t.stringLiteral(n)));
188
+ const embedderDecl = t.variableDeclaration('const', [
189
+ t.variableDeclarator(t.identifier('embedder'), t.callExpression(t.identifier('resolveEmbedder'), [])),
190
+ ]);
191
+ const noEmbedderCheck = t.ifStatement(t.unaryExpression('!', t.identifier('embedder')), t.blockStatement([
192
+ t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('console'), t.identifier('error')), [
193
+ t.stringLiteral('--auto-embed requires an embedder. Set EMBEDDER_PROVIDER=ollama (and optionally EMBEDDER_MODEL, EMBEDDER_BASE_URL).'),
194
+ ])),
195
+ t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('process'), t.identifier('exit')), [t.numericLiteral(1)])),
196
+ ]));
197
+ const autoEmbedCall = t.awaitExpression(t.callExpression(t.identifier('autoEmbedWhere'), [
198
+ t.logicalExpression('??', whereExpr, t.objectExpression([])),
199
+ fieldNamesArray,
200
+ t.identifier('embedder'),
201
+ ]));
202
+ const bodyStatements = [embedderDecl, noEmbedderCheck];
203
+ if (assignBack) {
204
+ // findManyArgs.where = await autoEmbedWhere(findManyArgs.where ?? {}, [...], embedder);
205
+ bodyStatements.push(t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.identifier('findManyArgs'), t.identifier('where')), autoEmbedCall)));
206
+ }
207
+ else {
208
+ // await autoEmbedWhere(searchWhere, [...], embedder);
209
+ bodyStatements.push(t.expressionStatement(autoEmbedCall));
210
+ }
211
+ return t.ifStatement(t.memberExpression(t.identifier('argv'), t.stringLiteral('auto-embed'), true), t.blockStatement(bodyStatements));
212
+ }
213
+ /**
214
+ * Build an auto-embed block for mutation handlers (create/update).
215
+ *
216
+ * Generates code equivalent to:
217
+ * if (argv['auto-embed']) {
218
+ * const embedder = resolveEmbedder();
219
+ * if (!embedder) {
220
+ * console.error('--auto-embed requires an embedder. Set EMBEDDER_PROVIDER=ollama');
221
+ * process.exit(1);
222
+ * }
223
+ * await autoEmbedInput(cleanedData, ['embedding'], embedder);
224
+ * }
225
+ *
226
+ * @param vectorFieldNames - Names of vector embedding fields detected at codegen time
227
+ */
228
+ function buildAutoEmbedInputBlock(vectorFieldNames) {
229
+ const fieldNamesArray = t.arrayExpression(vectorFieldNames.map((n) => t.stringLiteral(n)));
230
+ const embedderDecl = t.variableDeclaration('const', [
231
+ t.variableDeclarator(t.identifier('embedder'), t.callExpression(t.identifier('resolveEmbedder'), [])),
232
+ ]);
233
+ const noEmbedderCheck = t.ifStatement(t.unaryExpression('!', t.identifier('embedder')), t.blockStatement([
234
+ t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('console'), t.identifier('error')), [
235
+ t.stringLiteral('--auto-embed requires an embedder. Set EMBEDDER_PROVIDER=ollama (and optionally EMBEDDER_MODEL, EMBEDDER_BASE_URL).'),
236
+ ])),
237
+ t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('process'), t.identifier('exit')), [t.numericLiteral(1)])),
238
+ ]));
239
+ const autoEmbedCall = t.awaitExpression(t.callExpression(t.identifier('autoEmbedInput'), [
240
+ t.identifier('cleanedData'),
241
+ fieldNamesArray,
242
+ t.identifier('embedder'),
243
+ ]));
244
+ return t.ifStatement(t.memberExpression(t.identifier('argv'), t.stringLiteral('auto-embed'), true), t.blockStatement([
245
+ embedderDecl,
246
+ noEmbedderCheck,
247
+ t.expressionStatement(autoEmbedCall),
248
+ ]));
249
+ }
250
+ /**
251
+ * Build the FindManyArgs type instantiation for a table:
252
+ * FindManyArgs<SelectType, FilterType, ConditionType, OrderByType> & { select: SelectType }
253
+ *
254
+ * The intersection with { select: SelectType } makes select required,
255
+ * matching what the ORM's findMany method expects. parseFindManyArgs
256
+ * always sets select at runtime (from defaultSelect or --select flag).
257
+ */
258
+ function buildFindManyArgsType(table, conditionEnabled) {
259
+ const { typeName } = getTableNames(table);
260
+ const selectTypeName = `${typeName}Select`;
261
+ const whereTypeName = getFilterTypeName(table);
262
+ const conditionTypeName = conditionEnabled ? getConditionTypeName(table) : undefined;
263
+ const orderByTypeName = getOrderByTypeName(table);
264
+ const findManyType = t.tsTypeReference(t.identifier('FindManyArgs'), t.tsTypeParameterInstantiation([
265
+ t.tsTypeReference(t.identifier(selectTypeName)),
266
+ t.tsTypeReference(t.identifier(whereTypeName)),
267
+ conditionTypeName
268
+ ? t.tsTypeReference(t.identifier(conditionTypeName))
269
+ : t.tsNeverKeyword(),
270
+ t.tsTypeReference(t.identifier(orderByTypeName)),
271
+ ]));
272
+ // Intersect with { select: SelectType } to make select required
273
+ return t.tsIntersectionType([
274
+ findManyType,
275
+ t.tsTypeLiteral([
276
+ Object.assign(t.tsPropertySignature(t.identifier('select'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(selectTypeName)))), { optional: false }),
277
+ ]),
278
+ ]);
279
+ }
280
+ /**
281
+ * Build the FindFirstArgs type instantiation for a table:
282
+ * FindFirstArgs<SelectType, FilterType, ConditionType> & { select: SelectType }
283
+ *
284
+ * The intersection with { select: SelectType } makes select required,
285
+ * matching what the ORM's findFirst method expects.
286
+ */
287
+ function buildFindFirstArgsType(table, conditionEnabled) {
288
+ const { typeName } = getTableNames(table);
289
+ const selectTypeName = `${typeName}Select`;
290
+ const whereTypeName = getFilterTypeName(table);
291
+ const conditionTypeName = conditionEnabled ? getConditionTypeName(table) : undefined;
292
+ const findFirstType = t.tsTypeReference(t.identifier('FindFirstArgs'), t.tsTypeParameterInstantiation([
293
+ t.tsTypeReference(t.identifier(selectTypeName)),
294
+ t.tsTypeReference(t.identifier(whereTypeName)),
295
+ conditionTypeName
296
+ ? t.tsTypeReference(t.identifier(conditionTypeName))
297
+ : t.tsNeverKeyword(),
298
+ ]));
299
+ // Intersect with { select: SelectType } to make select required
300
+ return t.tsIntersectionType([
301
+ findFirstType,
302
+ t.tsTypeLiteral([
303
+ Object.assign(t.tsPropertySignature(t.identifier('select'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(selectTypeName)))), { optional: false }),
304
+ ]),
305
+ ]);
306
+ }
307
+ function buildListHandler(table, vectorFieldNames, targetName, typeRegistry, conditionEnabled = false) {
169
308
  const { singularName } = getTableNames(table);
170
309
  const defaultSelectObj = buildSelectObject(table, typeRegistry);
171
310
  // --- Build the try body ---
@@ -174,13 +313,23 @@ function buildListHandler(table, targetName, typeRegistry) {
174
313
  tryBody.push(t.variableDeclaration('const', [
175
314
  t.variableDeclarator(t.identifier('defaultSelect'), defaultSelectObj),
176
315
  ]));
177
- // const findManyArgs = parseFindManyArgs(argv, defaultSelect);
178
- tryBody.push(t.variableDeclaration('const', [
179
- t.variableDeclarator(t.identifier('findManyArgs'), t.callExpression(t.identifier('parseFindManyArgs'), [
316
+ // const findManyArgs = parseFindManyArgs<FindManyArgs<...>>(argv, defaultSelect);
317
+ {
318
+ const callExpr = t.callExpression(t.identifier('parseFindManyArgs'), [
180
319
  t.identifier('argv'),
181
320
  t.identifier('defaultSelect'),
182
- ])),
183
- ]));
321
+ ]);
322
+ callExpr.typeParameters = t.tsTypeParameterInstantiation([
323
+ buildFindManyArgsType(table, conditionEnabled),
324
+ ]);
325
+ tryBody.push(t.variableDeclaration('const', [
326
+ t.variableDeclarator(t.identifier('findManyArgs'), callExpr),
327
+ ]));
328
+ }
329
+ // Auto-embed vector fields in the where clause when --auto-embed is passed
330
+ if (vectorFieldNames.length > 0) {
331
+ tryBody.push(buildAutoEmbedBlock(t.memberExpression(t.identifier('findManyArgs'), t.identifier('where')), vectorFieldNames, true));
332
+ }
184
333
  tryBody.push(buildGetClientStatement(targetName));
185
334
  // const result = await client.<singular>.findMany(findManyArgs).execute();
186
335
  tryBody.push(t.variableDeclaration('const', [
@@ -197,10 +346,10 @@ function buildListHandler(table, targetName, typeRegistry) {
197
346
  }
198
347
  /**
199
348
  * Build a `handleFindFirst` function — CLI equivalent of the TS SDK's findFirst().
200
- * Accepts --fields, --where.<field>.<op>, --condition.<field>.<op> flags.
349
+ * Accepts --select, --where.<field>.<op>, --condition.<field>.<op> flags.
201
350
  * Internally calls findMany with first:1 and returns a single record (or null).
202
351
  */
203
- function buildFindFirstHandler(table, targetName, typeRegistry) {
352
+ function buildFindFirstHandler(table, targetName, typeRegistry, conditionEnabled = false) {
204
353
  const { singularName } = getTableNames(table);
205
354
  const defaultSelectObj = buildSelectObject(table, typeRegistry);
206
355
  const tryBody = [];
@@ -208,13 +357,19 @@ function buildFindFirstHandler(table, targetName, typeRegistry) {
208
357
  tryBody.push(t.variableDeclaration('const', [
209
358
  t.variableDeclarator(t.identifier('defaultSelect'), defaultSelectObj),
210
359
  ]));
211
- // const findFirstArgs = parseFindFirstArgs(argv, defaultSelect);
212
- tryBody.push(t.variableDeclaration('const', [
213
- t.variableDeclarator(t.identifier('findFirstArgs'), t.callExpression(t.identifier('parseFindFirstArgs'), [
360
+ // const findFirstArgs = parseFindFirstArgs<FindFirstArgs<...>>(argv, defaultSelect);
361
+ {
362
+ const callExpr = t.callExpression(t.identifier('parseFindFirstArgs'), [
214
363
  t.identifier('argv'),
215
364
  t.identifier('defaultSelect'),
216
- ])),
217
- ]));
365
+ ]);
366
+ callExpr.typeParameters = t.tsTypeParameterInstantiation([
367
+ buildFindFirstArgsType(table, conditionEnabled),
368
+ ]);
369
+ tryBody.push(t.variableDeclaration('const', [
370
+ t.variableDeclarator(t.identifier('findFirstArgs'), callExpr),
371
+ ]));
372
+ }
218
373
  tryBody.push(buildGetClientStatement(targetName));
219
374
  // const result = await client.<singular>.findFirst(findFirstArgs).execute();
220
375
  tryBody.push(t.variableDeclaration('const', [
@@ -233,9 +388,9 @@ function buildFindFirstHandler(table, targetName, typeRegistry) {
233
388
  * Build a `handleSearch` function for tables with search-capable fields.
234
389
  * Extracts the first positional arg as the query string and auto-builds a
235
390
  * `where` clause that targets all detected search fields (tsvector, BM25,
236
- * trigram, vector embedding). Supports --limit, --offset, --fields, --orderBy.
391
+ * trigram, vector embedding). Supports --limit, --offset, --select, --orderBy.
237
392
  */
238
- function buildSearchHandler(table, specialGroups, targetName, typeRegistry) {
393
+ function buildSearchHandler(table, specialGroups, vectorFieldNames, targetName, typeRegistry, conditionEnabled = false) {
239
394
  const { singularName } = getTableNames(table);
240
395
  const defaultSelectObj = buildSelectObject(table, typeRegistry);
241
396
  const tryBody = [];
@@ -251,7 +406,7 @@ function buildSearchHandler(table, specialGroups, targetName, typeRegistry) {
251
406
  t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('process'), t.identifier('exit')), [t.numericLiteral(1)])),
252
407
  ])));
253
408
  // Build the where clause properties from detected search fields
254
- // e.g. { tsvContent: { query }, bm25Body: { query }, trgmTitle: { value: query, threshold: 0.3 }, vectorEmbedding: { value: query } }
409
+ // e.g. { tsvContent: { query }, bm25Body: { query }, trgmTitle: { value: query, threshold: 0.3 }, vectorEmbedding: { vector: query } }
255
410
  const whereProps = [];
256
411
  for (const group of specialGroups) {
257
412
  for (const field of group.fields) {
@@ -281,9 +436,10 @@ function buildSearchHandler(table, specialGroups, targetName, typeRegistry) {
281
436
  ])));
282
437
  }
283
438
  else if (group.category === 'embedding') {
284
- // Vector embedding field: { value: query }
439
+ // Vector embedding field: { vector: query }
440
+ // When --auto-embed is used, autoEmbedWhere will convert the text to a vector
285
441
  whereProps.push(t.objectProperty(t.identifier(field.name), t.objectExpression([
286
- t.objectProperty(t.identifier('value'), t.identifier('query')),
442
+ t.objectProperty(t.identifier('vector'), t.identifier('query')),
287
443
  ])));
288
444
  }
289
445
  }
@@ -292,18 +448,28 @@ function buildSearchHandler(table, specialGroups, targetName, typeRegistry) {
292
448
  tryBody.push(t.variableDeclaration('const', [
293
449
  t.variableDeclarator(t.identifier('searchWhere'), t.objectExpression(whereProps)),
294
450
  ]));
451
+ // Auto-embed vector fields in the where clause when --auto-embed is passed
452
+ if (vectorFieldNames.length > 0) {
453
+ tryBody.push(buildAutoEmbedBlock(t.identifier('searchWhere'), vectorFieldNames, false));
454
+ }
295
455
  // const defaultSelect = { ... };
296
456
  tryBody.push(t.variableDeclaration('const', [
297
457
  t.variableDeclarator(t.identifier('defaultSelect'), defaultSelectObj),
298
458
  ]));
299
- // const findManyArgs = parseFindManyArgs(argv, defaultSelect, searchWhere);
300
- tryBody.push(t.variableDeclaration('const', [
301
- t.variableDeclarator(t.identifier('findManyArgs'), t.callExpression(t.identifier('parseFindManyArgs'), [
459
+ // const findManyArgs = parseFindManyArgs<FindManyArgs<...>>(argv, defaultSelect, searchWhere);
460
+ {
461
+ const callExpr = t.callExpression(t.identifier('parseFindManyArgs'), [
302
462
  t.identifier('argv'),
303
463
  t.identifier('defaultSelect'),
304
464
  t.identifier('searchWhere'),
305
- ])),
306
- ]));
465
+ ]);
466
+ callExpr.typeParameters = t.tsTypeParameterInstantiation([
467
+ buildFindManyArgsType(table, conditionEnabled),
468
+ ]);
469
+ tryBody.push(t.variableDeclaration('const', [
470
+ t.variableDeclarator(t.identifier('findManyArgs'), callExpr),
471
+ ]));
472
+ }
307
473
  tryBody.push(buildGetClientStatement(targetName));
308
474
  // const result = await client.<singular>.findMany(findManyArgs).execute();
309
475
  tryBody.push(t.variableDeclaration('const', [
@@ -375,7 +541,7 @@ export function getFieldsWithDefaults(table, typeRegistry) {
375
541
  }
376
542
  return fieldsWithDefaults;
377
543
  }
378
- function buildMutationHandler(table, operation, targetName, typeRegistry, ormTypes) {
544
+ function buildMutationHandler(table, operation, vectorFieldNames, targetName, typeRegistry, ormTypes) {
379
545
  const { singularName } = getTableNames(table);
380
546
  const pkFields = getPrimaryKeyInfo(table);
381
547
  const pk = pkFields[0];
@@ -503,6 +669,11 @@ function buildMutationHandler(table, operation, targetName, typeRegistry, ormTyp
503
669
  tryBody.push(t.variableDeclaration('const', [
504
670
  t.variableDeclarator(t.identifier('cleanedData'), cleanedDataExpr),
505
671
  ]));
672
+ // Inject --auto-embed block for create/update when table has vector fields.
673
+ // Converts text strings in vector fields to embeddings before the ORM call.
674
+ if (vectorFieldNames.length > 0) {
675
+ tryBody.push(buildAutoEmbedInputBlock(vectorFieldNames));
676
+ }
506
677
  }
507
678
  tryBody.push(buildGetClientStatement(targetName), t.variableDeclaration('const', [
508
679
  t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCall(singularName, operation, ormArgs))),
@@ -517,7 +688,7 @@ function buildMutationHandler(table, operation, targetName, typeRegistry, ormTyp
517
688
  ]), false, true);
518
689
  }
519
690
  export function generateTableCommand(table, options) {
520
- const { singularName } = getTableNames(table);
691
+ const { singularName, typeName } = getTableNames(table);
521
692
  const commandName = toKebabCase(singularName);
522
693
  const statements = [];
523
694
  const executorPath = options?.executorImportPath ?? '../executor';
@@ -542,7 +713,25 @@ export function generateTableCommand(table, options) {
542
713
  const inputTypesPath = options?.targetName
543
714
  ? `../../../orm/input-types`
544
715
  : `../../orm/input-types`;
545
- statements.push(createImportDeclaration(inputTypesPath, [createInputTypeName, patchTypeName], true));
716
+ // Import table-specific ORM types for generic type parameters on parseFindManyArgs/parseFindFirstArgs
717
+ const selectTypeName = `${typeName}Select`;
718
+ const whereTypeName = getFilterTypeName(table);
719
+ const orderByTypeName = getOrderByTypeName(table);
720
+ const conditionEnabled = options?.condition === true;
721
+ const conditionTypeName = conditionEnabled ? getConditionTypeName(table) : undefined;
722
+ statements.push(createImportDeclaration(inputTypesPath, [
723
+ createInputTypeName,
724
+ patchTypeName,
725
+ selectTypeName,
726
+ whereTypeName,
727
+ ...(conditionTypeName ? [conditionTypeName] : []),
728
+ orderByTypeName,
729
+ ], true));
730
+ // Import FindManyArgs/FindFirstArgs from select-types for proper generic typing
731
+ const selectTypesPath = options?.targetName
732
+ ? `../../../orm/select-types`
733
+ : `../../orm/select-types`;
734
+ statements.push(createImportDeclaration(selectTypesPath, ['FindManyArgs', 'FindFirstArgs'], true));
546
735
  // Generate field schema for type coercion
547
736
  // Use explicit FieldSchema type annotation so TS narrows string literals to FieldType
548
737
  const fieldSchemaId = t.identifier('fieldSchema');
@@ -563,6 +752,21 @@ export function generateTableCommand(table, options) {
563
752
  // Detect whether this table has search-capable fields (tsvector, BM25, trgm, vector embedding)
564
753
  const specialGroups = categorizeSpecialFields(table, options?.typeRegistry);
565
754
  const hasSearchFields = specialGroups.some((g) => g.category === 'search' || g.category === 'embedding');
755
+ // Collect vector embedding field names for --auto-embed support
756
+ const vectorFieldNames = [];
757
+ for (const group of specialGroups) {
758
+ if (group.category === 'embedding') {
759
+ for (const field of group.fields) {
760
+ vectorFieldNames.push(field.name);
761
+ }
762
+ }
763
+ }
764
+ const hasEmbeddings = vectorFieldNames.length > 0;
765
+ // Import embedder functions when table has vector embedding fields
766
+ if (hasEmbeddings) {
767
+ const embedderPath = options?.targetName ? '../../embedder' : '../embedder';
768
+ statements.push(createImportDeclaration(embedderPath, ['resolveEmbedder', 'autoEmbedWhere', 'autoEmbedInput']));
769
+ }
566
770
  const subcommands = ['list', 'find-first'];
567
771
  if (hasSearchFields)
568
772
  subcommands.push('search');
@@ -588,11 +792,21 @@ export function generateTableCommand(table, options) {
588
792
  usageLines.push(` create Create a new ${singularName}`);
589
793
  if (hasUpdate)
590
794
  usageLines.push(` update Update an existing ${singularName}`);
795
+ if (hasEmbeddings) {
796
+ usageLines.push('', 'Create/Update Options:', ' --auto-embed Convert text values in vector fields to embeddings before saving');
797
+ }
591
798
  if (hasDelete)
592
799
  usageLines.push(` delete Delete a ${singularName}`);
593
- usageLines.push('', 'List Options:', ' --limit <n> Max number of records to return (forward pagination)', ' --last <n> Number of records from the end (backward pagination)', ' --after <cursor> Cursor for forward pagination', ' --before <cursor> Cursor for backward pagination', ' --offset <n> Number of records to skip', ' --fields <fields> Comma-separated list of fields to return', ' --where.<field>.<op> Filter (dot-notation, e.g. --where.name.equalTo foo)', ' --condition.<f>.<op> Condition filter (dot-notation)', ' --orderBy <values> Comma-separated ordering values (e.g. NAME_ASC,CREATED_AT_DESC)', '', 'Find-First Options:', ' --fields <fields> Comma-separated list of fields to return', ' --where.<field>.<op> Filter (dot-notation, e.g. --where.status.equalTo active)', ' --condition.<f>.<op> Condition filter (dot-notation)', '');
800
+ usageLines.push('', 'List Options:', ' --limit <n> Max number of records to return (forward pagination)', ' --last <n> Number of records from the end (backward pagination)', ' --after <cursor> Cursor for forward pagination', ' --before <cursor> Cursor for backward pagination', ' --offset <n> Number of records to skip', ' --select <fields> Comma-separated list of fields to return', ' --where.<field>.<op> Filter (dot-notation, e.g. --where.name.equalTo foo)', ' --condition.<f>.<op> Condition filter (dot-notation)', ' --orderBy <values> Comma-separated ordering values (e.g. NAME_ASC,CREATED_AT_DESC)', '', 'Find-First Options:', ' --select <fields> Comma-separated list of fields to return', ' --where.<field>.<op> Filter (dot-notation, e.g. --where.status.equalTo active)', ' --condition.<f>.<op> Condition filter (dot-notation)', '');
594
801
  if (hasSearchFields) {
595
- usageLines.push('Search Options:', ' <query> Search query string (required)', ' --limit <n> Max number of records to return', ' --offset <n> Number of records to skip', ' --fields <fields> Comma-separated list of fields to return', ' --orderBy <values> Comma-separated list of ordering values', '');
802
+ usageLines.push('Search Options:', ' <query> Search query string (required)', ' --limit <n> Max number of records to return', ' --offset <n> Number of records to skip', ' --select <fields> Comma-separated list of fields to return', ' --orderBy <values> Comma-separated list of ordering values');
803
+ if (hasEmbeddings) {
804
+ usageLines.push(' --auto-embed Convert text queries to vectors via configured embedder');
805
+ }
806
+ usageLines.push('');
807
+ }
808
+ if (hasEmbeddings) {
809
+ usageLines.push('Embedding Options (for --auto-embed):', ' Set EMBEDDER_PROVIDER=ollama to enable text-to-vector embedding.', ' Optional: EMBEDDER_MODEL (default: nomic-embed-text)', ' Optional: EMBEDDER_BASE_URL (default: http://localhost:11434)', '');
596
810
  }
597
811
  usageLines.push(' --help, -h Show this help message', '');
598
812
  statements.push(t.variableDeclaration('const', [
@@ -655,17 +869,17 @@ export function generateTableCommand(table, options) {
655
869
  ]), false, true));
656
870
  const tn = options?.targetName;
657
871
  const ormTypes = { createInputTypeName, patchTypeName, innerFieldName };
658
- statements.push(buildListHandler(table, tn, options?.typeRegistry));
659
- statements.push(buildFindFirstHandler(table, tn, options?.typeRegistry));
872
+ statements.push(buildListHandler(table, vectorFieldNames, tn, options?.typeRegistry, conditionEnabled));
873
+ statements.push(buildFindFirstHandler(table, tn, options?.typeRegistry, conditionEnabled));
660
874
  if (hasSearchFields)
661
- statements.push(buildSearchHandler(table, specialGroups, tn, options?.typeRegistry));
875
+ statements.push(buildSearchHandler(table, specialGroups, vectorFieldNames, tn, options?.typeRegistry, conditionEnabled));
662
876
  if (hasGet)
663
877
  statements.push(buildGetHandler(table, tn, options?.typeRegistry));
664
- statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry, ormTypes));
878
+ statements.push(buildMutationHandler(table, 'create', vectorFieldNames, tn, options?.typeRegistry, ormTypes));
665
879
  if (hasUpdate)
666
- statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry, ormTypes));
880
+ statements.push(buildMutationHandler(table, 'update', vectorFieldNames, tn, options?.typeRegistry, ormTypes));
667
881
  if (hasDelete)
668
- statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry, ormTypes));
882
+ statements.push(buildMutationHandler(table, 'delete', vectorFieldNames, tn, options?.typeRegistry, ormTypes));
669
883
  const header = getGeneratedFileHeader(`CLI commands for ${table.name}`);
670
884
  const code = generateCode(statements);
671
885
  return {
@@ -25,3 +25,11 @@ export declare function generateNodeFetchFile(): GeneratedFile;
25
25
  * provide their own entry point with custom configuration.
26
26
  */
27
27
  export declare function generateEntryPointFile(): GeneratedFile;
28
+ /**
29
+ * Generate an embedder.ts file with pluggable text-to-vector embedding.
30
+ *
31
+ * Provides a runtime embedder registry using @agentic-kit/ollama so that
32
+ * CLI search and list commands can convert text queries into vector arrays
33
+ * for pgvector similarity search when --auto-embed is passed.
34
+ */
35
+ export declare function generateEmbedderFile(): GeneratedFile;
@@ -67,3 +67,16 @@ export function generateEntryPointFile() {
67
67
  content: readTemplateFile('cli-entry.ts', 'CLI entry point'),
68
68
  };
69
69
  }
70
+ /**
71
+ * Generate an embedder.ts file with pluggable text-to-vector embedding.
72
+ *
73
+ * Provides a runtime embedder registry using @agentic-kit/ollama so that
74
+ * CLI search and list commands can convert text queries into vector arrays
75
+ * for pgvector similarity search when --auto-embed is passed.
76
+ */
77
+ export function generateEmbedderFile() {
78
+ return {
79
+ fileName: 'embedder.ts',
80
+ content: readTemplateFile('embedder.ts', 'CLI embedder — pluggable text-to-vector embedding for search commands'),
81
+ };
82
+ }
@@ -65,6 +65,23 @@ export declare function buildSpecialFieldsMarkdown(groups: SpecialFieldGroup[]):
65
65
  * Returns empty array when there are no special fields.
66
66
  */
67
67
  export declare function buildSpecialFieldsPlain(groups: SpecialFieldGroup[]): string[];
68
+ export interface SearchExample {
69
+ description: string;
70
+ code: string[];
71
+ }
72
+ /**
73
+ * Build concrete, field-specific CLI examples for tables with search fields.
74
+ * Uses the same field-name derivation logic as buildSearchHandler in
75
+ * table-command-generator.ts so the examples match the actual generated code.
76
+ *
77
+ * Returns an empty array when the table has no search/embedding fields.
78
+ */
79
+ export declare function buildSearchExamples(specialGroups: SpecialFieldGroup[], toolName: string, cmd: string): SearchExample[];
80
+ /**
81
+ * Build markdown lines for search-specific examples in README-style docs.
82
+ * Returns empty array when there are no search examples.
83
+ */
84
+ export declare function buildSearchExamplesMarkdown(specialGroups: SpecialFieldGroup[], toolName: string, cmd: string): string[];
68
85
  /**
69
86
  * Represents a flattened argument for docs/skills generation.
70
87
  * INPUT_OBJECT args are expanded to dot-notation fields.
@@ -234,6 +234,131 @@ export function buildSpecialFieldsPlain(groups) {
234
234
  }
235
235
  return lines;
236
236
  }
237
+ /**
238
+ * Build concrete, field-specific CLI examples for tables with search fields.
239
+ * Uses the same field-name derivation logic as buildSearchHandler in
240
+ * table-command-generator.ts so the examples match the actual generated code.
241
+ *
242
+ * Returns an empty array when the table has no search/embedding fields.
243
+ */
244
+ export function buildSearchExamples(specialGroups, toolName, cmd) {
245
+ const examples = [];
246
+ const scoreFields = [];
247
+ for (const group of specialGroups) {
248
+ for (const field of group.fields) {
249
+ // tsvector (FullText scalar) — where input uses the column name directly
250
+ if (field.type.gqlType === 'FullText' && !field.type.isArray) {
251
+ examples.push({
252
+ description: `Full-text search via tsvector (\`${field.name}\`)`,
253
+ code: [
254
+ `${toolName} ${cmd} list --where.${field.name} "search query" --select title,tsvRank`,
255
+ ],
256
+ });
257
+ scoreFields.push('tsvRank');
258
+ }
259
+ // BM25 computed score — bodyBm25Score → bm25Body
260
+ if (/Bm25Score$/.test(field.name)) {
261
+ const baseName = field.name.replace(/Bm25Score$/, '');
262
+ const inputName = `bm25${baseName.charAt(0).toUpperCase()}${baseName.slice(1)}`;
263
+ examples.push({
264
+ description: `BM25 keyword search via \`${inputName}\``,
265
+ code: [
266
+ `${toolName} ${cmd} list --where.${inputName}.query "search query" --select title,${field.name}`,
267
+ ],
268
+ });
269
+ scoreFields.push(field.name);
270
+ }
271
+ // Trigram similarity — titleTrgmSimilarity → trgmTitle
272
+ if (/TrgmSimilarity$/.test(field.name)) {
273
+ const baseName = field.name.replace(/TrgmSimilarity$/, '');
274
+ const inputName = `trgm${baseName.charAt(0).toUpperCase()}${baseName.slice(1)}`;
275
+ examples.push({
276
+ description: `Fuzzy search via trigram similarity (\`${inputName}\`)`,
277
+ code: [
278
+ `${toolName} ${cmd} list --where.${inputName}.value "approximate query" --where.${inputName}.threshold 0.3 --select title,${field.name}`,
279
+ ],
280
+ });
281
+ scoreFields.push(field.name);
282
+ }
283
+ // pgvector embedding — uses column name, with --auto-embed for text-to-vector
284
+ if (group.category === 'embedding') {
285
+ examples.push({
286
+ description: `Vector similarity search via \`${field.name}\` (manual vector)`,
287
+ code: [
288
+ `# Pass a pre-computed vector array via dot-notation`,
289
+ `${toolName} ${cmd} list --where.${field.name}.vector '[0.1,0.2,0.3]' --where.${field.name}.distance 1.0 --select title,${field.name}VectorDistance`,
290
+ ],
291
+ });
292
+ examples.push({
293
+ description: `Vector semantic search via \`${field.name}\` with --auto-embed`,
294
+ code: [
295
+ `# --auto-embed converts text to vectors using the configured embedder (e.g. Ollama nomic-embed-text)`,
296
+ `EMBEDDER_PROVIDER=ollama ${toolName} ${cmd} search "semantic query" --auto-embed --select title,${field.name}VectorDistance`,
297
+ `EMBEDDER_PROVIDER=ollama ${toolName} ${cmd} list --where.${field.name}.vector "semantic query" --auto-embed --select title,${field.name}VectorDistance`,
298
+ ],
299
+ });
300
+ examples.push({
301
+ description: `Create/update with auto-embedded \`${field.name}\` via --auto-embed`,
302
+ code: [
303
+ `# --auto-embed on create/update converts text strings in vector fields to embeddings before saving`,
304
+ `EMBEDDER_PROVIDER=ollama ${toolName} ${cmd} create --${field.name} "text to embed" --auto-embed`,
305
+ `EMBEDDER_PROVIDER=ollama ${toolName} ${cmd} update --${field.name} "new text to embed" --auto-embed`,
306
+ ],
307
+ });
308
+ }
309
+ // searchScore — composite blend field, useful for ordering
310
+ if (field.name === 'searchScore') {
311
+ scoreFields.push('searchScore');
312
+ }
313
+ }
314
+ }
315
+ // Composite fullTextSearch example (dispatches to all text adapters)
316
+ const hasTextSearch = specialGroups.some((g) => g.category === 'search' && g.fields.some((f) => f.type.gqlType === 'FullText' || /TrgmSimilarity$/.test(f.name) || /Bm25Score$/.test(f.name)));
317
+ if (hasTextSearch) {
318
+ const fieldsArg = scoreFields.length > 0
319
+ ? `title,${[...new Set(scoreFields)].join(',')}`
320
+ : 'title';
321
+ examples.push({
322
+ description: 'Composite search (fullTextSearch dispatches to all text adapters)',
323
+ code: [
324
+ `${toolName} ${cmd} list --where.fullTextSearch "search query" --select ${fieldsArg}`,
325
+ ],
326
+ });
327
+ }
328
+ // Combined search + pagination + ordering example
329
+ if (examples.length > 0) {
330
+ examples.push({
331
+ description: 'Search with pagination and field projection',
332
+ code: [
333
+ `${toolName} ${cmd} list --where.fullTextSearch "query" --limit 10 --select id,title,searchScore`,
334
+ `${toolName} ${cmd} search "query" --limit 10 --select id,title,searchScore`,
335
+ ],
336
+ });
337
+ }
338
+ return examples;
339
+ }
340
+ /**
341
+ * Build markdown lines for search-specific examples in README-style docs.
342
+ * Returns empty array when there are no search examples.
343
+ */
344
+ export function buildSearchExamplesMarkdown(specialGroups, toolName, cmd) {
345
+ const examples = buildSearchExamples(specialGroups, toolName, cmd);
346
+ if (examples.length === 0)
347
+ return [];
348
+ const lines = [];
349
+ lines.push('**Search Examples:**');
350
+ lines.push('');
351
+ for (const ex of examples) {
352
+ lines.push(`*${ex.description}:*`);
353
+ lines.push('```bash');
354
+ for (const c of ex.code) {
355
+ lines.push(c);
356
+ }
357
+ lines.push('```');
358
+ lines.push('');
359
+ }
360
+ return lines;
361
+ }
237
362
  function unwrapNonNull(typeRef) {
238
363
  if (typeRef.kind === 'NON_NULL' && typeRef.ofType) {
239
364
  return { inner: typeRef.ofType, required: true };
@@ -1,4 +1,4 @@
1
- import { toKebabCase } from 'komoji';
1
+ import { toKebabCase } from 'inflekt';
2
2
  import { buildSkillFile, buildSkillReference, formatArgType, fieldPlaceholder, pkPlaceholder, argPlaceholder, getReadmeHeader, getReadmeFooter, } from './docs-utils';
3
3
  import { getTableNames, getScalarFields, getPrimaryKeyInfo, getListQueryHookName, getSingleQueryHookName, getCreateMutationHookName, getUpdateMutationHookName, getDeleteMutationHookName, hasValidPrimaryKey, ucFirst, } from './utils';
4
4
  function getCustomHookName(op) {
@@ -92,7 +92,7 @@ export function generate(options) {
92
92
  hasInvalidation = true;
93
93
  }
94
94
  // Condition types (PostGraphile simple equality filters)
95
- const conditionEnabled = config.codegen?.condition !== false;
95
+ const conditionEnabled = config.codegen?.condition === true;
96
96
  // 4. Generate table-based query hooks (queries/*.ts)
97
97
  const queryHooks = generateAllQueryHooks(tables, {
98
98
  reactQueryEnabled,
@@ -1,4 +1,4 @@
1
- import { toKebabCase } from 'komoji';
1
+ import { toKebabCase } from 'inflekt';
2
2
  import { buildSkillFile, buildSkillReference, formatArgType, fieldPlaceholder, pkPlaceholder, argPlaceholder, getEditableFields, categorizeSpecialFields, buildSpecialFieldsMarkdown, getReadmeHeader, getReadmeFooter, } from '../docs-utils';
3
3
  import { getScalarFields, getTableNames, getPrimaryKeyInfo, lcFirst, } from '../utils';
4
4
  export function generateOrmReadme(tables, customOperations, registry) {
@@ -9,7 +9,7 @@ import { generateAllModelFiles } from './model-generator';
9
9
  export function generateOrm(options) {
10
10
  const { tables, customOperations, sharedTypesPath } = options;
11
11
  const commentsEnabled = options.config.codegen?.comments !== false;
12
- const conditionEnabled = options.config.codegen?.condition !== false;
12
+ const conditionEnabled = options.config.codegen?.condition === true;
13
13
  const files = [];
14
14
  // Use shared types when a sharedTypesPath is provided (unified output mode)
15
15
  const useSharedTypes = !!sharedTypesPath;
@@ -1382,7 +1382,7 @@ function collectConditionExtraInputTypes(tables, typeRegistry) {
1382
1382
  * Generate comprehensive input-types.ts file using Babel AST
1383
1383
  */
1384
1384
  export function generateInputTypesFile(typeRegistry, usedInputTypes, tables, usedPayloadTypes, comments = true, options) {
1385
- const conditionEnabled = options?.condition !== false;
1385
+ const conditionEnabled = options?.condition === true;
1386
1386
  const statements = [];
1387
1387
  const tablesList = tables ?? [];
1388
1388
  const hasTables = tablesList.length > 0;