@danielfgray/pg-sourcerer 0.1.8 → 0.1.10

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 (66) hide show
  1. package/dist/cli.js +30 -105
  2. package/dist/cli.js.map +1 -1
  3. package/dist/generate.d.ts.map +1 -1
  4. package/dist/generate.js +4 -8
  5. package/dist/generate.js.map +1 -1
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/init.d.ts +4 -1
  11. package/dist/init.d.ts.map +1 -1
  12. package/dist/init.js +207 -86
  13. package/dist/init.js.map +1 -1
  14. package/dist/lib/conjure.d.ts +15 -2
  15. package/dist/lib/conjure.d.ts.map +1 -1
  16. package/dist/lib/conjure.js +44 -3
  17. package/dist/lib/conjure.js.map +1 -1
  18. package/dist/lib/field-utils.js +5 -8
  19. package/dist/lib/field-utils.js.map +1 -1
  20. package/dist/plugins/arktype.d.ts +7 -0
  21. package/dist/plugins/arktype.d.ts.map +1 -1
  22. package/dist/plugins/arktype.js +219 -40
  23. package/dist/plugins/arktype.js.map +1 -1
  24. package/dist/plugins/effect-model.d.ts +8 -1
  25. package/dist/plugins/effect-model.d.ts.map +1 -1
  26. package/dist/plugins/effect-model.js +163 -15
  27. package/dist/plugins/effect-model.js.map +1 -1
  28. package/dist/plugins/kysely-queries.d.ts +60 -1
  29. package/dist/plugins/kysely-queries.d.ts.map +1 -1
  30. package/dist/plugins/kysely-queries.js +703 -123
  31. package/dist/plugins/kysely-queries.js.map +1 -1
  32. package/dist/plugins/kysely-types.d.ts +35 -0
  33. package/dist/plugins/kysely-types.d.ts.map +1 -0
  34. package/dist/plugins/kysely-types.js +601 -0
  35. package/dist/plugins/kysely-types.js.map +1 -0
  36. package/dist/plugins/sql-queries.d.ts +3 -1
  37. package/dist/plugins/sql-queries.d.ts.map +1 -1
  38. package/dist/plugins/sql-queries.js +82 -20
  39. package/dist/plugins/sql-queries.js.map +1 -1
  40. package/dist/plugins/types.d.ts +2 -0
  41. package/dist/plugins/types.d.ts.map +1 -1
  42. package/dist/plugins/types.js +1 -1
  43. package/dist/plugins/types.js.map +1 -1
  44. package/dist/plugins/zod.d.ts +7 -0
  45. package/dist/plugins/zod.d.ts.map +1 -1
  46. package/dist/plugins/zod.js +169 -13
  47. package/dist/plugins/zod.js.map +1 -1
  48. package/dist/services/artifact-store.d.ts.map +1 -1
  49. package/dist/services/artifact-store.js +2 -5
  50. package/dist/services/artifact-store.js.map +1 -1
  51. package/dist/services/emissions.d.ts.map +1 -1
  52. package/dist/services/emissions.js +31 -23
  53. package/dist/services/emissions.js.map +1 -1
  54. package/dist/services/file-writer.d.ts +0 -4
  55. package/dist/services/file-writer.d.ts.map +1 -1
  56. package/dist/services/file-writer.js +3 -12
  57. package/dist/services/file-writer.js.map +1 -1
  58. package/dist/services/inflection.d.ts +51 -30
  59. package/dist/services/inflection.d.ts.map +1 -1
  60. package/dist/services/inflection.js +86 -93
  61. package/dist/services/inflection.js.map +1 -1
  62. package/dist/services/plugin.d.ts +10 -10
  63. package/dist/services/plugin.d.ts.map +1 -1
  64. package/dist/services/plugin.js +16 -3
  65. package/dist/services/plugin.js.map +1 -1
  66. package/package.json +3 -2
@@ -6,39 +6,71 @@
6
6
  */
7
7
  import { Schema as S } from "effect";
8
8
  import { definePlugin } from "../services/plugin.js";
9
- import { getTableEntities, getEnumEntities } from "../ir/semantic-ir.js";
9
+ import { getTableEntities, getEnumEntities, getFunctionEntities, getCompositeEntities } from "../ir/semantic-ir.js";
10
10
  import { conjure, cast } from "../lib/conjure.js";
11
11
  import { resolveFieldType, tsTypeToAst } from "../lib/field-utils.js";
12
12
  import { inflect } from "../services/inflection.js";
13
13
  const { ts, b } = conjure;
14
14
  const { toExpr } = cast;
15
- // ============================================================================
16
- // Configuration
17
- // ============================================================================
18
- const KyselyQueriesPluginConfig = S.Struct({
19
- outputDir: S.String,
20
- header: S.optional(S.String),
15
+ /** Default export name: entityName + methodName (e.g., "UserFindById") */
16
+ const defaultExportName = (entityName, methodName) => `${entityName}${methodName}`;
17
+ /** Default function export name: camelCase of pg function name */
18
+ const defaultFunctionExportName = (pgFunctionName) => inflect.camelCase(pgFunctionName);
19
+ /**
20
+ * Schema for serializable config options (JSON/YAML compatible).
21
+ * Function options are typed separately in KyselyQueriesConfigInput.
22
+ */
23
+ const KyselyQueriesPluginConfigSchema = S.Struct({
24
+ outputDir: S.optionalWith(S.String, { default: () => "kysely-queries" }),
21
25
  /**
22
26
  * Path to import DB type from (relative to outputDir).
23
- * Defaults to "../DB.js" which works with kysely-codegen's DB.d.ts output.
24
- * For node16/nodenext module resolution, use ".js" extension even for .d.ts files.
27
+ * Defaults to "../db.js" which works with kysely-types plugin output.
28
+ * For node16/nodenext module resolution, use ".js" extension even for .ts files.
25
29
  */
26
- dbTypesPath: S.optional(S.String),
30
+ dbTypesPath: S.optionalWith(S.String, { default: () => "../db.js" }),
27
31
  /**
28
32
  * Whether to call .execute() / .executeTakeFirst() on queries.
29
33
  * When true (default), methods return Promise<Row> or Promise<Row[]>.
30
34
  * When false, methods return the query builder for further customization.
31
35
  */
32
- executeQueries: S.optional(S.Boolean),
36
+ executeQueries: S.optionalWith(S.Boolean, { default: () => true }),
37
+ /**
38
+ * Whether to generate listMany() method for unfiltered table scans.
39
+ * Disabled by default since unfiltered scans don't use indexes.
40
+ * When enabled, generates: listMany(db, limit = 50, offset = 0)
41
+ */
42
+ generateListMany: S.optionalWith(S.Boolean, { default: () => false }),
43
+ /**
44
+ * Whether to generate function wrappers for stored functions.
45
+ * When true (default), generates queries/mutations namespaces in functions.ts.
46
+ */
47
+ generateFunctions: S.optionalWith(S.Boolean, { default: () => true }),
48
+ /**
49
+ * Output file name for function wrappers (relative to outputDir).
50
+ */
51
+ functionsFile: S.optionalWith(S.String, { default: () => "functions.ts" }),
52
+ /**
53
+ * Export name function (validated as Any, properly typed in KyselyQueriesConfigInput)
54
+ */
55
+ exportName: S.optional(S.Any),
56
+ /**
57
+ * Function export name function (validated as Any, properly typed in KyselyQueriesConfigInput)
58
+ */
59
+ functionExportName: S.optional(S.Any),
33
60
  });
34
61
  /**
35
62
  * Get the Kysely table interface name from the entity.
36
- * Converts schema.table to PascalCase: app_public.users -> AppPublicUsers
37
- * Uses the inflection utility to match kysely-codegen's naming convention.
63
+ * Uses entity.name which is already PascalCase from inflection (e.g., Users).
64
+ */
65
+ const getTableTypeName = (entity) => entity.name;
66
+ /**
67
+ * Get the table reference for Kysely queries.
68
+ * Uses schema-qualified name only if the schema is NOT in defaultSchemas.
69
+ * This matches the keys in the DB interface from kysely-types plugin.
38
70
  */
39
- const getTableTypeName = (entity) => `${inflect.pascalCase(entity.schemaName)}${inflect.pascalCase(entity.pgName)}`;
40
- /** Get the schema-qualified table name for Kysely */
41
- const getTableRef = (entity) => `${entity.schemaName}.${entity.pgName}`;
71
+ const getTableRef = (entity, defaultSchemas) => defaultSchemas.includes(entity.schemaName)
72
+ ? entity.pgName
73
+ : `${entity.schemaName}.${entity.pgName}`;
42
74
  /** Find a field in the row shape by column name */
43
75
  const findRowField = (entity, columnName) => entity.shapes.row.fields.find(f => f.columnName === columnName);
44
76
  /** Get the TypeScript type AST for a field */
@@ -49,6 +81,41 @@ const getFieldTypeAst = (field, ctx) => {
49
81
  return resolved.enumDef ? ts.ref(resolved.enumDef.name) : tsTypeToAst(resolved.tsType);
50
82
  };
51
83
  // ============================================================================
84
+ // FK Semantic Naming Helpers
85
+ // ============================================================================
86
+ /**
87
+ * Find a belongsTo relation that uses the given column as its local FK column.
88
+ * For single-column indexes only.
89
+ */
90
+ const findRelationForColumn = (entity, columnName) => entity.relations.find(r => r.kind === "belongsTo" && r.columns.length === 1 && r.columns[0]?.local === columnName);
91
+ /**
92
+ * Derive semantic name for an FK-based lookup.
93
+ * Priority: @fieldName tag → column minus _id suffix → target entity name
94
+ */
95
+ const deriveSemanticName = (relation, columnName) => {
96
+ // 1. Check for @fieldName smart tag
97
+ if (relation.tags.fieldName && typeof relation.tags.fieldName === "string") {
98
+ return relation.tags.fieldName;
99
+ }
100
+ // 2. Strip common FK suffixes from column name
101
+ const suffixes = ["_id", "_fk", "Id", "Fk"];
102
+ for (const suffix of suffixes) {
103
+ if (columnName.endsWith(suffix)) {
104
+ const stripped = columnName.slice(0, -suffix.length);
105
+ if (stripped.length > 0)
106
+ return stripped;
107
+ }
108
+ }
109
+ // 3. Fall back to target entity name (lowercased first char)
110
+ const target = relation.targetEntity;
111
+ return target.charAt(0).toLowerCase() + target.slice(1);
112
+ };
113
+ /**
114
+ * Convert to PascalCase for use in method names.
115
+ * Handles snake_case (created_at → CreatedAt) and regular strings.
116
+ */
117
+ const toPascalCase = (s) => inflect.pascalCase(s);
118
+ // ============================================================================
52
119
  // AST Building Helpers
53
120
  // ============================================================================
54
121
  /** Create identifier */
@@ -91,6 +158,15 @@ const deleteFrom = (tableRef) => call(id("db"), "deleteFrom", [str(tableRef)]);
91
158
  * Chain method call onto existing expression
92
159
  */
93
160
  const chain = (expr, method, args = []) => call(expr, method, args);
161
+ /**
162
+ * Create an exported const declaration: export const name = value
163
+ */
164
+ const exportConst = (name, value) => {
165
+ const constDecl = b.variableDeclaration("const", [
166
+ b.variableDeclarator(id(name), toExpr(value))
167
+ ]);
168
+ return b.exportNamedDeclaration(constDecl, []);
169
+ };
94
170
  /**
95
171
  * Build arrow function expression: (params) => body
96
172
  */
@@ -106,21 +182,398 @@ const objProp = (key, value) => {
106
182
  return prop;
107
183
  };
108
184
  // ============================================================================
185
+ // PostgreSQL Type Name to TypeScript Mapping
186
+ // ============================================================================
187
+ /**
188
+ * Map PostgreSQL type name to TypeScript type string.
189
+ * Used for function argument and return type resolution.
190
+ */
191
+ const pgTypeNameToTs = (typeName) => {
192
+ // Normalize: strip schema prefix if present
193
+ const baseName = typeName.includes(".") ? typeName.split(".").pop() : typeName;
194
+ switch (baseName) {
195
+ // Boolean
196
+ case "bool":
197
+ case "boolean":
198
+ return "boolean";
199
+ // Integer types → number
200
+ case "int2":
201
+ case "smallint":
202
+ case "int4":
203
+ case "integer":
204
+ case "int":
205
+ case "oid":
206
+ case "float4":
207
+ case "real":
208
+ case "float8":
209
+ case "double precision":
210
+ return "number";
211
+ // Big integers/numeric → string (to avoid precision loss)
212
+ case "int8":
213
+ case "bigint":
214
+ case "numeric":
215
+ case "decimal":
216
+ case "money":
217
+ return "string";
218
+ // Text types → string
219
+ case "text":
220
+ case "varchar":
221
+ case "character varying":
222
+ case "char":
223
+ case "character":
224
+ case "bpchar":
225
+ case "name":
226
+ case "xml":
227
+ case "bit":
228
+ case "varbit":
229
+ case "bit varying":
230
+ case "uuid":
231
+ case "inet":
232
+ case "cidr":
233
+ case "macaddr":
234
+ case "macaddr8":
235
+ case "time":
236
+ case "timetz":
237
+ case "time with time zone":
238
+ case "time without time zone":
239
+ case "interval":
240
+ return "string";
241
+ // Date/Time with date component → Date
242
+ case "date":
243
+ case "timestamp":
244
+ case "timestamptz":
245
+ case "timestamp with time zone":
246
+ case "timestamp without time zone":
247
+ return "Date";
248
+ // JSON → unknown
249
+ case "json":
250
+ case "jsonb":
251
+ case "jsonpath":
252
+ return "unknown";
253
+ // Binary → Buffer
254
+ case "bytea":
255
+ return "Buffer";
256
+ // Void
257
+ case "void":
258
+ return "void";
259
+ // Default to unknown
260
+ default:
261
+ return "unknown";
262
+ }
263
+ };
264
+ /**
265
+ * Check if a function argument type matches a table/view entity (row type argument).
266
+ * Functions with row-type arguments are computed fields (e.g., posts_short_body(posts))
267
+ * and should be excluded from function wrapper generation.
268
+ */
269
+ const hasRowTypeArg = (arg, ir) => {
270
+ const tableEntities = getTableEntities(ir);
271
+ // Check if arg.typeName matches a table entity's qualified name
272
+ // Format: "schema.tablename" or just "tablename" for public schema
273
+ return tableEntities.some(entity => {
274
+ const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
275
+ return arg.typeName === qualifiedName || arg.typeName === entity.pgName;
276
+ });
277
+ };
278
+ /**
279
+ * Check if a function should be included in generated wrappers.
280
+ *
281
+ * Includes functions that:
282
+ * - Have canExecute permission
283
+ * - Are not trigger functions
284
+ * - Are not from extensions
285
+ * - Are not @omit tagged
286
+ * - Don't have row-type arguments (computed fields)
287
+ */
288
+ const isGeneratableFunction = (fn, ir) => {
289
+ if (!fn.canExecute)
290
+ return false;
291
+ if (fn.returnTypeName === "trigger")
292
+ return false;
293
+ if (fn.isFromExtension)
294
+ return false;
295
+ if (fn.tags.omit === true)
296
+ return false;
297
+ // Check for row-type args (computed field pattern)
298
+ if (fn.args.some(arg => hasRowTypeArg(arg, ir)))
299
+ return false;
300
+ return true;
301
+ };
302
+ /**
303
+ * Categorize functions by volatility.
304
+ * Volatile functions go in mutations namespace, stable/immutable in queries.
305
+ */
306
+ const categorizeFunction = (fn) => fn.volatility === "volatile" ? "mutations" : "queries";
307
+ /**
308
+ * Get all generatable functions from the IR, categorized by volatility.
309
+ */
310
+ const getGeneratableFunctions = (ir) => {
311
+ const all = getFunctionEntities(ir).filter(fn => isGeneratableFunction(fn, ir));
312
+ return {
313
+ queries: all.filter(fn => categorizeFunction(fn) === "queries"),
314
+ mutations: all.filter(fn => categorizeFunction(fn) === "mutations"),
315
+ };
316
+ };
317
+ /**
318
+ * Resolve a function's return type to TypeScript type information.
319
+ */
320
+ const resolveReturnType = (fn, ir) => {
321
+ const returnTypeName = fn.returnTypeName;
322
+ const isArray = fn.returnsSet;
323
+ // 1. Check if it's a table return type
324
+ const tableEntities = getTableEntities(ir);
325
+ const tableMatch = tableEntities.find(entity => {
326
+ const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
327
+ return returnTypeName === qualifiedName || returnTypeName === entity.pgName;
328
+ });
329
+ if (tableMatch) {
330
+ return {
331
+ tsType: tableMatch.name,
332
+ isArray,
333
+ isScalar: false,
334
+ needsImport: tableMatch.name,
335
+ returnEntity: tableMatch,
336
+ };
337
+ }
338
+ // 2. Check if it's a composite type return
339
+ const compositeEntities = getCompositeEntities(ir);
340
+ const compositeMatch = compositeEntities.find(entity => {
341
+ const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
342
+ return returnTypeName === qualifiedName || returnTypeName === entity.pgName;
343
+ });
344
+ if (compositeMatch) {
345
+ return {
346
+ tsType: compositeMatch.name,
347
+ isArray,
348
+ isScalar: false,
349
+ needsImport: compositeMatch.name,
350
+ returnEntity: compositeMatch,
351
+ };
352
+ }
353
+ // 3. It's a scalar type - map via type name
354
+ // Handle "schema.typename" format by extracting just the type name
355
+ const baseTypeName = returnTypeName.includes(".")
356
+ ? returnTypeName.split(".").pop()
357
+ : returnTypeName;
358
+ const tsType = pgTypeNameToTs(baseTypeName);
359
+ return {
360
+ tsType,
361
+ isArray,
362
+ isScalar: true,
363
+ };
364
+ };
365
+ /**
366
+ * Resolve a function argument to TypeScript type information.
367
+ */
368
+ const resolveArg = (arg, ir) => {
369
+ const typeName = arg.typeName;
370
+ // Check if it's an array type (ends with [])
371
+ const isArrayType = typeName.endsWith("[]");
372
+ const baseTypeName = isArrayType ? typeName.slice(0, -2) : typeName;
373
+ // Check enums
374
+ const enums = getEnumEntities(ir);
375
+ const enumMatch = enums.find(e => {
376
+ const qualifiedName = `${e.schemaName}.${e.pgName}`;
377
+ return baseTypeName === qualifiedName || baseTypeName === e.pgName;
378
+ });
379
+ if (enumMatch) {
380
+ const tsType = isArrayType ? `${enumMatch.name}[]` : enumMatch.name;
381
+ return {
382
+ name: arg.name || "arg",
383
+ tsType,
384
+ isOptional: arg.hasDefault,
385
+ needsImport: enumMatch.name,
386
+ };
387
+ }
388
+ // Check composites
389
+ const composites = getCompositeEntities(ir);
390
+ const compositeMatch = composites.find(e => {
391
+ const qualifiedName = `${e.schemaName}.${e.pgName}`;
392
+ return baseTypeName === qualifiedName || baseTypeName === e.pgName;
393
+ });
394
+ if (compositeMatch) {
395
+ const tsType = isArrayType ? `${compositeMatch.name}[]` : compositeMatch.name;
396
+ return {
397
+ name: arg.name || "arg",
398
+ tsType,
399
+ isOptional: arg.hasDefault,
400
+ needsImport: compositeMatch.name,
401
+ };
402
+ }
403
+ // Scalar type - map via type name
404
+ // Handle "schema.typename" format
405
+ const scalarBase = baseTypeName.includes(".")
406
+ ? baseTypeName.split(".").pop()
407
+ : baseTypeName;
408
+ const scalarTs = pgTypeNameToTs(scalarBase);
409
+ const tsType = isArrayType ? `${scalarTs}[]` : scalarTs;
410
+ return {
411
+ name: arg.name || "arg",
412
+ tsType,
413
+ isOptional: arg.hasDefault,
414
+ };
415
+ };
416
+ /**
417
+ * Resolve all arguments for a function.
418
+ */
419
+ const resolveArgs = (fn, ir) => fn.args.map(arg => resolveArg(arg, ir));
420
+ // ============================================================================
421
+ // Function Wrapper AST Generation
422
+ // ============================================================================
423
+ /**
424
+ * Generate a typed parameter with explicit type annotation from type string.
425
+ */
426
+ const typedParamFromString = (name, typeStr) => {
427
+ const param = id(name);
428
+ // Map type string to AST
429
+ let typeAst;
430
+ switch (typeStr) {
431
+ case "string":
432
+ typeAst = ts.string();
433
+ break;
434
+ case "number":
435
+ typeAst = ts.number();
436
+ break;
437
+ case "boolean":
438
+ typeAst = ts.boolean();
439
+ break;
440
+ case "Date":
441
+ typeAst = ts.ref("Date");
442
+ break;
443
+ case "Buffer":
444
+ typeAst = ts.ref("Buffer");
445
+ break;
446
+ case "unknown":
447
+ typeAst = ts.unknown();
448
+ break;
449
+ case "void":
450
+ typeAst = ts.void();
451
+ break;
452
+ default:
453
+ // Handle array types like "string[]"
454
+ if (typeStr.endsWith("[]")) {
455
+ const elemType = typeStr.slice(0, -2);
456
+ const elemAst = elemType === "string" ? ts.string()
457
+ : elemType === "number" ? ts.number()
458
+ : elemType === "boolean" ? ts.boolean()
459
+ : ts.ref(elemType);
460
+ typeAst = ts.array(elemAst);
461
+ }
462
+ else {
463
+ // Assume it's a type reference (composite, enum, etc.)
464
+ typeAst = ts.ref(typeStr);
465
+ }
466
+ }
467
+ param.typeAnnotation = b.tsTypeAnnotation(cast.toTSType(typeAst));
468
+ return param;
469
+ };
470
+ /**
471
+ * Generate an optional typed parameter with explicit type annotation.
472
+ */
473
+ const optionalTypedParamFromString = (name, typeStr) => {
474
+ const param = typedParamFromString(name, typeStr);
475
+ param.optional = true;
476
+ return param;
477
+ };
478
+ /**
479
+ * Get the fully qualified function name for use in eb.fn call.
480
+ */
481
+ const getFunctionQualifiedName = (fn) => `${fn.schemaName}.${fn.pgName}`;
482
+ /**
483
+ * Generate a function wrapper method as an object property.
484
+ *
485
+ * Patterns:
486
+ * - SETOF/table return: db.selectFrom(eb => eb.fn<Type>(...).as('f')).selectAll().execute()
487
+ * - Single row return: db.selectFrom(eb => eb.fn<Type>(...).as('f')).selectAll().executeTakeFirst()
488
+ * - Scalar return: db.selectNoFrom(eb => eb.fn<Type>(...).as('result')).executeTakeFirst().then(r => r?.result)
489
+ */
490
+ const generateFunctionWrapper = (fn, ir, executeQueries, functionExportName) => {
491
+ const resolvedReturn = resolveReturnType(fn, ir);
492
+ const resolvedArgs = resolveArgs(fn, ir);
493
+ const qualifiedName = getFunctionQualifiedName(fn);
494
+ // Build eb.val(arg) for each argument
495
+ const fnArgs = resolvedArgs.map(arg => call(id("eb"), "val", [id(arg.name)]));
496
+ // Build eb.fn<Type>('schema.fn_name', [args]).as('alias')
497
+ // The type parameter is the return type
498
+ const returnTypeAst = resolvedReturn.isScalar
499
+ ? typedParamFromString("_", resolvedReturn.tsType).typeAnnotation.typeAnnotation
500
+ : ts.ref(resolvedReturn.tsType);
501
+ // Create eb.fn with type parameter: eb.fn<Type>
502
+ const fnMember = b.memberExpression(id("eb"), id("fn"));
503
+ const fnWithType = b.tsInstantiationExpression(fnMember, b.tsTypeParameterInstantiation([cast.toTSType(returnTypeAst)]));
504
+ // Call it: eb.fn<Type>(name, args)
505
+ const fnCallBase = b.callExpression(fnWithType, [str(qualifiedName), b.arrayExpression(fnArgs.map(toExpr))]);
506
+ // .as('f') or .as('result') for scalar
507
+ const alias = resolvedReturn.isScalar ? "result" : "f";
508
+ const fnCallWithAlias = call(fnCallBase, "as", [str(alias)]);
509
+ // Arrow function for selectFrom callback: eb => eb.fn<...>(...).as('f')
510
+ const selectCallback = arrowFn([id("eb")], fnCallWithAlias);
511
+ // Build the query chain
512
+ let query;
513
+ if (resolvedReturn.isScalar) {
514
+ // Scalar: db.selectNoFrom(eb => ...).executeTakeFirst()
515
+ // Returns { result: T } | undefined - caller accesses .result
516
+ query = call(id("db"), "selectNoFrom", [selectCallback]);
517
+ if (executeQueries) {
518
+ query = chain(query, "executeTakeFirst");
519
+ }
520
+ }
521
+ else {
522
+ // Table/composite: db.selectFrom(eb => ...).selectAll()
523
+ query = chain(call(id("db"), "selectFrom", [selectCallback]), "selectAll");
524
+ if (executeQueries) {
525
+ // SETOF → .execute(), single row → .executeTakeFirst()
526
+ query = chain(query, resolvedReturn.isArray ? "execute" : "executeTakeFirst");
527
+ }
528
+ }
529
+ // Build the parameters: (db: Kysely<DB>, arg1: Type1, arg2?: Type2, ...)
530
+ const params = [
531
+ typedParam("db", ts.ref("Kysely", [ts.ref("DB")])),
532
+ ...resolvedArgs.map(arg => arg.isOptional
533
+ ? optionalTypedParamFromString(arg.name, arg.tsType)
534
+ : typedParamFromString(arg.name, arg.tsType))
535
+ ];
536
+ const wrapperFn = arrowFn(params, query);
537
+ const exportName = functionExportName(fn.pgName);
538
+ const constDecl = b.variableDeclaration("const", [
539
+ b.variableDeclarator(id(exportName), wrapperFn)
540
+ ]);
541
+ return b.exportNamedDeclaration(constDecl, []);
542
+ };
543
+ /**
544
+ * Collect all type imports needed for function wrappers.
545
+ */
546
+ const collectFunctionTypeImports = (functions, ir) => {
547
+ const imports = new Set();
548
+ for (const fn of functions) {
549
+ const resolvedReturn = resolveReturnType(fn, ir);
550
+ if (resolvedReturn.needsImport) {
551
+ imports.add(resolvedReturn.needsImport);
552
+ }
553
+ for (const arg of resolveArgs(fn, ir)) {
554
+ if (arg.needsImport) {
555
+ imports.add(arg.needsImport);
556
+ }
557
+ }
558
+ }
559
+ return imports;
560
+ };
561
+ // ============================================================================
109
562
  // CRUD Method Generators
110
563
  // ============================================================================
111
564
  /**
112
565
  * Generate findById method:
113
- * findById: (db, id) => db.selectFrom('table').selectAll().where('id', '=', id).executeTakeFirst()
566
+ * export const UserFindById = (db, id) => db.selectFrom('table').selectAll().where('id', '=', id).executeTakeFirst()
114
567
  */
115
568
  const generateFindById = (ctx) => {
116
- const { entity, executeQueries } = ctx;
569
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
117
570
  if (!entity.primaryKey || !entity.permissions.canSelect)
118
571
  return undefined;
119
572
  const pkColName = entity.primaryKey.columns[0];
120
573
  const pkField = findRowField(entity, pkColName);
121
574
  if (!pkField)
122
575
  return undefined;
123
- const tableRef = getTableRef(entity);
576
+ const tableRef = getTableRef(entity, defaultSchemas);
124
577
  const fieldName = pkField.name;
125
578
  const fieldType = getFieldTypeAst(pkField, ctx);
126
579
  // db.selectFrom('table').selectAll().where('col', '=', id)
@@ -129,71 +582,49 @@ const generateFindById = (ctx) => {
129
582
  query = chain(query, "executeTakeFirst");
130
583
  }
131
584
  const fn = arrowFn([typedParam("db", ts.ref("Kysely", [ts.ref("DB")])), typedParam(fieldName, fieldType)], query);
132
- return objProp("findById", fn);
585
+ return exportConst(exportName(entityName, "FindById"), fn);
133
586
  };
587
+ /** Default limit for findMany queries */
588
+ const DEFAULT_LIMIT = 50;
589
+ /** Default offset for findMany queries */
590
+ const DEFAULT_OFFSET = 0;
591
+ /**
592
+ * Create a parameter with a default value: name = defaultValue
593
+ * Type is inferred from the default value, no explicit annotation.
594
+ */
595
+ const paramWithDefault = (name, defaultValue) => b.assignmentPattern(id(name), toExpr(defaultValue));
134
596
  /**
135
- * Generate findMany method with optional pagination:
136
- * findMany: (db, opts?) => db.selectFrom('table').selectAll()
137
- * .$if(opts?.limit != null, q => q.limit(opts!.limit!))
138
- * .$if(opts?.offset != null, q => q.offset(opts!.offset!))
139
- * .execute()
597
+ * Generate listMany method with pagination defaults:
598
+ * export const UserListMany = (db, limit = 50, offset = 0) => db.selectFrom('table').selectAll()
599
+ * .limit(limit).offset(offset).execute()
140
600
  */
141
- const generateFindMany = (ctx) => {
142
- const { entity, executeQueries } = ctx;
601
+ const generateListMany = (ctx) => {
602
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
143
603
  if (!entity.permissions.canSelect)
144
604
  return undefined;
145
- const tableRef = getTableRef(entity);
146
- // Build the base query
147
- let query = chain(selectFrom(tableRef), "selectAll");
148
- // Add $if for limit
149
- // q => q.limit(opts!.limit!)
150
- const limitCallback = arrowFn([id("q")], call(id("q"), "limit", [
151
- member(b.tsNonNullExpression(member(id("opts"), "limit")),
152
- // This creates opts!.limit, we need to wrap the whole thing
153
- // Actually we want: opts!.limit!
154
- "")
155
- ]));
156
- // Simpler approach - just use opts?.limit directly with non-null assertion where needed
157
- const limitCheck = b.binaryExpression("!=", toExpr(member(b.optionalMemberExpression(id("opts"), id("limit"), false, true), "")), b.nullLiteral());
158
- // Actually let's simplify - use a cleaner pattern
159
- // $if(opts?.limit != null, q => q.limit(opts!.limit!))
160
- query = call(query, "$if", [
161
- b.binaryExpression("!=", toExpr(b.optionalMemberExpression(id("opts"), id("limit"), false, true)), b.nullLiteral()),
162
- arrowFn([id("q")], call(id("q"), "limit", [
163
- b.tsNonNullExpression(toExpr(member(b.tsNonNullExpression(toExpr(id("opts"))), "limit")))
164
- ]))
165
- ]);
166
- // Add $if for offset
167
- query = call(query, "$if", [
168
- b.binaryExpression("!=", toExpr(b.optionalMemberExpression(id("opts"), id("offset"), false, true)), b.nullLiteral()),
169
- arrowFn([id("q")], call(id("q"), "offset", [
170
- b.tsNonNullExpression(toExpr(member(b.tsNonNullExpression(toExpr(id("opts"))), "offset")))
171
- ]))
172
- ]);
605
+ const tableRef = getTableRef(entity, defaultSchemas);
606
+ // Build query: db.selectFrom('table').selectAll().limit(limit).offset(offset)
607
+ let query = chain(chain(chain(selectFrom(tableRef), "selectAll"), "limit", [id("limit")]), "offset", [id("offset")]);
173
608
  // Add .execute() if executeQueries is true
174
609
  if (executeQueries) {
175
610
  query = chain(query, "execute");
176
611
  }
177
- // Build opts parameter type: { limit?: number; offset?: number }
178
- const optsType = ts.objectType([
179
- { name: "limit", type: ts.number(), optional: true },
180
- { name: "offset", type: ts.number(), optional: true },
181
- ]);
182
612
  const fn = arrowFn([
183
613
  typedParam("db", ts.ref("Kysely", [ts.ref("DB")])),
184
- optionalTypedParam("opts", optsType),
614
+ paramWithDefault("limit", b.numericLiteral(DEFAULT_LIMIT)),
615
+ paramWithDefault("offset", b.numericLiteral(DEFAULT_OFFSET)),
185
616
  ], query);
186
- return objProp("findMany", fn);
617
+ return exportConst(exportName(entityName, "ListMany"), fn);
187
618
  };
188
619
  /**
189
620
  * Generate create method:
190
- * create: (db, data) => db.insertInto('table').values(data).returningAll().executeTakeFirstOrThrow()
621
+ * export const UserCreate = (db, data) => db.insertInto('table').values(data).returningAll().executeTakeFirstOrThrow()
191
622
  */
192
623
  const generateCreate = (ctx) => {
193
- const { entity, executeQueries } = ctx;
624
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
194
625
  if (!entity.permissions.canInsert)
195
626
  return undefined;
196
- const tableRef = getTableRef(entity);
627
+ const tableRef = getTableRef(entity, defaultSchemas);
197
628
  const tableTypeName = getTableTypeName(entity);
198
629
  // db.insertInto('table').values(data).returningAll()
199
630
  let query = chain(chain(insertInto(tableRef), "values", [id("data")]), "returningAll");
@@ -205,21 +636,21 @@ const generateCreate = (ctx) => {
205
636
  typedParam("db", ts.ref("Kysely", [ts.ref("DB")])),
206
637
  typedParam("data", ts.ref("Insertable", [ts.ref(tableTypeName)])),
207
638
  ], query);
208
- return objProp("create", fn);
639
+ return exportConst(exportName(entityName, "Create"), fn);
209
640
  };
210
641
  /**
211
642
  * Generate update method:
212
- * update: (db, id, data) => db.updateTable('table').set(data).where('id', '=', id).returningAll().executeTakeFirstOrThrow()
643
+ * export const UserUpdate = (db, id, data) => db.updateTable('table').set(data).where('id', '=', id).returningAll().executeTakeFirstOrThrow()
213
644
  */
214
645
  const generateUpdate = (ctx) => {
215
- const { entity, executeQueries } = ctx;
646
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
216
647
  if (!entity.primaryKey || !entity.permissions.canUpdate)
217
648
  return undefined;
218
649
  const pkColName = entity.primaryKey.columns[0];
219
650
  const pkField = findRowField(entity, pkColName);
220
651
  if (!pkField)
221
652
  return undefined;
222
- const tableRef = getTableRef(entity);
653
+ const tableRef = getTableRef(entity, defaultSchemas);
223
654
  const fieldName = pkField.name;
224
655
  const fieldType = getFieldTypeAst(pkField, ctx);
225
656
  const tableTypeName = getTableTypeName(entity);
@@ -234,21 +665,21 @@ const generateUpdate = (ctx) => {
234
665
  typedParam(fieldName, fieldType),
235
666
  typedParam("data", ts.ref("Updateable", [ts.ref(tableTypeName)])),
236
667
  ], query);
237
- return objProp("update", fn);
668
+ return exportConst(exportName(entityName, "Update"), fn);
238
669
  };
239
670
  /**
240
671
  * Generate delete method:
241
- * delete: (db, id) => db.deleteFrom('table').where('id', '=', id).execute()
672
+ * export const UserRemove = (db, id) => db.deleteFrom('table').where('id', '=', id).execute()
242
673
  */
243
674
  const generateDelete = (ctx) => {
244
- const { entity, executeQueries } = ctx;
675
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
245
676
  if (!entity.primaryKey || !entity.permissions.canDelete)
246
677
  return undefined;
247
678
  const pkColName = entity.primaryKey.columns[0];
248
679
  const pkField = findRowField(entity, pkColName);
249
680
  if (!pkField)
250
681
  return undefined;
251
- const tableRef = getTableRef(entity);
682
+ const tableRef = getTableRef(entity, defaultSchemas);
252
683
  const fieldName = pkField.name;
253
684
  const fieldType = getFieldTypeAst(pkField, ctx);
254
685
  // db.deleteFrom('table').where('id', '=', id)
@@ -257,13 +688,12 @@ const generateDelete = (ctx) => {
257
688
  query = chain(query, "execute");
258
689
  }
259
690
  const fn = arrowFn([typedParam("db", ts.ref("Kysely", [ts.ref("DB")])), typedParam(fieldName, fieldType)], query);
260
- // Use 'remove' since 'delete' is a reserved word
261
- return objProp("remove", fn);
691
+ return exportConst(exportName(entityName, "Remove"), fn);
262
692
  };
263
693
  /** Generate all CRUD methods for an entity */
264
694
  const generateCrudMethods = (ctx) => [
265
695
  generateFindById(ctx),
266
- generateFindMany(ctx),
696
+ ctx.generateListMany ? generateListMany(ctx) : undefined,
267
697
  generateCreate(ctx),
268
698
  generateUpdate(ctx),
269
699
  generateDelete(ctx),
@@ -277,42 +707,108 @@ const shouldGenerateLookup = (index) => !index.isPartial &&
277
707
  index.columns.length === 1 &&
278
708
  index.method !== "gin" &&
279
709
  index.method !== "gist";
280
- /** Generate a method name for an index-based lookup */
281
- const generateLookupName = (index) => {
282
- const colName = index.columns[0];
283
- const capitalizedCol = colName.charAt(0).toUpperCase() + colName.slice(1);
284
- return `findBy${capitalizedCol}`;
710
+ /**
711
+ * Generate the method name portion for an index-based lookup.
712
+ * Uses semantic naming when the column corresponds to an FK relation.
713
+ */
714
+ const generateLookupMethodName = (entity, index, relation) => {
715
+ const isUnique = isUniqueLookup(entity, index);
716
+ // Uses "FindOneBy" or "FindManyBy" suffix
717
+ const suffix = isUnique ? "FindOneBy" : "FindManyBy";
718
+ // Use semantic name if FK relation exists, otherwise fall back to column name
719
+ const columnName = index.columnNames[0];
720
+ const byName = relation
721
+ ? deriveSemanticName(relation, columnName)
722
+ : index.columns[0];
723
+ return `${suffix}${toPascalCase(byName)}`;
285
724
  };
286
- /** Generate a lookup method for a single-column index */
725
+ /**
726
+ * Generate a lookup method for a single-column index.
727
+ * Uses semantic parameter naming when the column corresponds to an FK relation.
728
+ */
287
729
  const generateLookupMethod = (index, ctx) => {
288
- const { entity, executeQueries } = ctx;
289
- const tableRef = getTableRef(entity);
730
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
731
+ const tableRef = getTableRef(entity, defaultSchemas);
290
732
  const columnName = index.columnNames[0];
291
733
  const field = findRowField(entity, columnName);
292
734
  const fieldName = field?.name ?? index.columns[0];
293
- const fieldType = getFieldTypeAst(field, ctx);
294
- const isUnique = index.isUnique || index.isPrimary;
735
+ const isUnique = isUniqueLookup(entity, index);
736
+ // Check if this index column corresponds to an FK relation
737
+ const relation = findRelationForColumn(entity, columnName);
738
+ // Use semantic param name if FK relation exists, otherwise use field name
739
+ const paramName = relation
740
+ ? deriveSemanticName(relation, columnName)
741
+ : fieldName;
742
+ // For FK columns, use indexed access on Selectable<TableType> to get the unwrapped type
743
+ // (Kysely's Generated<T> types need Selectable to unwrap for use in where clauses)
744
+ // For regular columns, use the field's type directly
745
+ const useSemanticNaming = relation !== undefined && paramName !== fieldName;
746
+ const tableTypeName = getTableTypeName(entity);
747
+ const paramType = useSemanticNaming
748
+ ? ts.indexedAccess(ts.ref("Selectable", [ts.ref(tableTypeName)]), ts.literal(fieldName))
749
+ : getFieldTypeAst(field, ctx);
295
750
  // db.selectFrom('table').selectAll().where('col', '=', value)
296
- let query = chain(chain(selectFrom(tableRef), "selectAll"), "where", [str(columnName), str("="), id(fieldName)]);
751
+ let query = chain(chain(selectFrom(tableRef), "selectAll"), "where", [str(columnName), str("="), id(paramName)]);
297
752
  if (executeQueries) {
298
753
  query = chain(query, isUnique ? "executeTakeFirst" : "execute");
299
754
  }
300
- const fn = arrowFn([typedParam("db", ts.ref("Kysely", [ts.ref("DB")])), typedParam(fieldName, fieldType)], query);
301
- return objProp(generateLookupName(index), fn);
755
+ const fn = arrowFn([typedParam("db", ts.ref("Kysely", [ts.ref("DB")])), typedParam(paramName, paramType)], query);
756
+ const methodName = generateLookupMethodName(entity, index, relation);
757
+ return exportConst(exportName(entityName, methodName), fn);
302
758
  };
303
- /** Generate lookup methods for all eligible indexes, deduplicating by name */
304
- const generateLookupMethods = (ctx) => {
305
- const seen = new Set();
306
- return ctx.entity.indexes
307
- .filter(index => shouldGenerateLookup(index) && !index.isPrimary && ctx.entity.permissions.canSelect)
308
- .filter(index => {
309
- const name = generateLookupName(index);
310
- if (seen.has(name))
759
+ /**
760
+ * Check if a column is covered by a unique constraint (not just unique index).
761
+ * This helps determine if a non-unique B-tree index on the column still
762
+ * returns at most one row.
763
+ */
764
+ const columnHasUniqueConstraint = (entity, columnName) => {
765
+ const constraints = entity.pgClass.getConstraints();
766
+ return constraints.some(c => {
767
+ // 'u' = unique constraint, 'p' = primary key
768
+ if (c.contype !== "u" && c.contype !== "p")
311
769
  return false;
312
- seen.add(name);
770
+ // Single-column constraint on our column?
771
+ const conkey = c.conkey ?? [];
772
+ if (conkey.length !== 1)
773
+ return false;
774
+ // Find the attribute with this attnum
775
+ const attrs = entity.pgClass.getAttributes();
776
+ const attr = attrs.find(a => a.attnum === conkey[0]);
777
+ return attr?.attname === columnName;
778
+ });
779
+ };
780
+ /**
781
+ * Determine if a lookup should be treated as unique (returns one row).
782
+ * True if: index is unique, index is primary, OR column has unique constraint.
783
+ */
784
+ const isUniqueLookup = (entity, index) => {
785
+ if (index.isUnique || index.isPrimary)
313
786
  return true;
314
- })
315
- .map(index => generateLookupMethod(index, ctx));
787
+ // Check if the single column has a unique constraint
788
+ const columnName = index.columnNames[0];
789
+ return columnName ? columnHasUniqueConstraint(entity, columnName) : false;
790
+ };
791
+ /** Generate lookup methods for all eligible indexes, deduplicating by column */
792
+ const generateLookupMethods = (ctx) => {
793
+ const eligibleIndexes = ctx.entity.indexes
794
+ .filter(index => shouldGenerateLookup(index) && !index.isPrimary && ctx.entity.permissions.canSelect);
795
+ // Group by column name, keeping only one index per column
796
+ // Prefer unique indexes, but also consider columns with unique constraints
797
+ const byColumn = new Map();
798
+ for (const index of eligibleIndexes) {
799
+ const columnName = index.columnNames[0];
800
+ const existing = byColumn.get(columnName);
801
+ if (!existing) {
802
+ byColumn.set(columnName, index);
803
+ }
804
+ else {
805
+ // Prefer explicitly unique index over non-unique
806
+ if (index.isUnique && !existing.isUnique) {
807
+ byColumn.set(columnName, index);
808
+ }
809
+ }
810
+ }
811
+ return Array.from(byColumn.values()).map(index => generateLookupMethod(index, ctx));
316
812
  };
317
813
  // ============================================================================
318
814
  // Plugin Definition
@@ -321,23 +817,50 @@ export const kyselyQueriesPlugin = definePlugin({
321
817
  name: "kysely-queries",
322
818
  provides: ["queries", "queries:kysely"],
323
819
  requires: [], // No dependency on types:kysely for now - uses external kysely-codegen types
324
- configSchema: KyselyQueriesPluginConfig,
820
+ configSchema: KyselyQueriesPluginConfigSchema,
325
821
  inflection: {
326
822
  outputFile: ctx => `${ctx.entityName}.ts`,
327
823
  symbolName: (entityName, artifactKind) => `${entityName}${artifactKind}`,
328
824
  },
329
- run: (ctx, config) => {
825
+ run: (ctx, rawConfig) => {
826
+ // Resolve config with function defaults
827
+ const config = {
828
+ ...rawConfig,
829
+ exportName: rawConfig.exportName ?? defaultExportName,
830
+ functionExportName: rawConfig.functionExportName ?? defaultFunctionExportName,
831
+ };
330
832
  const enums = getEnumEntities(ctx.ir);
331
- const dbTypesPath = config.dbTypesPath ?? "../DB.js";
332
- const executeQueries = config.executeQueries ?? true;
833
+ const defaultSchemas = ctx.ir.schemas;
834
+ const { dbTypesPath, executeQueries, generateListMany, exportName, functionExportName } = config;
835
+ // Pre-compute function groupings by return entity name
836
+ // Functions returning entities go in that entity's file; scalars go in functions.ts
837
+ const functionsByEntity = new Map();
838
+ const scalarFunctions = [];
839
+ if (config.generateFunctions) {
840
+ const { queries, mutations } = getGeneratableFunctions(ctx.ir);
841
+ const allFunctions = [...queries, ...mutations];
842
+ for (const fn of allFunctions) {
843
+ const resolved = resolveReturnType(fn, ctx.ir);
844
+ if (resolved.returnEntity) {
845
+ const entityName = resolved.returnEntity.name;
846
+ const existing = functionsByEntity.get(entityName) ?? [];
847
+ functionsByEntity.set(entityName, [...existing, fn]);
848
+ }
849
+ else {
850
+ scalarFunctions.push(fn);
851
+ }
852
+ }
853
+ }
333
854
  getTableEntities(ctx.ir)
334
855
  .filter(entity => entity.tags.omit !== true)
335
856
  .forEach(entity => {
336
- const genCtx = { entity, enums, ir: ctx.ir, dbTypesPath, executeQueries };
337
- const methods = [...generateCrudMethods(genCtx), ...generateLookupMethods(genCtx)];
338
- if (methods.length === 0)
339
- return;
340
857
  const entityName = ctx.inflection.entityName(entity.pgClass, entity.tags);
858
+ const genCtx = { entity, enums, ir: ctx.ir, defaultSchemas, dbTypesPath, executeQueries, generateListMany, entityName, exportName };
859
+ const crudStatements = [...generateCrudMethods(genCtx), ...generateLookupMethods(genCtx)];
860
+ // Get functions that return this entity
861
+ const entityFunctions = functionsByEntity.get(entity.name) ?? [];
862
+ if (crudStatements.length === 0 && entityFunctions.length === 0)
863
+ return;
341
864
  const fileNameCtx = {
342
865
  entityName,
343
866
  pgName: entity.pgName,
@@ -346,35 +869,92 @@ export const kyselyQueriesPlugin = definePlugin({
346
869
  entity,
347
870
  };
348
871
  const filePath = `${config.outputDir}/${ctx.pluginInflection.outputFile(fileNameCtx)}`;
349
- // Build the namespace object: export const users = { findById, findMany, ... }
350
- const namespaceObj = b.objectExpression(methods.map(m => m));
351
- // Lowercase entity name for the namespace variable
352
- const namespaceName = entity.pgName;
353
- const constDecl = b.variableDeclaration("const", [
354
- b.variableDeclarator(id(namespaceName), namespaceObj)
355
- ]);
356
- const exportDecl = b.exportNamedDeclaration(constDecl, []);
872
+ // All statements for the file: CRUD methods + function wrappers
873
+ const statements = [...crudStatements];
874
+ // Add function wrappers as flat exports
875
+ for (const fn of entityFunctions) {
876
+ statements.push(generateFunctionWrapper(fn, ctx.ir, executeQueries, config.functionExportName));
877
+ }
357
878
  const file = ctx
358
- .file(filePath)
359
- .header(config.header ? `${config.header}\n` : "// This file is auto-generated. Do not edit.\n");
879
+ .file(filePath);
360
880
  // Import Kysely type and DB from kysely-codegen output
361
881
  file.import({ kind: "package", types: ["Kysely"], from: "kysely" });
362
882
  file.import({ kind: "relative", types: ["DB"], from: dbTypesPath });
363
883
  // Import Insertable/Updateable helper types and table type if we generate create/update
364
884
  const tableTypeName = getTableTypeName(entity);
885
+ // Check if any lookup methods use semantic naming (FK with Selectable indexed access)
886
+ const hasSemanticLookups = entity.indexes.some(index => {
887
+ if (!shouldGenerateLookup(index) || index.isPrimary)
888
+ return false;
889
+ const columnName = index.columnNames[0];
890
+ const relation = findRelationForColumn(entity, columnName);
891
+ if (!relation)
892
+ return false;
893
+ const paramName = deriveSemanticName(relation, columnName);
894
+ const field = findRowField(entity, columnName);
895
+ const fieldName = field?.name ?? index.columns[0];
896
+ return paramName !== fieldName;
897
+ });
898
+ // Import table type if needed for Insertable/Updateable or semantic lookups
899
+ const needsTableType = entity.permissions.canInsert || entity.permissions.canUpdate || hasSemanticLookups;
900
+ if (needsTableType) {
901
+ file.import({ kind: "relative", types: [tableTypeName], from: dbTypesPath });
902
+ }
903
+ // Import Selectable if we have semantic lookups (for unwrapping Generated<T>)
904
+ if (hasSemanticLookups) {
905
+ file.import({ kind: "package", types: ["Selectable"], from: "kysely" });
906
+ }
365
907
  if (entity.permissions.canInsert) {
366
908
  file.import({ kind: "package", types: ["Insertable"], from: "kysely" });
367
- file.import({ kind: "relative", types: [tableTypeName], from: dbTypesPath });
368
909
  }
369
910
  if (entity.permissions.canUpdate) {
370
911
  file.import({ kind: "package", types: ["Updateable"], from: "kysely" });
371
- // Only import table type if not already imported by canInsert
372
- if (!entity.permissions.canInsert) {
373
- file.import({ kind: "relative", types: [tableTypeName], from: dbTypesPath });
912
+ }
913
+ // Import types needed by function args (for functions grouped into this file)
914
+ if (entityFunctions.length > 0) {
915
+ const fnTypeImports = collectFunctionTypeImports(entityFunctions, ctx.ir);
916
+ // Remove the entity's own type (already in scope or self-referential)
917
+ fnTypeImports.delete(entity.name);
918
+ if (fnTypeImports.size > 0) {
919
+ file.import({ kind: "relative", types: [...fnTypeImports], from: dbTypesPath });
374
920
  }
375
921
  }
376
- file.ast(conjure.program(exportDecl)).emit();
922
+ file.ast(conjure.program(...statements)).emit();
377
923
  });
924
+ // Generate files for composite types that have functions returning them
925
+ if (config.generateFunctions) {
926
+ const composites = getCompositeEntities(ctx.ir);
927
+ for (const composite of composites) {
928
+ const compositeFunctions = functionsByEntity.get(composite.name) ?? [];
929
+ if (compositeFunctions.length === 0)
930
+ continue;
931
+ const filePath = `${config.outputDir}/${composite.name}.ts`;
932
+ const statements = compositeFunctions.map(fn => generateFunctionWrapper(fn, ctx.ir, executeQueries, config.functionExportName));
933
+ const file = ctx.file(filePath);
934
+ file.import({ kind: "package", types: ["Kysely"], from: "kysely" });
935
+ file.import({ kind: "relative", types: ["DB"], from: dbTypesPath });
936
+ // Import the composite type and any types needed by function args
937
+ const fnTypeImports = collectFunctionTypeImports(compositeFunctions, ctx.ir);
938
+ fnTypeImports.add(composite.name); // Always import the composite type
939
+ file.import({ kind: "relative", types: [...fnTypeImports], from: dbTypesPath });
940
+ file.ast(conjure.program(...statements)).emit();
941
+ }
942
+ }
943
+ // Generate functions.ts for scalar-returning functions only
944
+ if (config.generateFunctions && scalarFunctions.length > 0) {
945
+ const filePath = `${config.outputDir}/${config.functionsFile}`;
946
+ const statements = scalarFunctions.map(fn => generateFunctionWrapper(fn, ctx.ir, executeQueries, config.functionExportName));
947
+ const file = ctx.file(filePath);
948
+ // Import Kysely type and DB
949
+ file.import({ kind: "package", types: ["Kysely"], from: "kysely" });
950
+ file.import({ kind: "relative", types: ["DB"], from: dbTypesPath });
951
+ // Import any types needed for function args (scalars don't need return type imports)
952
+ const typeImports = collectFunctionTypeImports(scalarFunctions, ctx.ir);
953
+ if (typeImports.size > 0) {
954
+ file.import({ kind: "relative", types: [...typeImports], from: dbTypesPath });
955
+ }
956
+ file.ast(conjure.program(...statements)).emit();
957
+ }
378
958
  },
379
959
  });
380
960
  //# sourceMappingURL=kysely-queries.js.map