@danielfgray/pg-sourcerer 0.1.9 → 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.
@@ -6,24 +6,28 @@
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({
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({
19
24
  outputDir: S.optionalWith(S.String, { default: () => "kysely-queries" }),
20
- header: S.optional(S.String),
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.optionalWith(S.String, { default: () => "../DB.js" }),
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[]>.
@@ -36,15 +40,37 @@ const KyselyQueriesPluginConfig = S.Struct({
36
40
  * When enabled, generates: listMany(db, limit = 50, offset = 0)
37
41
  */
38
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),
39
60
  });
40
61
  /**
41
62
  * Get the Kysely table interface name from the entity.
42
- * Converts schema.table to PascalCase: app_public.users -> AppPublicUsers
43
- * Uses the inflection utility to match kysely-codegen's naming convention.
63
+ * Uses entity.name which is already PascalCase from inflection (e.g., Users).
44
64
  */
45
- const getTableTypeName = (entity) => `${inflect.pascalCase(entity.schemaName)}${inflect.pascalCase(entity.pgName)}`;
46
- /** Get the schema-qualified table name for Kysely */
47
- const getTableRef = (entity) => `${entity.schemaName}.${entity.pgName}`;
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.
70
+ */
71
+ const getTableRef = (entity, defaultSchemas) => defaultSchemas.includes(entity.schemaName)
72
+ ? entity.pgName
73
+ : `${entity.schemaName}.${entity.pgName}`;
48
74
  /** Find a field in the row shape by column name */
49
75
  const findRowField = (entity, columnName) => entity.shapes.row.fields.find(f => f.columnName === columnName);
50
76
  /** Get the TypeScript type AST for a field */
@@ -132,6 +158,15 @@ const deleteFrom = (tableRef) => call(id("db"), "deleteFrom", [str(tableRef)]);
132
158
  * Chain method call onto existing expression
133
159
  */
134
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
+ };
135
170
  /**
136
171
  * Build arrow function expression: (params) => body
137
172
  */
@@ -147,21 +182,398 @@ const objProp = (key, value) => {
147
182
  return prop;
148
183
  };
149
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
+ // ============================================================================
150
562
  // CRUD Method Generators
151
563
  // ============================================================================
152
564
  /**
153
565
  * Generate findById method:
154
- * 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()
155
567
  */
156
568
  const generateFindById = (ctx) => {
157
- const { entity, executeQueries } = ctx;
569
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
158
570
  if (!entity.primaryKey || !entity.permissions.canSelect)
159
571
  return undefined;
160
572
  const pkColName = entity.primaryKey.columns[0];
161
573
  const pkField = findRowField(entity, pkColName);
162
574
  if (!pkField)
163
575
  return undefined;
164
- const tableRef = getTableRef(entity);
576
+ const tableRef = getTableRef(entity, defaultSchemas);
165
577
  const fieldName = pkField.name;
166
578
  const fieldType = getFieldTypeAst(pkField, ctx);
167
579
  // db.selectFrom('table').selectAll().where('col', '=', id)
@@ -170,7 +582,7 @@ const generateFindById = (ctx) => {
170
582
  query = chain(query, "executeTakeFirst");
171
583
  }
172
584
  const fn = arrowFn([typedParam("db", ts.ref("Kysely", [ts.ref("DB")])), typedParam(fieldName, fieldType)], query);
173
- return objProp("findById", fn);
585
+ return exportConst(exportName(entityName, "FindById"), fn);
174
586
  };
175
587
  /** Default limit for findMany queries */
176
588
  const DEFAULT_LIMIT = 50;
@@ -183,14 +595,14 @@ const DEFAULT_OFFSET = 0;
183
595
  const paramWithDefault = (name, defaultValue) => b.assignmentPattern(id(name), toExpr(defaultValue));
184
596
  /**
185
597
  * Generate listMany method with pagination defaults:
186
- * listMany: (db, limit = 50, offset = 0) => db.selectFrom('table').selectAll()
598
+ * export const UserListMany = (db, limit = 50, offset = 0) => db.selectFrom('table').selectAll()
187
599
  * .limit(limit).offset(offset).execute()
188
600
  */
189
601
  const generateListMany = (ctx) => {
190
- const { entity, executeQueries } = ctx;
602
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
191
603
  if (!entity.permissions.canSelect)
192
604
  return undefined;
193
- const tableRef = getTableRef(entity);
605
+ const tableRef = getTableRef(entity, defaultSchemas);
194
606
  // Build query: db.selectFrom('table').selectAll().limit(limit).offset(offset)
195
607
  let query = chain(chain(chain(selectFrom(tableRef), "selectAll"), "limit", [id("limit")]), "offset", [id("offset")]);
196
608
  // Add .execute() if executeQueries is true
@@ -202,17 +614,17 @@ const generateListMany = (ctx) => {
202
614
  paramWithDefault("limit", b.numericLiteral(DEFAULT_LIMIT)),
203
615
  paramWithDefault("offset", b.numericLiteral(DEFAULT_OFFSET)),
204
616
  ], query);
205
- return objProp("listMany", fn);
617
+ return exportConst(exportName(entityName, "ListMany"), fn);
206
618
  };
207
619
  /**
208
620
  * Generate create method:
209
- * create: (db, data) => db.insertInto('table').values(data).returningAll().executeTakeFirstOrThrow()
621
+ * export const UserCreate = (db, data) => db.insertInto('table').values(data).returningAll().executeTakeFirstOrThrow()
210
622
  */
211
623
  const generateCreate = (ctx) => {
212
- const { entity, executeQueries } = ctx;
624
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
213
625
  if (!entity.permissions.canInsert)
214
626
  return undefined;
215
- const tableRef = getTableRef(entity);
627
+ const tableRef = getTableRef(entity, defaultSchemas);
216
628
  const tableTypeName = getTableTypeName(entity);
217
629
  // db.insertInto('table').values(data).returningAll()
218
630
  let query = chain(chain(insertInto(tableRef), "values", [id("data")]), "returningAll");
@@ -224,21 +636,21 @@ const generateCreate = (ctx) => {
224
636
  typedParam("db", ts.ref("Kysely", [ts.ref("DB")])),
225
637
  typedParam("data", ts.ref("Insertable", [ts.ref(tableTypeName)])),
226
638
  ], query);
227
- return objProp("create", fn);
639
+ return exportConst(exportName(entityName, "Create"), fn);
228
640
  };
229
641
  /**
230
642
  * Generate update method:
231
- * 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()
232
644
  */
233
645
  const generateUpdate = (ctx) => {
234
- const { entity, executeQueries } = ctx;
646
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
235
647
  if (!entity.primaryKey || !entity.permissions.canUpdate)
236
648
  return undefined;
237
649
  const pkColName = entity.primaryKey.columns[0];
238
650
  const pkField = findRowField(entity, pkColName);
239
651
  if (!pkField)
240
652
  return undefined;
241
- const tableRef = getTableRef(entity);
653
+ const tableRef = getTableRef(entity, defaultSchemas);
242
654
  const fieldName = pkField.name;
243
655
  const fieldType = getFieldTypeAst(pkField, ctx);
244
656
  const tableTypeName = getTableTypeName(entity);
@@ -253,21 +665,21 @@ const generateUpdate = (ctx) => {
253
665
  typedParam(fieldName, fieldType),
254
666
  typedParam("data", ts.ref("Updateable", [ts.ref(tableTypeName)])),
255
667
  ], query);
256
- return objProp("update", fn);
668
+ return exportConst(exportName(entityName, "Update"), fn);
257
669
  };
258
670
  /**
259
671
  * Generate delete method:
260
- * delete: (db, id) => db.deleteFrom('table').where('id', '=', id).execute()
672
+ * export const UserRemove = (db, id) => db.deleteFrom('table').where('id', '=', id).execute()
261
673
  */
262
674
  const generateDelete = (ctx) => {
263
- const { entity, executeQueries } = ctx;
675
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
264
676
  if (!entity.primaryKey || !entity.permissions.canDelete)
265
677
  return undefined;
266
678
  const pkColName = entity.primaryKey.columns[0];
267
679
  const pkField = findRowField(entity, pkColName);
268
680
  if (!pkField)
269
681
  return undefined;
270
- const tableRef = getTableRef(entity);
682
+ const tableRef = getTableRef(entity, defaultSchemas);
271
683
  const fieldName = pkField.name;
272
684
  const fieldType = getFieldTypeAst(pkField, ctx);
273
685
  // db.deleteFrom('table').where('id', '=', id)
@@ -276,8 +688,7 @@ const generateDelete = (ctx) => {
276
688
  query = chain(query, "execute");
277
689
  }
278
690
  const fn = arrowFn([typedParam("db", ts.ref("Kysely", [ts.ref("DB")])), typedParam(fieldName, fieldType)], query);
279
- // Use 'remove' since 'delete' is a reserved word
280
- return objProp("remove", fn);
691
+ return exportConst(exportName(entityName, "Remove"), fn);
281
692
  };
282
693
  /** Generate all CRUD methods for an entity */
283
694
  const generateCrudMethods = (ctx) => [
@@ -297,27 +708,27 @@ const shouldGenerateLookup = (index) => !index.isPartial &&
297
708
  index.method !== "gin" &&
298
709
  index.method !== "gist";
299
710
  /**
300
- * Generate a method name for an index-based lookup.
711
+ * Generate the method name portion for an index-based lookup.
301
712
  * Uses semantic naming when the column corresponds to an FK relation.
302
713
  */
303
- const generateLookupName = (entity, index, relation) => {
714
+ const generateLookupMethodName = (entity, index, relation) => {
304
715
  const isUnique = isUniqueLookup(entity, index);
305
- // Kysely uses "findBy" prefix consistently, with "One" or "Many" suffix
306
- const prefix = isUnique ? "findOneBy" : "findManyBy";
716
+ // Uses "FindOneBy" or "FindManyBy" suffix
717
+ const suffix = isUnique ? "FindOneBy" : "FindManyBy";
307
718
  // Use semantic name if FK relation exists, otherwise fall back to column name
308
719
  const columnName = index.columnNames[0];
309
720
  const byName = relation
310
721
  ? deriveSemanticName(relation, columnName)
311
722
  : index.columns[0];
312
- return `${prefix}${toPascalCase(byName)}`;
723
+ return `${suffix}${toPascalCase(byName)}`;
313
724
  };
314
725
  /**
315
726
  * Generate a lookup method for a single-column index.
316
727
  * Uses semantic parameter naming when the column corresponds to an FK relation.
317
728
  */
318
729
  const generateLookupMethod = (index, ctx) => {
319
- const { entity, executeQueries } = ctx;
320
- const tableRef = getTableRef(entity);
730
+ const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
731
+ const tableRef = getTableRef(entity, defaultSchemas);
321
732
  const columnName = index.columnNames[0];
322
733
  const field = findRowField(entity, columnName);
323
734
  const fieldName = field?.name ?? index.columns[0];
@@ -342,8 +753,8 @@ const generateLookupMethod = (index, ctx) => {
342
753
  query = chain(query, isUnique ? "executeTakeFirst" : "execute");
343
754
  }
344
755
  const fn = arrowFn([typedParam("db", ts.ref("Kysely", [ts.ref("DB")])), typedParam(paramName, paramType)], query);
345
- const methodName = generateLookupName(entity, index, relation);
346
- return objProp(methodName, fn);
756
+ const methodName = generateLookupMethodName(entity, index, relation);
757
+ return exportConst(exportName(entityName, methodName), fn);
347
758
  };
348
759
  /**
349
760
  * Check if a column is covered by a unique constraint (not just unique index).
@@ -406,22 +817,50 @@ export const kyselyQueriesPlugin = definePlugin({
406
817
  name: "kysely-queries",
407
818
  provides: ["queries", "queries:kysely"],
408
819
  requires: [], // No dependency on types:kysely for now - uses external kysely-codegen types
409
- configSchema: KyselyQueriesPluginConfig,
820
+ configSchema: KyselyQueriesPluginConfigSchema,
410
821
  inflection: {
411
822
  outputFile: ctx => `${ctx.entityName}.ts`,
412
823
  symbolName: (entityName, artifactKind) => `${entityName}${artifactKind}`,
413
824
  },
414
- 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
+ };
415
832
  const enums = getEnumEntities(ctx.ir);
416
- const { dbTypesPath, executeQueries, generateListMany } = config;
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
+ }
417
854
  getTableEntities(ctx.ir)
418
855
  .filter(entity => entity.tags.omit !== true)
419
856
  .forEach(entity => {
420
- const genCtx = { entity, enums, ir: ctx.ir, dbTypesPath, executeQueries, generateListMany };
421
- const methods = [...generateCrudMethods(genCtx), ...generateLookupMethods(genCtx)];
422
- if (methods.length === 0)
423
- return;
424
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;
425
864
  const fileNameCtx = {
426
865
  entityName,
427
866
  pgName: entity.pgName,
@@ -430,17 +869,14 @@ export const kyselyQueriesPlugin = definePlugin({
430
869
  entity,
431
870
  };
432
871
  const filePath = `${config.outputDir}/${ctx.pluginInflection.outputFile(fileNameCtx)}`;
433
- // Build the namespace object: export const users = { findById, findMany, ... }
434
- const namespaceObj = b.objectExpression(methods.map(m => m));
435
- // Lowercase entity name for the namespace variable
436
- const namespaceName = entity.pgName;
437
- const constDecl = b.variableDeclaration("const", [
438
- b.variableDeclarator(id(namespaceName), namespaceObj)
439
- ]);
440
- 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
+ }
441
878
  const file = ctx
442
- .file(filePath)
443
- .header(config.header ? `${config.header}\n` : "// This file is auto-generated. Do not edit.\n");
879
+ .file(filePath);
444
880
  // Import Kysely type and DB from kysely-codegen output
445
881
  file.import({ kind: "package", types: ["Kysely"], from: "kysely" });
446
882
  file.import({ kind: "relative", types: ["DB"], from: dbTypesPath });
@@ -474,8 +910,51 @@ export const kyselyQueriesPlugin = definePlugin({
474
910
  if (entity.permissions.canUpdate) {
475
911
  file.import({ kind: "package", types: ["Updateable"], from: "kysely" });
476
912
  }
477
- file.ast(conjure.program(exportDecl)).emit();
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 });
920
+ }
921
+ }
922
+ file.ast(conjure.program(...statements)).emit();
478
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
+ }
479
958
  },
480
959
  });
481
960
  //# sourceMappingURL=kysely-queries.js.map