@danielfgray/pg-sourcerer 0.2.0 → 0.2.2

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 (173) hide show
  1. package/dist/cli.js +25 -19
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +1 -1
  6. package/dist/config.js.map +1 -1
  7. package/dist/errors.d.ts +14 -1
  8. package/dist/errors.d.ts.map +1 -1
  9. package/dist/errors.js +2 -0
  10. package/dist/errors.js.map +1 -1
  11. package/dist/generate.d.ts +31 -21
  12. package/dist/generate.d.ts.map +1 -1
  13. package/dist/generate.js +53 -43
  14. package/dist/generate.js.map +1 -1
  15. package/dist/index.d.ts +20 -12
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +27 -13
  18. package/dist/index.js.map +1 -1
  19. package/dist/init.d.ts +0 -1
  20. package/dist/init.d.ts.map +1 -1
  21. package/dist/init.js +90 -35
  22. package/dist/init.js.map +1 -1
  23. package/dist/ir/extensions/queries.d.ts +264 -0
  24. package/dist/ir/extensions/queries.d.ts.map +1 -0
  25. package/dist/ir/extensions/queries.js +153 -0
  26. package/dist/ir/extensions/queries.js.map +1 -0
  27. package/dist/ir/extensions/schema-builder.d.ts +61 -0
  28. package/dist/ir/extensions/schema-builder.d.ts.map +1 -0
  29. package/dist/ir/extensions/schema-builder.js +5 -0
  30. package/dist/ir/extensions/schema-builder.js.map +1 -0
  31. package/dist/lib/conjure.d.ts +101 -0
  32. package/dist/lib/conjure.d.ts.map +1 -1
  33. package/dist/lib/conjure.js +204 -26
  34. package/dist/lib/conjure.js.map +1 -1
  35. package/dist/lib/hex.d.ts +10 -8
  36. package/dist/lib/hex.d.ts.map +1 -1
  37. package/dist/lib/hex.js +18 -15
  38. package/dist/lib/hex.js.map +1 -1
  39. package/dist/plugins/arktype.d.ts +27 -14
  40. package/dist/plugins/arktype.d.ts.map +1 -1
  41. package/dist/plugins/arktype.js +166 -130
  42. package/dist/plugins/arktype.js.map +1 -1
  43. package/dist/plugins/effect.d.ts +53 -0
  44. package/dist/plugins/effect.d.ts.map +1 -0
  45. package/dist/plugins/effect.js +1074 -0
  46. package/dist/plugins/effect.js.map +1 -0
  47. package/dist/plugins/http-elysia.d.ts +32 -0
  48. package/dist/plugins/http-elysia.d.ts.map +1 -0
  49. package/dist/plugins/http-elysia.js +613 -0
  50. package/dist/plugins/http-elysia.js.map +1 -0
  51. package/dist/plugins/http-express.d.ts +36 -0
  52. package/dist/plugins/http-express.d.ts.map +1 -0
  53. package/dist/plugins/http-express.js +388 -0
  54. package/dist/plugins/http-express.js.map +1 -0
  55. package/dist/plugins/http-hono.d.ts +36 -0
  56. package/dist/plugins/http-hono.d.ts.map +1 -0
  57. package/dist/plugins/http-hono.js +453 -0
  58. package/dist/plugins/http-hono.js.map +1 -0
  59. package/dist/plugins/http-orpc.d.ts +55 -0
  60. package/dist/plugins/http-orpc.d.ts.map +1 -0
  61. package/dist/plugins/http-orpc.js +370 -0
  62. package/dist/plugins/http-orpc.js.map +1 -0
  63. package/dist/plugins/http-trpc.d.ts +59 -0
  64. package/dist/plugins/http-trpc.d.ts.map +1 -0
  65. package/dist/plugins/http-trpc.js +392 -0
  66. package/dist/plugins/http-trpc.js.map +1 -0
  67. package/dist/plugins/kysely/queries.d.ts +92 -0
  68. package/dist/plugins/kysely/queries.d.ts.map +1 -0
  69. package/dist/plugins/kysely/queries.js +1169 -0
  70. package/dist/plugins/kysely/queries.js.map +1 -0
  71. package/dist/plugins/kysely/shared.d.ts +59 -0
  72. package/dist/plugins/kysely/shared.d.ts.map +1 -0
  73. package/dist/plugins/kysely/shared.js +247 -0
  74. package/dist/plugins/kysely/shared.js.map +1 -0
  75. package/dist/plugins/kysely/types.d.ts +22 -0
  76. package/dist/plugins/kysely/types.d.ts.map +1 -0
  77. package/dist/plugins/kysely/types.js +428 -0
  78. package/dist/plugins/kysely/types.js.map +1 -0
  79. package/dist/plugins/kysely.d.ts +72 -0
  80. package/dist/plugins/kysely.d.ts.map +1 -0
  81. package/dist/plugins/kysely.js +906 -0
  82. package/dist/plugins/kysely.js.map +1 -0
  83. package/dist/plugins/sql-queries.d.ts +55 -7
  84. package/dist/plugins/sql-queries.d.ts.map +1 -1
  85. package/dist/plugins/sql-queries.js +747 -121
  86. package/dist/plugins/sql-queries.js.map +1 -1
  87. package/dist/plugins/types.d.ts +20 -14
  88. package/dist/plugins/types.d.ts.map +1 -1
  89. package/dist/plugins/types.js +90 -112
  90. package/dist/plugins/types.js.map +1 -1
  91. package/dist/plugins/valibot.d.ts +45 -0
  92. package/dist/plugins/valibot.d.ts.map +1 -0
  93. package/dist/plugins/valibot.js +422 -0
  94. package/dist/plugins/valibot.js.map +1 -0
  95. package/dist/plugins/zod.d.ts +27 -14
  96. package/dist/plugins/zod.d.ts.map +1 -1
  97. package/dist/plugins/zod.js +231 -166
  98. package/dist/plugins/zod.js.map +1 -1
  99. package/dist/services/artifact-store.d.ts +11 -1
  100. package/dist/services/artifact-store.d.ts.map +1 -1
  101. package/dist/services/artifact-store.js +9 -0
  102. package/dist/services/artifact-store.js.map +1 -1
  103. package/dist/services/config-loader.d.ts +4 -0
  104. package/dist/services/config-loader.d.ts.map +1 -1
  105. package/dist/services/config-loader.js +1 -1
  106. package/dist/services/config-loader.js.map +1 -1
  107. package/dist/services/config.d.ts +57 -0
  108. package/dist/services/config.d.ts.map +1 -0
  109. package/dist/services/config.js +66 -0
  110. package/dist/services/config.js.map +1 -0
  111. package/dist/services/core-providers.d.ts +15 -0
  112. package/dist/services/core-providers.d.ts.map +1 -0
  113. package/dist/services/core-providers.js +23 -0
  114. package/dist/services/core-providers.js.map +1 -0
  115. package/dist/services/emissions.d.ts +14 -0
  116. package/dist/services/emissions.d.ts.map +1 -1
  117. package/dist/services/emissions.js +86 -47
  118. package/dist/services/emissions.js.map +1 -1
  119. package/dist/services/execution.d.ts +35 -0
  120. package/dist/services/execution.d.ts.map +1 -0
  121. package/dist/services/execution.js +86 -0
  122. package/dist/services/execution.js.map +1 -0
  123. package/dist/services/file-builder.d.ts +4 -0
  124. package/dist/services/file-builder.d.ts.map +1 -1
  125. package/dist/services/file-builder.js.map +1 -1
  126. package/dist/services/inflection.d.ts +2 -2
  127. package/dist/services/inflection.d.ts.map +1 -1
  128. package/dist/services/inflection.js +4 -4
  129. package/dist/services/inflection.js.map +1 -1
  130. package/dist/services/ir-builder.d.ts.map +1 -1
  131. package/dist/services/ir-builder.js +10 -3
  132. package/dist/services/ir-builder.js.map +1 -1
  133. package/dist/services/pg-types.d.ts +31 -0
  134. package/dist/services/pg-types.d.ts.map +1 -1
  135. package/dist/services/pg-types.js +24 -0
  136. package/dist/services/pg-types.js.map +1 -1
  137. package/dist/services/plugin-runner.d.ts +27 -37
  138. package/dist/services/plugin-runner.d.ts.map +1 -1
  139. package/dist/services/plugin-runner.js +73 -171
  140. package/dist/services/plugin-runner.js.map +1 -1
  141. package/dist/services/plugin.d.ts +350 -215
  142. package/dist/services/plugin.d.ts.map +1 -1
  143. package/dist/services/plugin.js +182 -130
  144. package/dist/services/plugin.js.map +1 -1
  145. package/dist/services/resolution.d.ts +38 -0
  146. package/dist/services/resolution.d.ts.map +1 -0
  147. package/dist/services/resolution.js +242 -0
  148. package/dist/services/resolution.js.map +1 -0
  149. package/dist/services/service-registry.d.ts +74 -0
  150. package/dist/services/service-registry.d.ts.map +1 -0
  151. package/dist/services/service-registry.js +61 -0
  152. package/dist/services/service-registry.js.map +1 -0
  153. package/dist/services/symbols.d.ts +59 -0
  154. package/dist/services/symbols.d.ts.map +1 -1
  155. package/dist/services/symbols.js +16 -0
  156. package/dist/services/symbols.js.map +1 -1
  157. package/dist/testing.d.ts +4 -25
  158. package/dist/testing.d.ts.map +1 -1
  159. package/dist/testing.js +2 -23
  160. package/dist/testing.js.map +1 -1
  161. package/package.json +1 -1
  162. package/dist/plugins/effect-model.d.ts +0 -17
  163. package/dist/plugins/effect-model.d.ts.map +0 -1
  164. package/dist/plugins/effect-model.js +0 -409
  165. package/dist/plugins/effect-model.js.map +0 -1
  166. package/dist/plugins/kysely-queries.d.ts +0 -66
  167. package/dist/plugins/kysely-queries.d.ts.map +0 -1
  168. package/dist/plugins/kysely-queries.js +0 -960
  169. package/dist/plugins/kysely-queries.js.map +0 -1
  170. package/dist/plugins/kysely-types.d.ts +0 -35
  171. package/dist/plugins/kysely-types.d.ts.map +0 -1
  172. package/dist/plugins/kysely-types.js +0 -601
  173. package/dist/plugins/kysely-types.js.map +0 -1
@@ -1,24 +1,65 @@
1
1
  /**
2
- * SQL Queries Plugin - Generate raw SQL query functions using template strings
2
+ * SQL Queries Provider - Generate raw SQL query functions using template strings
3
3
  */
4
4
  import { Schema as S } from "effect";
5
5
  import { definePlugin } from "../services/plugin.js";
6
- import { getTableEntities, getEnumEntities } from "../ir/semantic-ir.js";
6
+ import { getTableEntities, getEnumEntities, getFunctionEntities, getCompositeEntities, } from "../ir/semantic-ir.js";
7
7
  import { conjure } from "../lib/conjure.js";
8
8
  import { hex } from "../lib/hex.js";
9
9
  import { resolveFieldType, tsTypeToAst } from "../lib/field-utils.js";
10
10
  import { inflect } from "../services/inflection.js";
11
- const { ts, b, param } = conjure;
12
- // ============================================================================
13
- // Configuration
14
- // ============================================================================
15
- const SqlQueriesPluginConfig = S.Struct({
11
+ const { ts, b, param, asyncFn } = conjure;
12
+ /** Default export name: camelCase of methodName + entityName (e.g., "findUserById") */
13
+ const defaultExportName = (entityName, methodName) => {
14
+ // methodName is like "FindById", "Insert", "GetByUsername"
15
+ // We want: findUserById, insertUser, getUserByUsername
16
+ const camelMethod = methodName.charAt(0).toLowerCase() + methodName.slice(1);
17
+ // Insert entity name after the verb (find, insert, get, delete)
18
+ // Pattern: verb + Entity + rest (e.g., find + User + ById)
19
+ const verbMatch = camelMethod.match(/^(find|insert|delete|get)(.*)$/);
20
+ if (verbMatch) {
21
+ const [, verb, rest] = verbMatch;
22
+ return `${verb}${entityName}${rest}`;
23
+ }
24
+ // Fallback: just prepend entity
25
+ return `${camelMethod}${entityName}`;
26
+ };
27
+ const SqlQueriesPluginConfigSchema = S.Struct({
16
28
  outputDir: S.optionalWith(S.String, { default: () => "sql-queries" }),
29
+ /**
30
+ * Header content to prepend to each generated file.
31
+ * Must include the SQL client import (e.g., `import { sql } from "../db"`).
32
+ */
33
+ header: S.String,
17
34
  /** SQL query style. Defaults to "tag" (tagged template literals) */
18
- sqlStyle: S.optionalWith(S.Union(S.Literal("tag"), S.Literal("string")), { default: () => "tag" }),
35
+ sqlStyle: S.optionalWith(S.Union(S.Literal("tag"), S.Literal("string")), {
36
+ default: () => "tag",
37
+ }),
38
+ /**
39
+ * Use explicit column lists instead of SELECT *.
40
+ * When true, generates "SELECT col1, col2" which excludes omitted fields at runtime.
41
+ * Defaults to true.
42
+ */
43
+ explicitColumns: S.optionalWith(S.Boolean, { default: () => true }),
44
+ /** Generate wrappers for PostgreSQL functions. Defaults to true. */
45
+ generateFunctions: S.optionalWith(S.Boolean, { default: () => true }),
46
+ /** Output file for scalar-returning functions. Defaults to "functions.ts". */
47
+ functionsFile: S.optionalWith(S.String, { default: () => "functions.ts" }),
48
+ /** Export name function - use S.Any for schema, properly typed after resolution */
49
+ exportName: S.optional(S.Any),
50
+ /**
51
+ * Export style for generated query functions.
52
+ * - "flat": Individual exports (e.g., `export async function findById() {...}`)
53
+ * - "namespace": Single object export (e.g., `export const User = { findById: ... }`)
54
+ */
55
+ exportStyle: S.optionalWith(S.Literal("flat", "namespace"), { default: () => "flat" }),
19
56
  });
20
57
  /** Find a field in the row shape by column name */
21
58
  const findRowField = (entity, columnName) => entity.shapes.row.fields.find(f => f.columnName === columnName);
59
+ /** Build comma-separated column list from row shape fields */
60
+ const buildColumnList = (entity) => entity.shapes.row.fields.map(f => f.columnName).join(", ");
61
+ /** Build SELECT clause - explicit columns or * based on config */
62
+ const buildSelectClause = (entity, explicitColumns) => explicitColumns ? `select ${buildColumnList(entity)}` : "select *";
22
63
  /** Get the TypeScript type AST for a field */
23
64
  const getFieldTypeAst = (field, ctx) => {
24
65
  if (!field)
@@ -67,9 +108,16 @@ const toPascalCase = (s) => inflect.pascalCase(s);
67
108
  // ============================================================================
68
109
  // CRUD Function Generators
69
110
  // ============================================================================
70
- /** Generate findById function if entity has a primary key and canSelect permission */
111
+ /** Get TypeScript type string for a field */
112
+ const getFieldTypeString = (field, ctx) => {
113
+ if (!field)
114
+ return "string";
115
+ const resolved = resolveFieldType(field, ctx.enums, ctx.ir.extensions);
116
+ return resolved.enumDef ? resolved.enumDef.name : resolved.tsType;
117
+ };
118
+ /** Generate findById method if entity has a primary key and canSelect permission */
71
119
  const generateFindById = (ctx) => {
72
- const { entity, sqlStyle } = ctx;
120
+ const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
73
121
  if (!entity.primaryKey || !entity.permissions.canSelect)
74
122
  return undefined;
75
123
  const pkColName = entity.primaryKey.columns[0];
@@ -78,38 +126,73 @@ const generateFindById = (ctx) => {
78
126
  return undefined;
79
127
  const rowType = entity.shapes.row.name;
80
128
  const fieldName = pkField.name; // JS property name (e.g., "id")
129
+ const selectClause = buildSelectClause(entity, explicitColumns);
81
130
  const parts = {
82
- templateParts: [`select * from ${entity.schemaName}.${entity.pgName} where ${pkColName} = `, ""],
131
+ templateParts: [
132
+ `${selectClause} from ${entity.schemaName}.${entity.pgName} where ${pkColName} = `,
133
+ "",
134
+ ],
83
135
  params: [b.identifier(fieldName)],
84
136
  };
85
137
  // Build query and extract first row
86
138
  const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
87
139
  const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
88
- return hex.exportFn(hex.asyncFn(`find${entity.name}ById`, [param.pick([fieldName], rowType)], [
89
- varDecl,
90
- b.returnStatement(b.identifier("result")),
91
- ]));
140
+ const name = exportName(entityName, "FindById");
141
+ const fn = asyncFn(name, [param.pick([fieldName], rowType)], [varDecl, b.returnStatement(b.identifier("result"))]);
142
+ const meta = {
143
+ name,
144
+ kind: "read",
145
+ params: [
146
+ {
147
+ name: fieldName,
148
+ type: getFieldTypeString(pkField, ctx),
149
+ required: true,
150
+ columnName: pkColName,
151
+ source: "pk",
152
+ },
153
+ ],
154
+ returns: { type: rowType, nullable: true, isArray: false },
155
+ callSignature: { style: "named" },
156
+ };
157
+ return { name, fn, meta };
92
158
  };
93
- /** Generate findMany function with pagination if entity has canSelect permission */
159
+ /** Generate findMany method with pagination if entity has canSelect permission */
94
160
  const generateFindMany = (ctx) => {
95
- const { entity, sqlStyle } = ctx;
161
+ const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
96
162
  if (!entity.permissions.canSelect)
97
163
  return undefined;
98
164
  const rowType = entity.shapes.row.name;
165
+ const selectClause = buildSelectClause(entity, explicitColumns);
99
166
  const parts = {
100
- templateParts: [`select * from ${entity.schemaName}.${entity.pgName} limit `, ` offset `, ""],
167
+ templateParts: [
168
+ `${selectClause} from ${entity.schemaName}.${entity.pgName} limit `,
169
+ ` offset `,
170
+ "",
171
+ ],
101
172
  params: [b.identifier("limit"), b.identifier("offset")],
102
173
  };
103
- return hex.exportFn(hex.asyncFn(`findMany${entity.name}s`, [
174
+ const name = exportName(entityName, "FindManys");
175
+ const fn = asyncFn(name, [
104
176
  param.destructured([
105
177
  { name: "limit", type: ts.number(), optional: true, defaultValue: b.numericLiteral(50) },
106
178
  { name: "offset", type: ts.number(), optional: true, defaultValue: b.numericLiteral(0) },
107
179
  ]),
108
- ], hex.returnQuery(sqlStyle, parts, ts.array(ts.ref(rowType)))));
180
+ ], hex.returnQuery(sqlStyle, parts, ts.array(ts.ref(rowType))));
181
+ const meta = {
182
+ name,
183
+ kind: "list",
184
+ params: [
185
+ { name: "limit", type: "number", required: false, source: "pagination" },
186
+ { name: "offset", type: "number", required: false, source: "pagination" },
187
+ ],
188
+ returns: { type: rowType, nullable: false, isArray: true },
189
+ callSignature: { style: "named" },
190
+ };
191
+ return { name, fn, meta };
109
192
  };
110
- /** Generate delete function if entity has a primary key and canDelete permission */
193
+ /** Generate delete method if entity has a primary key and canDelete permission */
111
194
  const generateDelete = (ctx) => {
112
- const { entity, sqlStyle } = ctx;
195
+ const { entity, sqlStyle, entityName, exportName } = ctx;
113
196
  if (!entity.primaryKey || !entity.permissions.canDelete)
114
197
  return undefined;
115
198
  const pkColName = entity.primaryKey.columns[0];
@@ -124,13 +207,28 @@ const generateDelete = (ctx) => {
124
207
  };
125
208
  // Delete returns void, no type parameter needed
126
209
  const queryExpr = hex.query(sqlStyle, parts);
127
- return hex.exportFn(hex.asyncFn(`delete${entity.name}`, [param.pick([fieldName], rowType)], [
128
- b.expressionStatement(queryExpr),
129
- ]));
210
+ const name = exportName(entityName, "Delete");
211
+ const fn = asyncFn(name, [param.pick([fieldName], rowType)], [b.expressionStatement(queryExpr)]);
212
+ const meta = {
213
+ name,
214
+ kind: "delete",
215
+ params: [
216
+ {
217
+ name: fieldName,
218
+ type: getFieldTypeString(pkField, ctx),
219
+ required: true,
220
+ columnName: pkColName,
221
+ source: "pk",
222
+ },
223
+ ],
224
+ returns: { type: "void", nullable: false, isArray: false },
225
+ callSignature: { style: "named" },
226
+ };
227
+ return { name, fn, meta };
130
228
  };
131
- /** Generate insert function if entity has canInsert permission */
229
+ /** Generate insert method if entity has canInsert permission */
132
230
  const generateInsert = (ctx) => {
133
- const { entity, sqlStyle } = ctx;
231
+ const { entity, sqlStyle, entityName, exportName } = ctx;
134
232
  if (!entity.permissions.canInsert)
135
233
  return undefined;
136
234
  // Use insert shape if available, otherwise fall back to row
@@ -142,10 +240,15 @@ const generateInsert = (ctx) => {
142
240
  if (insertableFields.length === 0)
143
241
  return undefined;
144
242
  const columnNames = insertableFields.map(f => f.columnName);
145
- const fieldNames = insertableFields.map(f => f.name);
146
- // Build: insert into schema.table (col1, col2) values ($data.field1, $data.field2) returning *
243
+ // Build: insert into schema.table (col1, col2) values ($field1, $field2) returning *
147
244
  const columnList = columnNames.join(", ");
148
- const valuePlaceholders = fieldNames.map((_, i) => (i === 0 ? "" : ", "));
245
+ const valuePlaceholders = insertableFields.map((_, i) => (i === 0 ? "" : ", "));
246
+ // For optional fields (nullable or has default), use DEFAULT when undefined
247
+ // Required fields use the value directly
248
+ const paramExprs = insertableFields.map(f => {
249
+ const isOptional = f.optional || f.nullable;
250
+ return isOptional ? hex.defaultIfUndefined(f.name) : b.identifier(f.name);
251
+ });
149
252
  // Template parts: "insert into ... values (" + "" + ", " + ", " + ... + ") returning *"
150
253
  const parts = {
151
254
  templateParts: [
@@ -153,21 +256,33 @@ const generateInsert = (ctx) => {
153
256
  ...valuePlaceholders.slice(1),
154
257
  `) returning *`,
155
258
  ],
156
- params: fieldNames.map(f => b.memberExpression(b.identifier("data"), b.identifier(f), false)),
259
+ params: paramExprs,
157
260
  };
158
261
  const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
159
262
  const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
160
- // Simple typed parameter: data: InsertType
161
- const dataParam = param.typed("data", ts.ref(insertType));
162
- return hex.exportFn(hex.asyncFn(`insert${entity.name}`, [dataParam], [varDecl, b.returnStatement(b.identifier("result"))]));
263
+ // Destructured parameter - use Pick from insert type
264
+ const fieldNames = insertableFields.map(f => f.name);
265
+ const dataParam = param.pick(fieldNames, insertType);
266
+ const name = exportName(entityName, "Insert");
267
+ const fn = asyncFn(name, [dataParam], [varDecl, b.returnStatement(b.identifier("result"))]);
268
+ const meta = {
269
+ name,
270
+ kind: "create",
271
+ params: [
272
+ {
273
+ name: "data",
274
+ type: insertType,
275
+ required: true,
276
+ source: "body",
277
+ },
278
+ ],
279
+ returns: { type: rowType, nullable: false, isArray: false },
280
+ callSignature: { style: "named", bodyStyle: "spread" },
281
+ };
282
+ return { name, fn, meta };
163
283
  };
164
- /** Generate all CRUD functions for an entity */
165
- const generateCrudFunctions = (ctx) => [
166
- generateFindById(ctx),
167
- generateFindMany(ctx),
168
- generateInsert(ctx),
169
- generateDelete(ctx),
170
- ].filter((s) => s != null);
284
+ /** Generate all CRUD methods for an entity */
285
+ const generateCrudMethods = (ctx) => [generateFindById(ctx), generateFindMany(ctx), generateInsert(ctx), generateDelete(ctx)].filter((m) => m != null);
171
286
  // ============================================================================
172
287
  // Index-based Lookup Functions
173
288
  // ============================================================================
@@ -178,27 +293,22 @@ const shouldGenerateLookup = (index) => !index.isPartial &&
178
293
  index.method !== "gin" &&
179
294
  index.method !== "gist";
180
295
  /**
181
- * Generate a function name for an index-based lookup.
182
- * Uses semantic naming when the column corresponds to an FK relation.
296
+ * Generate the method name portion for an index-based lookup.
297
+ * Returns PascalCase like "GetByUsername" or "GetsByUser" for use with exportName.
183
298
  */
184
- const generateLookupName = (entity, index, relation) => {
299
+ const generateLookupMethodName = (index, relation, columnName) => {
185
300
  const isUnique = index.isUnique || index.isPrimary;
186
- const entityName = isUnique
187
- ? entity.name.replace(/s$/, "") // singular for unique
188
- : entity.name.replace(/s$/, "") + "s"; // plural for non-unique
301
+ const prefix = isUnique ? "GetBy" : "GetsBy";
189
302
  // Use semantic name if FK relation exists, otherwise fall back to column name
190
- const columnName = index.columnNames[0];
191
- const byName = relation
192
- ? deriveSemanticName(relation, columnName)
193
- : index.columns[0];
194
- return `get${entityName}By${toPascalCase(byName)}`;
303
+ const byName = relation ? deriveSemanticName(relation, columnName) : index.columns[0];
304
+ return `${prefix}${toPascalCase(byName)}`;
195
305
  };
196
306
  /**
197
- * Generate a lookup function for a single-column index.
307
+ * Generate a lookup method for a single-column index.
198
308
  * Uses semantic parameter naming when the column corresponds to an FK relation.
199
309
  */
200
- const generateLookupFunction = (index, ctx) => {
201
- const { entity, sqlStyle } = ctx;
310
+ const generateLookupMethod = (index, ctx) => {
311
+ const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
202
312
  const rowType = entity.shapes.row.name;
203
313
  const columnName = index.columnNames[0];
204
314
  const field = findRowField(entity, columnName);
@@ -207,105 +317,621 @@ const generateLookupFunction = (index, ctx) => {
207
317
  // Check if this index column corresponds to an FK relation
208
318
  const relation = findRelationForColumn(entity, columnName);
209
319
  // Use semantic param name if FK relation exists, otherwise use field name
210
- const paramName = relation
211
- ? deriveSemanticName(relation, columnName)
212
- : fieldName;
320
+ const paramName = relation ? deriveSemanticName(relation, columnName) : fieldName;
213
321
  // For semantic naming, use indexed access type (Post["userId"])
214
322
  // For regular naming, use Pick<Post, "fieldName">
215
323
  const useSemanticNaming = relation !== undefined && paramName !== fieldName;
324
+ const selectClause = buildSelectClause(entity, explicitColumns);
216
325
  const parts = {
217
- templateParts: [`select * from ${entity.schemaName}.${entity.pgName} where ${columnName} = `, ""],
326
+ templateParts: [
327
+ `${selectClause} from ${entity.schemaName}.${entity.pgName} where ${columnName} = `,
328
+ "",
329
+ ],
218
330
  params: [b.identifier(paramName)],
219
331
  };
220
- const fnName = generateLookupName(entity, index, relation);
221
- // Build the parameter - use indexed access type for semantic naming
222
- const paramNode = useSemanticNaming
223
- ? param.typed(paramName, ts.indexedAccess(ts.ref(rowType), ts.literal(fieldName)))
224
- : param.pick([fieldName], rowType);
332
+ const methodName = generateLookupMethodName(index, relation, columnName);
333
+ const name = exportName(entityName, methodName);
334
+ // Build the parameter - use destructured style for both cases
335
+ // Lookup params must be non-nullable (you're searching FOR a value, not handling null)
336
+ // Semantic naming: { user }: { user: NonNullable<Post["user_id"]> }
337
+ // Regular naming: { fieldName }: { fieldName: NonNullable<Post["fieldName"]> }
338
+ const indexedType = ts.indexedAccess(ts.ref(rowType), ts.literal(fieldName));
339
+ const paramType = ts.ref("NonNullable", [indexedType]);
340
+ const paramNode = param.destructured([{ name: paramName, type: paramType }]);
341
+ // Build metadata for the lookup method
342
+ const meta = {
343
+ name,
344
+ kind: "lookup",
345
+ params: [
346
+ {
347
+ name: paramName,
348
+ type: getFieldTypeString(field, ctx),
349
+ required: true,
350
+ columnName,
351
+ source: relation ? "fk" : "lookup",
352
+ },
353
+ ],
354
+ returns: {
355
+ type: rowType,
356
+ nullable: isUnique,
357
+ isArray: !isUnique,
358
+ },
359
+ lookupField: fieldName,
360
+ isUniqueLookup: isUnique,
361
+ callSignature: { style: "named" },
362
+ };
225
363
  if (isUnique) {
226
364
  // Extract first row for unique lookups
227
365
  const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
228
366
  const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
229
- return hex.exportFn(hex.asyncFn(fnName, [paramNode], [
230
- varDecl,
231
- b.returnStatement(b.identifier("result")),
232
- ]));
367
+ const fn = asyncFn(name, [paramNode], [varDecl, b.returnStatement(b.identifier("result"))]);
368
+ return { name, fn, meta };
233
369
  }
234
370
  // Non-unique: return all matching rows
235
- return hex.exportFn(hex.asyncFn(fnName, [paramNode], hex.returnQuery(sqlStyle, parts, ts.array(ts.ref(rowType)))));
371
+ const fn = asyncFn(name, [paramNode], hex.returnQuery(sqlStyle, parts, ts.array(ts.ref(rowType))));
372
+ return { name, fn, meta };
236
373
  };
237
- /** Generate lookup functions for all eligible indexes, deduplicating by name */
238
- const generateLookupFunctions = (ctx) => {
374
+ /** Generate lookup methods for all eligible indexes, deduplicating by name */
375
+ const generateLookupMethods = (ctx) => {
239
376
  const seen = new Set();
240
377
  return ctx.entity.indexes
241
378
  .filter(index => shouldGenerateLookup(index) && !index.isPrimary)
242
379
  .filter(index => {
243
380
  const columnName = index.columnNames[0];
244
381
  const relation = findRelationForColumn(ctx.entity, columnName);
245
- const name = generateLookupName(ctx.entity, index, relation);
382
+ const methodName = generateLookupMethodName(index, relation, columnName);
383
+ const name = ctx.exportName(ctx.entityName, methodName);
246
384
  if (seen.has(name))
247
385
  return false;
248
386
  seen.add(name);
249
387
  return true;
250
388
  })
251
- .map(index => generateLookupFunction(index, ctx));
389
+ .map(index => generateLookupMethod(index, ctx));
252
390
  };
253
391
  // ============================================================================
254
- // Plugin Definition
392
+ // Function Wrapper Generation
255
393
  // ============================================================================
256
- export const sqlQueriesPlugin = definePlugin({
257
- name: "sql-queries",
258
- provides: ["queries", "queries:sql"],
259
- requires: ["types"],
260
- configSchema: SqlQueriesPluginConfig,
261
- inflection: {
262
- outputFile: ctx => `${ctx.entityName}.ts`,
263
- symbolName: (entityName, artifactKind) => `${entityName}${artifactKind}`,
264
- },
265
- run: (ctx, config) => {
266
- const enums = getEnumEntities(ctx.ir);
267
- const { sqlStyle } = config;
268
- getTableEntities(ctx.ir)
269
- .filter(entity => entity.tags.omit !== true)
270
- .forEach(entity => {
271
- const genCtx = { entity, enums, ir: ctx.ir, sqlStyle };
272
- const statements = [...generateCrudFunctions(genCtx), ...generateLookupFunctions(genCtx)];
273
- if (statements.length === 0)
274
- return;
275
- const entityName = ctx.inflection.entityName(entity.pgClass, entity.tags);
276
- const fileNameCtx = {
277
- entityName,
278
- pgName: entity.pgName,
279
- schema: entity.schemaName,
280
- inflection: ctx.inflection,
281
- entity,
282
- };
283
- const filePath = `${config.outputDir}/${ctx.pluginInflection.outputFile(fileNameCtx)}`;
284
- const file = ctx.file(filePath);
285
- // Import the appropriate SQL client based on style
286
- if (sqlStyle === "tag") {
287
- file.import({ kind: "relative", names: ["sql"], from: "../db" });
394
+ /**
395
+ * Map PostgreSQL type names to TypeScript types.
396
+ * Simplified version - covers common scalar types.
397
+ */
398
+ const pgTypeNameToTs = (typeName) => {
399
+ const typeMap = {
400
+ // Numeric
401
+ int2: "number",
402
+ int4: "number",
403
+ int8: "string", // bigint as string
404
+ float4: "number",
405
+ float8: "number",
406
+ numeric: "string",
407
+ decimal: "string",
408
+ // Text
409
+ text: "string",
410
+ varchar: "string",
411
+ char: "string",
412
+ citext: "string",
413
+ name: "string",
414
+ // Boolean
415
+ bool: "boolean",
416
+ // Date/time
417
+ date: "Date",
418
+ timestamp: "Date",
419
+ timestamptz: "Date",
420
+ time: "string",
421
+ timetz: "string",
422
+ interval: "string",
423
+ // UUID
424
+ uuid: "string",
425
+ // JSON
426
+ json: "unknown",
427
+ jsonb: "unknown",
428
+ // Binary
429
+ bytea: "Buffer",
430
+ // Other
431
+ void: "void",
432
+ };
433
+ return typeMap[typeName] ?? "unknown";
434
+ };
435
+ /**
436
+ * Check if a function argument has a row type (composite type matching a table).
437
+ * Functions with row-type arguments are computed fields, not standalone functions.
438
+ */
439
+ const hasRowTypeArg = (arg, ir) => {
440
+ const tables = getTableEntities(ir);
441
+ return tables.some(t => {
442
+ const qualifiedName = `${t.schemaName}.${t.pgName}`;
443
+ return arg.typeName === qualifiedName || arg.typeName === t.pgName;
444
+ });
445
+ };
446
+ /**
447
+ * Check if a function can be wrapped (not a trigger, computed field, etc.)
448
+ */
449
+ const isGeneratableFunction = (fn, ir) => {
450
+ if (!fn.canExecute)
451
+ return false;
452
+ if (fn.returnTypeName === "trigger")
453
+ return false;
454
+ if (fn.isFromExtension)
455
+ return false;
456
+ if (fn.tags.omit === true)
457
+ return false;
458
+ // Filter out computed field functions (have row-type args)
459
+ if (fn.args.some(arg => hasRowTypeArg(arg, ir)))
460
+ return false;
461
+ return true;
462
+ };
463
+ /**
464
+ * Categorize functions by volatility.
465
+ * Volatile functions go in mutations namespace, stable/immutable in queries.
466
+ */
467
+ const categorizeFunction = (fn) => fn.volatility === "volatile" ? "mutations" : "queries";
468
+ /**
469
+ * Get all generatable functions from the IR, categorized by volatility.
470
+ */
471
+ const getGeneratableFunctions = (ir) => {
472
+ const all = getFunctionEntities(ir).filter(fn => isGeneratableFunction(fn, ir));
473
+ return {
474
+ queries: all.filter(fn => categorizeFunction(fn) === "queries"),
475
+ mutations: all.filter(fn => categorizeFunction(fn) === "mutations"),
476
+ };
477
+ };
478
+ /**
479
+ * Resolve a function's return type to TypeScript type information.
480
+ */
481
+ const resolveReturnType = (fn, ir) => {
482
+ const returnTypeName = fn.returnTypeName;
483
+ const isArray = fn.returnsSet;
484
+ // 1. Check if it's a table return type
485
+ const tableEntities = getTableEntities(ir);
486
+ const tableMatch = tableEntities.find(entity => {
487
+ const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
488
+ return returnTypeName === qualifiedName || returnTypeName === entity.pgName;
489
+ });
490
+ if (tableMatch) {
491
+ return {
492
+ tsType: tableMatch.name,
493
+ isArray,
494
+ isScalar: false,
495
+ needsImport: tableMatch.name,
496
+ returnEntity: tableMatch,
497
+ };
498
+ }
499
+ // 2. Check if it's a composite type return
500
+ const compositeEntities = getCompositeEntities(ir);
501
+ const compositeMatch = compositeEntities.find(entity => {
502
+ const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
503
+ return returnTypeName === qualifiedName || returnTypeName === entity.pgName;
504
+ });
505
+ if (compositeMatch) {
506
+ return {
507
+ tsType: compositeMatch.name,
508
+ isArray,
509
+ isScalar: false,
510
+ needsImport: compositeMatch.name,
511
+ returnEntity: compositeMatch,
512
+ };
513
+ }
514
+ // 3. It's a scalar type - map via type name
515
+ const baseTypeName = returnTypeName.includes(".")
516
+ ? returnTypeName.split(".").pop()
517
+ : returnTypeName;
518
+ const tsType = pgTypeNameToTs(baseTypeName);
519
+ return {
520
+ tsType,
521
+ isArray,
522
+ isScalar: true,
523
+ };
524
+ };
525
+ /**
526
+ * Resolve a function argument to TypeScript type information.
527
+ */
528
+ const resolveArg = (arg, ir) => {
529
+ const typeName = arg.typeName;
530
+ // Check if it's an array type (ends with [])
531
+ const isArrayType = typeName.endsWith("[]");
532
+ const baseTypeName = isArrayType ? typeName.slice(0, -2) : typeName;
533
+ // Check enums
534
+ const enums = getEnumEntities(ir);
535
+ const enumMatch = enums.find(e => {
536
+ const qualifiedName = `${e.schemaName}.${e.pgName}`;
537
+ return baseTypeName === qualifiedName || baseTypeName === e.pgName;
538
+ });
539
+ if (enumMatch) {
540
+ const tsType = isArrayType ? `${enumMatch.name}[]` : enumMatch.name;
541
+ return {
542
+ name: arg.name || "arg",
543
+ tsType,
544
+ isOptional: arg.hasDefault,
545
+ needsImport: enumMatch.name,
546
+ };
547
+ }
548
+ // Check composites
549
+ const composites = getCompositeEntities(ir);
550
+ const compositeMatch = composites.find(e => {
551
+ const qualifiedName = `${e.schemaName}.${e.pgName}`;
552
+ return baseTypeName === qualifiedName || baseTypeName === e.pgName;
553
+ });
554
+ if (compositeMatch) {
555
+ const tsType = isArrayType ? `${compositeMatch.name}[]` : compositeMatch.name;
556
+ return {
557
+ name: arg.name || "arg",
558
+ tsType,
559
+ isOptional: arg.hasDefault,
560
+ needsImport: compositeMatch.name,
561
+ };
562
+ }
563
+ // Scalar type - map via type name
564
+ const scalarBase = baseTypeName.includes(".") ? baseTypeName.split(".").pop() : baseTypeName;
565
+ const scalarTs = pgTypeNameToTs(scalarBase);
566
+ const tsType = isArrayType ? `${scalarTs}[]` : scalarTs;
567
+ return {
568
+ name: arg.name || "arg",
569
+ tsType,
570
+ isOptional: arg.hasDefault,
571
+ };
572
+ };
573
+ /**
574
+ * Resolve all arguments for a function.
575
+ */
576
+ const resolveArgs = (fn, ir) => fn.args.map(arg => resolveArg(arg, ir));
577
+ /**
578
+ * Get the fully qualified function name for SQL.
579
+ */
580
+ const getFunctionQualifiedName = (fn) => `${fn.schemaName}.${fn.pgName}`;
581
+ /**
582
+ * Generate a function wrapper for a PostgreSQL function.
583
+ *
584
+ * Patterns:
585
+ * - SETOF/table return: select * from schema.fn(args)
586
+ * - Single row return: select * from schema.fn(args) (same SQL, single result)
587
+ * - Scalar return: select schema.fn(args)
588
+ */
589
+ const generateFunctionWrapper = (fn, ir, sqlStyle) => {
590
+ const resolvedReturn = resolveReturnType(fn, ir);
591
+ const resolvedArgs = resolveArgs(fn, ir);
592
+ const qualifiedName = getFunctionQualifiedName(fn);
593
+ // Use fn.name which is already inflected by the IR builder
594
+ const name = fn.name;
595
+ // Helper to convert resolved type string to AST
596
+ const typeStrToAst = (typeStr) => {
597
+ if (typeStr.endsWith("[]")) {
598
+ const elemType = typeStr.slice(0, -2);
599
+ return ts.array(typeStrToAst(elemType));
600
+ }
601
+ switch (typeStr) {
602
+ case "string":
603
+ return ts.string();
604
+ case "number":
605
+ return ts.number();
606
+ case "boolean":
607
+ return ts.boolean();
608
+ case "void":
609
+ return ts.void();
610
+ case "unknown":
611
+ return ts.unknown();
612
+ case "Date":
613
+ return ts.ref("Date");
614
+ case "Buffer":
615
+ return ts.ref("Buffer");
616
+ default:
617
+ return ts.ref(typeStr);
618
+ }
619
+ };
620
+ // Build parameter: destructured object for named style (zero-arg functions have no params)
621
+ const params = resolvedArgs.length === 0
622
+ ? []
623
+ : [
624
+ param.destructured(resolvedArgs.map(arg => ({
625
+ name: arg.name,
626
+ type: typeStrToAst(arg.tsType),
627
+ optional: arg.isOptional,
628
+ }))),
629
+ ];
630
+ // Build SQL based on return type
631
+ let sql;
632
+ let resultType;
633
+ if (resolvedReturn.isScalar) {
634
+ // Scalar: select schema.fn(args)
635
+ const argPlaceholders = resolvedArgs.map((_, i) => `$${i + 1}`).join(", ");
636
+ sql = `select ${qualifiedName}(${argPlaceholders})`;
637
+ // Return type is a record with the function name as key
638
+ resultType = ts.array(ts.ref("Record", [ts.string(), typeStrToAst(resolvedReturn.tsType)]));
639
+ }
640
+ else {
641
+ // Table/composite: select * from schema.fn(args)
642
+ const argPlaceholders = resolvedArgs.map((_, i) => `$${i + 1}`).join(", ");
643
+ sql = `select * from ${qualifiedName}(${argPlaceholders})`;
644
+ resultType = ts.array(ts.ref(resolvedReturn.tsType));
645
+ }
646
+ const paramExprs = resolvedArgs.map(arg => b.identifier(arg.name));
647
+ // Build template parts by splitting on $N placeholders
648
+ let templateParts = sql.split(/\$\d+/);
649
+ // For zero-arg functions, template is just the SQL string
650
+ if (resolvedArgs.length === 0) {
651
+ templateParts = [sql];
652
+ }
653
+ const parts = {
654
+ templateParts,
655
+ params: paramExprs,
656
+ };
657
+ // Build the function body
658
+ let body;
659
+ if (resolvedReturn.isScalar) {
660
+ // Scalar: extract the result from the first row's first column
661
+ const queryExpr = hex.query(sqlStyle, parts, resultType);
662
+ const varDecl = hex.firstRowDecl(sqlStyle, "row", queryExpr);
663
+ // Use optional chaining: row?.[fn.pgName]
664
+ const optionalReturn = b.optionalMemberExpression(b.identifier("row"), b.identifier(fn.pgName), false, true);
665
+ body = [varDecl, b.returnStatement(optionalReturn)];
666
+ }
667
+ else if (resolvedReturn.isArray) {
668
+ // SETOF: return all rows
669
+ body = hex.returnQuery(sqlStyle, parts, resultType);
670
+ }
671
+ else {
672
+ // Single row: extract first row
673
+ const queryExpr = hex.query(sqlStyle, parts, resultType);
674
+ const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
675
+ body = [varDecl, b.returnStatement(b.identifier("result"))];
676
+ }
677
+ const fnDecl = asyncFn(name, params, body);
678
+ // Build metadata for the function wrapper
679
+ const meta = {
680
+ name,
681
+ kind: "function",
682
+ params: resolvedArgs.map(arg => ({
683
+ name: arg.name,
684
+ type: arg.tsType,
685
+ required: !arg.isOptional,
686
+ })),
687
+ returns: {
688
+ type: resolvedReturn.tsType,
689
+ nullable: resolvedReturn.isScalar || !resolvedReturn.isArray, // Scalars and single rows can be null
690
+ isArray: resolvedReturn.isArray,
691
+ },
692
+ callSignature: { style: "named" },
693
+ };
694
+ return { name, fn: fnDecl, meta };
695
+ };
696
+ /**
697
+ * Collect type imports needed for function wrappers.
698
+ */
699
+ const collectFunctionTypeImports = (functions, ir) => {
700
+ const imports = new Set();
701
+ for (const fn of functions) {
702
+ const resolvedReturn = resolveReturnType(fn, ir);
703
+ if (resolvedReturn.needsImport) {
704
+ imports.add(resolvedReturn.needsImport);
705
+ }
706
+ for (const arg of resolveArgs(fn, ir)) {
707
+ if (arg.needsImport) {
708
+ imports.add(arg.needsImport);
288
709
  }
289
- else {
290
- file.import({ kind: "relative", names: ["pool"], from: "../db" });
710
+ }
711
+ }
712
+ return imports;
713
+ };
714
+ // ============================================================================
715
+ // Export Style Helpers
716
+ // ============================================================================
717
+ /**
718
+ * Convert MethodDef array to flat export statements.
719
+ * Each method becomes: export function methodName(...) { ... }
720
+ */
721
+ const toFlatExports = (methods) => methods.map(m => conjure.export.fn(m.fn));
722
+ /**
723
+ * Convert a FunctionDeclaration to a FunctionExpression for object property use.
724
+ */
725
+ const fnDeclToExpr = (fn) => {
726
+ const expr = b.functionExpression(null, fn.params, fn.body);
727
+ expr.async = fn.async;
728
+ expr.generator = fn.generator;
729
+ return expr;
730
+ };
731
+ /**
732
+ * Convert MethodDef array to a single namespace object export.
733
+ * All methods become: export const EntityName = { methodName: async function(...) { ... }, ... }
734
+ */
735
+ const toNamespaceExport = (entityName, methods) => {
736
+ const properties = methods.map(m => b.objectProperty(b.identifier(m.name), fnDeclToExpr(m.fn)));
737
+ const obj = b.objectExpression(properties);
738
+ return conjure.export.const(entityName, obj);
739
+ };
740
+ /**
741
+ * Convert MethodDef array to statements based on export style.
742
+ */
743
+ const toStatements = (methods, exportStyle, entityName) => {
744
+ if (methods.length === 0)
745
+ return [];
746
+ return exportStyle === "namespace"
747
+ ? [toNamespaceExport(entityName, methods)]
748
+ : toFlatExports(methods);
749
+ };
750
+ /**
751
+ * Create a SQL queries provider that generates raw SQL query functions.
752
+ *
753
+ * @example
754
+ * ```typescript
755
+ * import { sqlQueries } from "pg-sourcerer"
756
+ *
757
+ * export default defineConfig({
758
+ * plugins: [
759
+ * types(),
760
+ * sqlQueries({ header: 'import { sql } from "../db"' }),
761
+ * ],
762
+ * })
763
+ * ```
764
+ */
765
+ export function sqlQueries(config) {
766
+ const parsed = S.decodeUnknownSync(SqlQueriesPluginConfigSchema)(config);
767
+ // Resolve config with properly typed exportName
768
+ const resolvedConfig = {
769
+ ...parsed,
770
+ exportName: config.exportName ?? defaultExportName,
771
+ };
772
+ return definePlugin({
773
+ name: "sql-queries",
774
+ kind: "queries",
775
+ singleton: true,
776
+ canProvide: () => true,
777
+ provide: (_params, _deps, ctx) => {
778
+ const { ir, inflection } = ctx;
779
+ const enums = getEnumEntities(ir);
780
+ const { sqlStyle, generateFunctions, exportName, exportStyle, outputDir, header, functionsFile, explicitColumns, } = resolvedConfig;
781
+ // Pre-compute function groupings by return entity name
782
+ // Functions returning entities go in that entity's file; scalars go in functions.ts
783
+ const functionsByEntity = new Map();
784
+ const scalarFunctions = [];
785
+ if (generateFunctions) {
786
+ const { queries, mutations } = getGeneratableFunctions(ir);
787
+ const allFunctions = [...queries, ...mutations];
788
+ for (const fn of allFunctions) {
789
+ const resolved = resolveReturnType(fn, ir);
790
+ if (resolved.returnEntity) {
791
+ const entityName = resolved.returnEntity.name;
792
+ const existing = functionsByEntity.get(entityName) ?? [];
793
+ functionsByEntity.set(entityName, [...existing, fn]);
794
+ }
795
+ else {
796
+ scalarFunctions.push(fn);
797
+ }
798
+ }
291
799
  }
292
- file.import({
293
- kind: "symbol",
294
- ref: { capability: "types", entity: entity.name, shape: "row" },
800
+ getTableEntities(ir)
801
+ .filter(entity => entity.tags.omit !== true)
802
+ .forEach(entity => {
803
+ const entityName = inflection.entityName(entity.pgClass, entity.tags);
804
+ const genCtx = {
805
+ entity,
806
+ enums,
807
+ ir,
808
+ sqlStyle,
809
+ entityName,
810
+ exportName,
811
+ explicitColumns,
812
+ };
813
+ // Generate CRUD and lookup methods
814
+ const crudMethods = [...generateCrudMethods(genCtx), ...generateLookupMethods(genCtx)];
815
+ // Get functions that return this entity
816
+ const entityFunctions = functionsByEntity.get(entity.name) ?? [];
817
+ if (crudMethods.length === 0 && entityFunctions.length === 0)
818
+ return;
819
+ const filePath = `${outputDir}/${entityName}.ts`;
820
+ // Convert methods to statements based on export style
821
+ const statements = toStatements(crudMethods, exportStyle, entityName);
822
+ // Add function wrappers (these are always flat exports for now)
823
+ for (const fn of entityFunctions) {
824
+ const wrapper = generateFunctionWrapper(fn, ir, sqlStyle);
825
+ statements.push(conjure.export.fn(wrapper.fn));
826
+ }
827
+ const file = ctx.file(filePath);
828
+ // Add user-provided header (must include SQL client import)
829
+ file.header(header);
830
+ file.import({
831
+ kind: "symbol",
832
+ ref: { capability: "types", entity: entity.name, shape: "row" },
833
+ });
834
+ // Import insert type if insert function is generated
835
+ if (entity.permissions.canInsert) {
836
+ const insertShape = entity.shapes.insert ?? entity.shapes.row;
837
+ // Only import if it's a different type than row
838
+ if (insertShape !== entity.shapes.row) {
839
+ file.import({
840
+ kind: "symbol",
841
+ ref: { capability: "types", entity: entity.name, shape: "insert" },
842
+ });
843
+ }
844
+ }
845
+ // Import types needed by function args (for functions grouped into this file)
846
+ if (entityFunctions.length > 0) {
847
+ const fnTypeImports = collectFunctionTypeImports(entityFunctions, ir);
848
+ // Remove the entity's own type (already in scope)
849
+ fnTypeImports.delete(entity.name);
850
+ for (const typeName of fnTypeImports) {
851
+ file.import({
852
+ kind: "symbol",
853
+ ref: { capability: "types", entity: typeName },
854
+ });
855
+ }
856
+ }
857
+ file.ast(conjure.program(...statements)).emit();
858
+ // Collect metadata for QueryArtifact
859
+ const pkField = entity.primaryKey?.columns[0]
860
+ ? findRowField(entity, entity.primaryKey.columns[0])
861
+ : undefined;
862
+ const pkType = pkField ? getFieldTypeString(pkField, genCtx) : undefined;
863
+ // Combine CRUD method metadata with entity-function metadata
864
+ const allMethodMetas = [
865
+ ...crudMethods.map(m => m.meta),
866
+ ...entityFunctions.map(fn => generateFunctionWrapper(fn, ir, sqlStyle).meta),
867
+ ];
868
+ // Register entity methods to symbol registry for HTTP providers
869
+ ctx.symbols.registerEntityMethods({
870
+ entity: entityName,
871
+ importPath: filePath,
872
+ pkType,
873
+ hasCompositePk: (entity.primaryKey?.columns.length ?? 0) > 1,
874
+ methods: allMethodMetas.map(m => ({
875
+ name: m.name,
876
+ file: filePath,
877
+ entity: entityName,
878
+ kind: m.kind,
879
+ params: m.params,
880
+ returns: m.returns,
881
+ lookupField: m.lookupField,
882
+ isUniqueLookup: m.isUniqueLookup,
883
+ callSignature: m.callSignature,
884
+ })),
885
+ }, "sql-queries");
295
886
  });
296
- // Import insert type if insert function is generated
297
- if (entity.permissions.canInsert) {
298
- const insertShape = entity.shapes.insert ?? entity.shapes.row;
299
- // Only import if it's a different type than row
300
- if (insertShape !== entity.shapes.row) {
887
+ // Generate files for composite types that have functions returning them
888
+ if (generateFunctions) {
889
+ const composites = getCompositeEntities(ir);
890
+ for (const composite of composites) {
891
+ const compositeFunctions = functionsByEntity.get(composite.name) ?? [];
892
+ if (compositeFunctions.length === 0)
893
+ continue;
894
+ const filePath = `${outputDir}/${composite.name}.ts`;
895
+ const methods = compositeFunctions.map(fn => generateFunctionWrapper(fn, ir, sqlStyle));
896
+ // Function wrappers are always flat exports
897
+ const statements = methods.map(m => conjure.export.fn(m.fn));
898
+ const file = ctx.file(filePath);
899
+ // Add user-provided header (must include SQL client import)
900
+ file.header(header);
901
+ // Import the composite type and any types needed by function args
902
+ const fnTypeImports = collectFunctionTypeImports(compositeFunctions, ir);
903
+ fnTypeImports.add(composite.name); // Always import the composite type
904
+ for (const typeName of fnTypeImports) {
905
+ file.import({
906
+ kind: "symbol",
907
+ ref: { capability: "types", entity: typeName },
908
+ });
909
+ }
910
+ file.ast(conjure.program(...statements)).emit();
911
+ }
912
+ }
913
+ // Generate functions.ts for scalar-returning functions only
914
+ if (generateFunctions && scalarFunctions.length > 0) {
915
+ const filePath = `${outputDir}/${functionsFile}`;
916
+ const methods = scalarFunctions.map(fn => generateFunctionWrapper(fn, ir, sqlStyle));
917
+ // Function wrappers are always flat exports
918
+ const statements = methods.map(m => conjure.export.fn(m.fn));
919
+ const file = ctx.file(filePath);
920
+ // Add user-provided header (must include SQL client import)
921
+ file.header(header);
922
+ // Import types needed by function args
923
+ const fnTypeImports = collectFunctionTypeImports(scalarFunctions, ir);
924
+ for (const typeName of fnTypeImports) {
301
925
  file.import({
302
926
  kind: "symbol",
303
- ref: { capability: "types", entity: entity.name, shape: "insert" },
927
+ ref: { capability: "types", entity: typeName },
304
928
  });
305
929
  }
930
+ file.ast(conjure.program(...statements)).emit();
931
+ // TODO: Register standalone functions to symbol registry when HTTP plugins need them
932
+ // For now, standalone functions are not exposed via routes
306
933
  }
307
- file.ast(conjure.program(...statements)).emit();
308
- });
309
- },
310
- });
934
+ },
935
+ });
936
+ }
311
937
  //# sourceMappingURL=sql-queries.js.map