@danielfgray/pg-sourcerer 0.2.2 → 0.4.0

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 (307) hide show
  1. package/bin/pgsourcerer +2 -0
  2. package/dist/__tests__/fixtures/index.d.ts +15 -0
  3. package/dist/__tests__/fixtures/index.d.ts.map +1 -0
  4. package/dist/__tests__/fixtures/index.js +19 -0
  5. package/dist/__tests__/fixtures/index.js.map +1 -0
  6. package/dist/__tests__/fixtures/introspection.json +40522 -0
  7. package/dist/cli.d.ts +0 -1
  8. package/dist/cli.js +7 -46
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +38 -5
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +13 -2
  13. package/dist/config.js.map +1 -1
  14. package/dist/{lib/conjure.d.ts → conjure/index.d.ts} +62 -3
  15. package/dist/conjure/index.d.ts.map +1 -0
  16. package/dist/{lib/conjure.js → conjure/index.js} +124 -3
  17. package/dist/conjure/index.js.map +1 -0
  18. package/dist/conjure/signature.d.ts +85 -0
  19. package/dist/conjure/signature.d.ts.map +1 -0
  20. package/dist/conjure/signature.js +130 -0
  21. package/dist/conjure/signature.js.map +1 -0
  22. package/dist/conjure/types.d.ts +97 -0
  23. package/dist/conjure/types.d.ts.map +1 -0
  24. package/dist/conjure/types.js +206 -0
  25. package/dist/conjure/types.js.map +1 -0
  26. package/dist/errors.d.ts +114 -139
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js +82 -36
  29. package/dist/errors.js.map +1 -1
  30. package/dist/generate.d.ts +45 -46
  31. package/dist/generate.d.ts.map +1 -1
  32. package/dist/generate.js +86 -59
  33. package/dist/generate.js.map +1 -1
  34. package/dist/hex/builder.d.ts +12 -0
  35. package/dist/hex/builder.d.ts.map +1 -0
  36. package/dist/hex/builder.js +64 -0
  37. package/dist/hex/builder.js.map +1 -0
  38. package/dist/hex/ddl.d.ts +53 -0
  39. package/dist/hex/ddl.d.ts.map +1 -0
  40. package/dist/hex/ddl.js +306 -0
  41. package/dist/hex/ddl.js.map +1 -0
  42. package/dist/hex/index.d.ts +105 -0
  43. package/dist/hex/index.d.ts.map +1 -0
  44. package/dist/hex/index.js +81 -0
  45. package/dist/hex/index.js.map +1 -0
  46. package/dist/hex/primitives.d.ts +23 -0
  47. package/dist/hex/primitives.d.ts.map +1 -0
  48. package/dist/hex/primitives.js +38 -0
  49. package/dist/hex/primitives.js.map +1 -0
  50. package/dist/hex/query.d.ts +116 -0
  51. package/dist/hex/query.d.ts.map +1 -0
  52. package/dist/hex/query.js +219 -0
  53. package/dist/hex/query.js.map +1 -0
  54. package/dist/hex/types.d.ts +287 -0
  55. package/dist/hex/types.d.ts.map +1 -0
  56. package/dist/hex/types.js +431 -0
  57. package/dist/hex/types.js.map +1 -0
  58. package/dist/index.d.ts +17 -25
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +33 -44
  61. package/dist/index.js.map +1 -1
  62. package/dist/init.d.ts.map +1 -1
  63. package/dist/init.js +76 -140
  64. package/dist/init.js.map +1 -1
  65. package/dist/ir/extensions/queries.d.ts +6 -6
  66. package/dist/ir/extensions/queries.d.ts.map +1 -1
  67. package/dist/ir/extensions/queries.js +6 -4
  68. package/dist/ir/extensions/queries.js.map +1 -1
  69. package/dist/ir/extensions/schema-builder.d.ts.map +1 -1
  70. package/dist/ir/extensions/schema-builder.js.map +1 -1
  71. package/dist/ir/index.d.ts.map +1 -1
  72. package/dist/ir/index.js.map +1 -1
  73. package/dist/ir/relation-graph.d.ts.map +1 -1
  74. package/dist/ir/relation-graph.js +8 -8
  75. package/dist/ir/relation-graph.js.map +1 -1
  76. package/dist/ir/semantic-ir.d.ts +38 -0
  77. package/dist/ir/semantic-ir.d.ts.map +1 -1
  78. package/dist/ir/semantic-ir.js +50 -2
  79. package/dist/ir/semantic-ir.js.map +1 -1
  80. package/dist/ir/smart-tags.d.ts.map +1 -1
  81. package/dist/ir/smart-tags.js.map +1 -1
  82. package/dist/lib/field-utils.d.ts.map +1 -1
  83. package/dist/lib/field-utils.js +7 -7
  84. package/dist/lib/field-utils.js.map +1 -1
  85. package/dist/lib/join-graph.d.ts +95 -0
  86. package/dist/lib/join-graph.d.ts.map +1 -0
  87. package/dist/lib/join-graph.js +305 -0
  88. package/dist/lib/join-graph.js.map +1 -0
  89. package/dist/lib/picker.d.ts +60 -0
  90. package/dist/lib/picker.d.ts.map +1 -0
  91. package/dist/lib/picker.js +325 -0
  92. package/dist/lib/picker.js.map +1 -0
  93. package/dist/plugins/arktype.d.ts +20 -24
  94. package/dist/plugins/arktype.d.ts.map +1 -1
  95. package/dist/plugins/arktype.js +462 -386
  96. package/dist/plugins/arktype.js.map +1 -1
  97. package/dist/plugins/effect/http.d.ts +7 -0
  98. package/dist/plugins/effect/http.d.ts.map +1 -0
  99. package/dist/plugins/effect/http.js +460 -0
  100. package/dist/plugins/effect/http.js.map +1 -0
  101. package/dist/plugins/effect/index.d.ts +22 -0
  102. package/dist/plugins/effect/index.d.ts.map +1 -0
  103. package/dist/plugins/effect/index.js +65 -0
  104. package/dist/plugins/effect/index.js.map +1 -0
  105. package/dist/plugins/effect/models.d.ts +6 -0
  106. package/dist/plugins/effect/models.d.ts.map +1 -0
  107. package/dist/plugins/effect/models.js +116 -0
  108. package/dist/plugins/effect/models.js.map +1 -0
  109. package/dist/plugins/effect/repos.d.ts +21 -0
  110. package/dist/plugins/effect/repos.d.ts.map +1 -0
  111. package/dist/plugins/effect/repos.js +131 -0
  112. package/dist/plugins/effect/repos.js.map +1 -0
  113. package/dist/plugins/effect/schemas.d.ts +7 -0
  114. package/dist/plugins/effect/schemas.d.ts.map +1 -0
  115. package/dist/plugins/effect/schemas.js +75 -0
  116. package/dist/plugins/effect/schemas.js.map +1 -0
  117. package/dist/plugins/effect/shared.d.ts +116 -0
  118. package/dist/plugins/effect/shared.d.ts.map +1 -0
  119. package/dist/plugins/effect/shared.js +164 -0
  120. package/dist/plugins/effect/shared.js.map +1 -0
  121. package/dist/plugins/http-elysia.d.ts +20 -27
  122. package/dist/plugins/http-elysia.d.ts.map +1 -1
  123. package/dist/plugins/http-elysia.js +350 -475
  124. package/dist/plugins/http-elysia.js.map +1 -1
  125. package/dist/plugins/http-express.d.ts +20 -31
  126. package/dist/plugins/http-express.d.ts.map +1 -1
  127. package/dist/plugins/http-express.js +281 -268
  128. package/dist/plugins/http-express.js.map +1 -1
  129. package/dist/plugins/http-hono.d.ts +17 -33
  130. package/dist/plugins/http-hono.d.ts.map +1 -1
  131. package/dist/plugins/http-hono.js +317 -341
  132. package/dist/plugins/http-hono.js.map +1 -1
  133. package/dist/plugins/http-orpc.d.ts +34 -33
  134. package/dist/plugins/http-orpc.d.ts.map +1 -1
  135. package/dist/plugins/http-orpc.js +345 -257
  136. package/dist/plugins/http-orpc.js.map +1 -1
  137. package/dist/plugins/http-trpc.d.ts +33 -35
  138. package/dist/plugins/http-trpc.d.ts.map +1 -1
  139. package/dist/plugins/http-trpc.js +337 -241
  140. package/dist/plugins/http-trpc.js.map +1 -1
  141. package/dist/plugins/kysely.d.ts +54 -59
  142. package/dist/plugins/kysely.d.ts.map +1 -1
  143. package/dist/plugins/kysely.js +826 -687
  144. package/dist/plugins/kysely.js.map +1 -1
  145. package/dist/plugins/sql-queries.d.ts +38 -44
  146. package/dist/plugins/sql-queries.d.ts.map +1 -1
  147. package/dist/plugins/sql-queries.js +497 -897
  148. package/dist/plugins/sql-queries.js.map +1 -1
  149. package/dist/plugins/types.d.ts +12 -20
  150. package/dist/plugins/types.d.ts.map +1 -1
  151. package/dist/plugins/types.js +84 -227
  152. package/dist/plugins/types.js.map +1 -1
  153. package/dist/plugins/valibot.d.ts +7 -44
  154. package/dist/plugins/valibot.d.ts.map +1 -1
  155. package/dist/plugins/valibot.js +376 -382
  156. package/dist/plugins/valibot.js.map +1 -1
  157. package/dist/plugins/zod.d.ts +20 -24
  158. package/dist/plugins/zod.d.ts.map +1 -1
  159. package/dist/plugins/zod.js +370 -367
  160. package/dist/plugins/zod.js.map +1 -1
  161. package/dist/runtime/emit.d.ts +64 -0
  162. package/dist/runtime/emit.d.ts.map +1 -0
  163. package/dist/runtime/emit.js +445 -0
  164. package/dist/runtime/emit.js.map +1 -0
  165. package/dist/runtime/errors.d.ts +36 -0
  166. package/dist/runtime/errors.d.ts.map +1 -0
  167. package/dist/runtime/errors.js +29 -0
  168. package/dist/runtime/errors.js.map +1 -0
  169. package/dist/runtime/file-assignment.d.ts +161 -0
  170. package/dist/runtime/file-assignment.d.ts.map +1 -0
  171. package/dist/runtime/file-assignment.js +195 -0
  172. package/dist/runtime/file-assignment.js.map +1 -0
  173. package/dist/runtime/orchestrator.d.ts +62 -0
  174. package/dist/runtime/orchestrator.d.ts.map +1 -0
  175. package/dist/runtime/orchestrator.js +99 -0
  176. package/dist/runtime/orchestrator.js.map +1 -0
  177. package/dist/runtime/registry.d.ts +268 -0
  178. package/dist/runtime/registry.d.ts.map +1 -0
  179. package/dist/runtime/registry.js +436 -0
  180. package/dist/runtime/registry.js.map +1 -0
  181. package/dist/runtime/types.d.ts +182 -0
  182. package/dist/runtime/types.d.ts.map +1 -0
  183. package/dist/runtime/types.js +2 -0
  184. package/dist/runtime/types.js.map +1 -0
  185. package/dist/runtime/validation.d.ts +41 -0
  186. package/dist/runtime/validation.d.ts.map +1 -0
  187. package/dist/runtime/validation.js +70 -0
  188. package/dist/runtime/validation.js.map +1 -0
  189. package/dist/services/config-loader.d.ts.map +1 -1
  190. package/dist/services/config-loader.js +15 -6
  191. package/dist/services/config-loader.js.map +1 -1
  192. package/dist/services/config.d.ts +55 -25
  193. package/dist/services/config.d.ts.map +1 -1
  194. package/dist/services/config.js +60 -34
  195. package/dist/services/config.js.map +1 -1
  196. package/dist/services/file-writer.d.ts +3 -3
  197. package/dist/services/file-writer.d.ts.map +1 -1
  198. package/dist/services/file-writer.js +6 -8
  199. package/dist/services/file-writer.js.map +1 -1
  200. package/dist/services/inflection.d.ts +126 -27
  201. package/dist/services/inflection.d.ts.map +1 -1
  202. package/dist/services/inflection.js +300 -72
  203. package/dist/services/inflection.js.map +1 -1
  204. package/dist/services/introspection.d.ts.map +1 -1
  205. package/dist/services/introspection.js +6 -6
  206. package/dist/services/introspection.js.map +1 -1
  207. package/dist/services/ir-builder.d.ts.map +1 -1
  208. package/dist/services/ir-builder.js +73 -77
  209. package/dist/services/ir-builder.js.map +1 -1
  210. package/dist/services/ir.d.ts.map +1 -1
  211. package/dist/services/ir.js.map +1 -1
  212. package/dist/services/pg-types.d.ts.map +1 -1
  213. package/dist/services/pg-types.js +3 -3
  214. package/dist/services/pg-types.js.map +1 -1
  215. package/dist/services/smart-tags-parser.d.ts.map +1 -1
  216. package/dist/services/smart-tags-parser.js +4 -4
  217. package/dist/services/smart-tags-parser.js.map +1 -1
  218. package/dist/services/type-hints.d.ts.map +1 -1
  219. package/dist/services/type-hints.js +1 -1
  220. package/dist/services/type-hints.js.map +1 -1
  221. package/dist/services/user-module-parser.d.ts +46 -0
  222. package/dist/services/user-module-parser.d.ts.map +1 -0
  223. package/dist/services/user-module-parser.js +181 -0
  224. package/dist/services/user-module-parser.js.map +1 -0
  225. package/dist/shared/converters.d.ts +60 -0
  226. package/dist/shared/converters.d.ts.map +1 -0
  227. package/dist/shared/converters.js +168 -0
  228. package/dist/shared/converters.js.map +1 -0
  229. package/dist/shared/query-types.d.ts +95 -0
  230. package/dist/shared/query-types.d.ts.map +1 -0
  231. package/dist/shared/query-types.js +9 -0
  232. package/dist/shared/query-types.js.map +1 -0
  233. package/dist/testing.d.ts +125 -37
  234. package/dist/testing.d.ts.map +1 -1
  235. package/dist/testing.js +134 -42
  236. package/dist/testing.js.map +1 -1
  237. package/dist/user-module.d.ts +86 -0
  238. package/dist/user-module.d.ts.map +1 -0
  239. package/dist/user-module.js +55 -0
  240. package/dist/user-module.js.map +1 -0
  241. package/package.json +10 -6
  242. package/dist/lib/conjure.d.ts.map +0 -1
  243. package/dist/lib/conjure.js.map +0 -1
  244. package/dist/lib/hex.d.ts +0 -119
  245. package/dist/lib/hex.d.ts.map +0 -1
  246. package/dist/lib/hex.js +0 -188
  247. package/dist/lib/hex.js.map +0 -1
  248. package/dist/plugins/effect.d.ts +0 -53
  249. package/dist/plugins/effect.d.ts.map +0 -1
  250. package/dist/plugins/effect.js +0 -1074
  251. package/dist/plugins/effect.js.map +0 -1
  252. package/dist/plugins/kysely/queries.d.ts +0 -92
  253. package/dist/plugins/kysely/queries.d.ts.map +0 -1
  254. package/dist/plugins/kysely/queries.js +0 -1169
  255. package/dist/plugins/kysely/queries.js.map +0 -1
  256. package/dist/plugins/kysely/shared.d.ts +0 -59
  257. package/dist/plugins/kysely/shared.d.ts.map +0 -1
  258. package/dist/plugins/kysely/shared.js +0 -247
  259. package/dist/plugins/kysely/shared.js.map +0 -1
  260. package/dist/plugins/kysely/types.d.ts +0 -22
  261. package/dist/plugins/kysely/types.d.ts.map +0 -1
  262. package/dist/plugins/kysely/types.js +0 -428
  263. package/dist/plugins/kysely/types.js.map +0 -1
  264. package/dist/services/artifact-store.d.ts +0 -65
  265. package/dist/services/artifact-store.d.ts.map +0 -1
  266. package/dist/services/artifact-store.js +0 -57
  267. package/dist/services/artifact-store.js.map +0 -1
  268. package/dist/services/core-providers.d.ts +0 -15
  269. package/dist/services/core-providers.d.ts.map +0 -1
  270. package/dist/services/core-providers.js +0 -23
  271. package/dist/services/core-providers.js.map +0 -1
  272. package/dist/services/emissions.d.ts +0 -103
  273. package/dist/services/emissions.d.ts.map +0 -1
  274. package/dist/services/emissions.js +0 -241
  275. package/dist/services/emissions.js.map +0 -1
  276. package/dist/services/execution.d.ts +0 -35
  277. package/dist/services/execution.d.ts.map +0 -1
  278. package/dist/services/execution.js +0 -86
  279. package/dist/services/execution.js.map +0 -1
  280. package/dist/services/file-builder.d.ts +0 -85
  281. package/dist/services/file-builder.d.ts.map +0 -1
  282. package/dist/services/file-builder.js +0 -112
  283. package/dist/services/file-builder.js.map +0 -1
  284. package/dist/services/plugin-meta.d.ts +0 -33
  285. package/dist/services/plugin-meta.d.ts.map +0 -1
  286. package/dist/services/plugin-meta.js +0 -24
  287. package/dist/services/plugin-meta.js.map +0 -1
  288. package/dist/services/plugin-runner.d.ts +0 -42
  289. package/dist/services/plugin-runner.d.ts.map +0 -1
  290. package/dist/services/plugin-runner.js +0 -84
  291. package/dist/services/plugin-runner.js.map +0 -1
  292. package/dist/services/plugin.d.ts +0 -421
  293. package/dist/services/plugin.d.ts.map +0 -1
  294. package/dist/services/plugin.js +0 -197
  295. package/dist/services/plugin.js.map +0 -1
  296. package/dist/services/resolution.d.ts +0 -38
  297. package/dist/services/resolution.d.ts.map +0 -1
  298. package/dist/services/resolution.js +0 -242
  299. package/dist/services/resolution.js.map +0 -1
  300. package/dist/services/service-registry.d.ts +0 -74
  301. package/dist/services/service-registry.d.ts.map +0 -1
  302. package/dist/services/service-registry.js +0 -61
  303. package/dist/services/service-registry.js.map +0 -1
  304. package/dist/services/symbols.d.ts +0 -144
  305. package/dist/services/symbols.d.ts.map +0 -1
  306. package/dist/services/symbols.js +0 -144
  307. package/dist/services/symbols.js.map +0 -1
@@ -1,937 +1,537 @@
1
1
  /**
2
- * SQL Queries Provider - Generate raw SQL query functions using template strings
3
- */
4
- import { Schema as S } from "effect";
5
- import { definePlugin } from "../services/plugin.js";
6
- import { getTableEntities, getEnumEntities, getFunctionEntities, getCompositeEntities, } from "../ir/semantic-ir.js";
7
- import { conjure } from "../lib/conjure.js";
8
- import { hex } from "../lib/hex.js";
9
- import { resolveFieldType, tsTypeToAst } from "../lib/field-utils.js";
10
- import { inflect } from "../services/inflection.js";
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({
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,
34
- /** SQL query style. Defaults to "tag" (tagged template literals) */
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" }),
56
- });
57
- /** Find a field in the row shape by column name */
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 *";
63
- /** Get the TypeScript type AST for a field */
64
- const getFieldTypeAst = (field, ctx) => {
65
- if (!field)
66
- return ts.string();
67
- const resolved = resolveFieldType(field, ctx.enums, ctx.ir.extensions);
68
- return resolved.enumDef ? ts.ref(resolved.enumDef.name) : tsTypeToAst(resolved.tsType);
69
- };
70
- // ============================================================================
71
- // FK Semantic Naming Helpers
72
- // ============================================================================
73
- /**
74
- * Find a belongsTo relation that uses the given column as its local FK column.
75
- * For single-column indexes only.
76
- */
77
- const findRelationForColumn = (entity, columnName) => entity.relations.find(r => r.kind === "belongsTo" && r.columns.length === 1 && r.columns[0]?.local === columnName);
78
- /**
79
- * Derive semantic name for an FK-based lookup.
80
- * Priority: @fieldName tag → column minus _id suffix → target entity name
81
- */
82
- const deriveSemanticName = (relation, columnName) => {
83
- // 1. Check for @fieldName smart tag
84
- if (relation.tags.fieldName && typeof relation.tags.fieldName === "string") {
85
- return relation.tags.fieldName;
86
- }
87
- // 2. Strip common FK suffixes from column name
88
- const suffixes = ["_id", "_fk", "Id", "Fk"];
89
- for (const suffix of suffixes) {
90
- if (columnName.endsWith(suffix)) {
91
- const stripped = columnName.slice(0, -suffix.length);
92
- if (stripped.length > 0)
93
- return stripped;
94
- }
95
- }
96
- // 3. Fall back to target entity name (lowercased first char)
97
- const target = relation.targetEntity;
98
- return target.charAt(0).toLowerCase() + target.slice(1);
99
- };
100
- /**
101
- * Capitalize first letter for use in function names
102
- */
103
- /**
104
- * Convert to PascalCase for use in function names.
105
- * Handles snake_case (created_at → CreatedAt) and regular strings.
2
+ * SQL Queries Plugin - Generate raw SQL query functions using template strings
3
+ *
4
+ * Generates SQL query functions with tagged template literals.
5
+ * Uses parameterized queries ($1, $2, etc.) for safety.
106
6
  */
107
- const toPascalCase = (s) => inflect.pascalCase(s);
7
+ import { Effect, Schema as S } from "effect";
8
+ import { normalizeFileNaming } from "../runtime/file-assignment.js";
9
+ import { IR } from "../services/ir.js";
10
+ import { Inflection } from "../services/inflection.js";
11
+ import { getTableEntities, getCursorPaginationCandidates, } from "../ir/semantic-ir.js";
12
+ import { conjure } from "../conjure/index.js";
13
+ const { fn, ts, param, str, b, exp } = conjure;
108
14
  // ============================================================================
109
- // CRUD Function Generators
15
+ // Name Building Helpers
110
16
  // ============================================================================
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 */
119
- const generateFindById = (ctx) => {
120
- const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
121
- if (!entity.primaryKey || !entity.permissions.canSelect)
122
- return undefined;
123
- const pkColName = entity.primaryKey.columns[0];
124
- const pkField = findRowField(entity, pkColName);
125
- if (!pkField)
126
- return undefined;
127
- const rowType = entity.shapes.row.name;
128
- const fieldName = pkField.name; // JS property name (e.g., "id")
129
- const selectClause = buildSelectClause(entity, explicitColumns);
130
- const parts = {
131
- templateParts: [
132
- `${selectClause} from ${entity.schemaName}.${entity.pgName} where ${pkColName} = `,
133
- "",
134
- ],
135
- params: [b.identifier(fieldName)],
136
- };
137
- // Build query and extract first row
138
- const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
139
- const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
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 };
158
- };
159
- /** Generate findMany method with pagination if entity has canSelect permission */
160
- const generateFindMany = (ctx) => {
161
- const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
162
- if (!entity.permissions.canSelect)
163
- return undefined;
164
- const rowType = entity.shapes.row.name;
165
- const selectClause = buildSelectClause(entity, explicitColumns);
166
- const parts = {
167
- templateParts: [
168
- `${selectClause} from ${entity.schemaName}.${entity.pgName} limit `,
169
- ` offset `,
170
- "",
171
- ],
172
- params: [b.identifier("limit"), b.identifier("offset")],
173
- };
174
- const name = exportName(entityName, "FindManys");
175
- const fn = asyncFn(name, [
176
- param.destructured([
177
- { name: "limit", type: ts.number(), optional: true, defaultValue: b.numericLiteral(50) },
178
- { name: "offset", type: ts.number(), optional: true, defaultValue: b.numericLiteral(0) },
179
- ]),
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 };
192
- };
193
- /** Generate delete method if entity has a primary key and canDelete permission */
194
- const generateDelete = (ctx) => {
195
- const { entity, sqlStyle, entityName, exportName } = ctx;
196
- if (!entity.primaryKey || !entity.permissions.canDelete)
197
- return undefined;
198
- const pkColName = entity.primaryKey.columns[0];
199
- const pkField = findRowField(entity, pkColName);
200
- if (!pkField)
201
- return undefined;
202
- const rowType = entity.shapes.row.name;
203
- const fieldName = pkField.name;
204
- const parts = {
205
- templateParts: [`delete from ${entity.schemaName}.${entity.pgName} where ${pkColName} = `, ""],
206
- params: [b.identifier(fieldName)],
207
- };
208
- // Delete returns void, no type parameter needed
209
- const queryExpr = hex.query(sqlStyle, parts);
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 };
228
- };
229
- /** Generate insert method if entity has canInsert permission */
230
- const generateInsert = (ctx) => {
231
- const { entity, sqlStyle, entityName, exportName } = ctx;
232
- if (!entity.permissions.canInsert)
233
- return undefined;
234
- // Use insert shape if available, otherwise fall back to row
235
- const insertShape = entity.shapes.insert ?? entity.shapes.row;
236
- const rowType = entity.shapes.row.name;
237
- const insertType = insertShape.name;
238
- // Build column list and values from insertable fields
239
- const insertableFields = insertShape.fields.filter(f => f.permissions.canInsert);
240
- if (insertableFields.length === 0)
241
- return undefined;
242
- const columnNames = insertableFields.map(f => f.columnName);
243
- // Build: insert into schema.table (col1, col2) values ($field1, $field2) returning *
244
- const columnList = columnNames.join(", ");
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
- });
252
- // Template parts: "insert into ... values (" + "" + ", " + ", " + ... + ") returning *"
253
- const parts = {
254
- templateParts: [
255
- `insert into ${entity.schemaName}.${entity.pgName} (${columnList}) values (`,
256
- ...valuePlaceholders.slice(1),
257
- `) returning *`,
258
- ],
259
- params: paramExprs,
260
- };
261
- const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
262
- const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
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 };
283
- };
284
- /** Generate all CRUD methods for an entity */
285
- const generateCrudMethods = (ctx) => [generateFindById(ctx), generateFindMany(ctx), generateInsert(ctx), generateDelete(ctx)].filter((m) => m != null);
17
+ function buildQueryName(inflection, entityName, operation) {
18
+ return inflection.variableName(entityName, operation);
19
+ }
20
+ function buildFindByName(inflection, entityName, columnName) {
21
+ return inflection.variableName(entityName, `FindBy${inflection.pascalCase(columnName)}`);
22
+ }
23
+ function buildListByName(inflection, entityName, columnName) {
24
+ return inflection.variableName(entityName, `ListBy${inflection.pascalCase(columnName)}`);
25
+ }
286
26
  // ============================================================================
287
- // Index-based Lookup Functions
27
+ // Configuration
288
28
  // ============================================================================
289
- /** Check if an index should generate a lookup function */
290
- const shouldGenerateLookup = (index) => !index.isPartial &&
291
- !index.hasExpressions &&
292
- index.columns.length === 1 &&
293
- index.method !== "gin" &&
294
- index.method !== "gist";
295
- /**
296
- * Generate the method name portion for an index-based lookup.
297
- * Returns PascalCase like "GetByUsername" or "GetsByUser" for use with exportName.
298
- */
299
- const generateLookupMethodName = (index, relation, columnName) => {
300
- const isUnique = index.isUnique || index.isPrimary;
301
- const prefix = isUnique ? "GetBy" : "GetsBy";
302
- // Use semantic name if FK relation exists, otherwise fall back to column name
303
- const byName = relation ? deriveSemanticName(relation, columnName) : index.columns[0];
304
- return `${prefix}${toPascalCase(byName)}`;
305
- };
306
- /**
307
- * Generate a lookup method for a single-column index.
308
- * Uses semantic parameter naming when the column corresponds to an FK relation.
309
- */
310
- const generateLookupMethod = (index, ctx) => {
311
- const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
312
- const rowType = entity.shapes.row.name;
313
- const columnName = index.columnNames[0];
314
- const field = findRowField(entity, columnName);
315
- const fieldName = field?.name ?? index.columns[0];
316
- const isUnique = index.isUnique || index.isPrimary;
317
- // Check if this index column corresponds to an FK relation
318
- const relation = findRelationForColumn(entity, columnName);
319
- // Use semantic param name if FK relation exists, otherwise use field name
320
- const paramName = relation ? deriveSemanticName(relation, columnName) : fieldName;
321
- // For semantic naming, use indexed access type (Post["userId"])
322
- // For regular naming, use Pick<Post, "fieldName">
323
- const useSemanticNaming = relation !== undefined && paramName !== fieldName;
324
- const selectClause = buildSelectClause(entity, explicitColumns);
325
- const parts = {
326
- templateParts: [
327
- `${selectClause} from ${entity.schemaName}.${entity.pgName} where ${columnName} = `,
328
- "",
329
- ],
330
- params: [b.identifier(paramName)],
331
- };
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
- };
363
- if (isUnique) {
364
- // Extract first row for unique lookups
365
- const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
366
- const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
367
- const fn = asyncFn(name, [paramNode], [varDecl, b.returnStatement(b.identifier("result"))]);
368
- return { name, fn, meta };
369
- }
370
- // Non-unique: return all matching rows
371
- const fn = asyncFn(name, [paramNode], hex.returnQuery(sqlStyle, parts, ts.array(ts.ref(rowType))));
372
- return { name, fn, meta };
373
- };
374
- /** Generate lookup methods for all eligible indexes, deduplicating by name */
375
- const generateLookupMethods = (ctx) => {
376
- const seen = new Set();
377
- return ctx.entity.indexes
378
- .filter(index => shouldGenerateLookup(index) && !index.isPrimary)
379
- .filter(index => {
380
- const columnName = index.columnNames[0];
381
- const relation = findRelationForColumn(ctx.entity, columnName);
382
- const methodName = generateLookupMethodName(index, relation, columnName);
383
- const name = ctx.exportName(ctx.entityName, methodName);
384
- if (seen.has(name))
385
- return false;
386
- seen.add(name);
387
- return true;
388
- })
389
- .map(index => generateLookupMethod(index, ctx));
390
- };
29
+ const SqlQueriesConfigSchema = S.Struct({
30
+ /** Generate query functions (default: true) */
31
+ generateQueries: S.optionalWith(S.Boolean, { default: () => true }),
32
+ /** SQL query style - always "tag" for template literals */
33
+ sqlStyle: S.optionalWith(S.Literal("tag"), { default: () => "tag" }),
34
+ /** Use explicit column lists instead of SELECT * (default: true) */
35
+ explicitColumns: S.optionalWith(S.Boolean, { default: () => true }),
36
+ /** Default limit for list queries (default: 50) */
37
+ defaultLimit: S.optionalWith(S.Number, { default: () => 50 }),
38
+ });
391
39
  // ============================================================================
392
- // Function Wrapper Generation
40
+ // Query Generation Helpers
393
41
  // ============================================================================
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));
42
+ function buildColumnList(fields) {
43
+ return fields.map(f => f.columnName).join(", ");
44
+ }
45
+ function buildSelectClause(entity, explicitColumns) {
46
+ return explicitColumns
47
+ ? `select ${buildColumnList(entity.shapes.row.fields)}`
48
+ : "select *";
49
+ }
50
+ function buildTableName(entity, defaultSchemas) {
51
+ return defaultSchemas.includes(entity.schemaName)
52
+ ? entity.pgName
53
+ : `${entity.schemaName}.${entity.pgName}`;
54
+ }
55
+ function getPgType(field) {
56
+ const pgType = field.pgAttribute.getType();
57
+ return pgType?.typname ?? "unknown";
58
+ }
59
+ function pgTypeToTsType(pgType) {
60
+ const lower = pgType.toLowerCase();
61
+ if (["uuid", "text", "varchar", "char", "citext", "name"].includes(lower))
62
+ return "string";
63
+ if (["int2", "int4", "int8", "integer", "smallint", "bigint", "numeric", "decimal", "real", "float4", "float8"].includes(lower))
64
+ return "number";
65
+ if (["bool", "boolean"].includes(lower))
66
+ return "boolean";
67
+ if (["timestamp", "timestamptz", "date"].includes(lower))
68
+ return "Date";
69
+ if (["json", "jsonb"].includes(lower))
70
+ return "unknown";
71
+ return "string";
72
+ }
73
+ function buildPkParam(field) {
473
74
  return {
474
- queries: all.filter(fn => categorizeFunction(fn) === "queries"),
475
- mutations: all.filter(fn => categorizeFunction(fn) === "mutations"),
75
+ name: field.name,
76
+ type: pgTypeToTsType(getPgType(field)),
77
+ required: true,
78
+ columnName: field.columnName,
79
+ source: "pk",
476
80
  };
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);
81
+ }
82
+ function buildLookupParam(field) {
519
83
  return {
520
- tsType,
521
- isArray,
522
- isScalar: true,
84
+ name: field.name,
85
+ type: pgTypeToTsType(getPgType(field)),
86
+ required: true,
87
+ columnName: field.columnName,
88
+ source: "lookup",
523
89
  };
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;
90
+ }
91
+ function buildBodyParam(entityName, shape) {
92
+ const wrapper = shape === "insert" ? "Insertable" : "Updateable";
567
93
  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
- }
94
+ name: "data",
95
+ type: `${wrapper}<${entityName}>`,
96
+ wrapper,
97
+ entityType: entityName,
98
+ required: true,
99
+ source: "body",
619
100
  };
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" },
101
+ }
102
+ function buildPaginationParams(defaultLimit) {
103
+ return [
104
+ { name: "limit", type: "number", required: false, defaultValue: defaultLimit, source: "pagination" },
105
+ { name: "offset", type: "number", required: false, defaultValue: 0, source: "pagination" },
106
+ ];
107
+ }
108
+ function buildReturnType(entityName, isArray, nullable) {
109
+ return {
110
+ type: entityName,
111
+ nullable,
112
+ isArray,
693
113
  };
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);
709
- }
710
- }
114
+ }
115
+ function isBodyParam(p) {
116
+ return "wrapper" in p;
117
+ }
118
+ function isPaginationParam(p) {
119
+ return "defaultValue" in p;
120
+ }
121
+ function buildParamType(p) {
122
+ if (isBodyParam(p)) {
123
+ return ts.ref(p.wrapper, [ts.ref(p.entityType)]);
711
124
  }
712
- return imports;
713
- };
125
+ return ts.ref(p.type);
126
+ }
127
+ function buildDestructuredParam(params) {
128
+ return param.destructured(params.map(p => ({
129
+ name: p.name,
130
+ type: buildParamType(p),
131
+ optional: "required" in p ? p.required === false : false,
132
+ defaultValue: isPaginationParam(p) ? conjure.num(p.defaultValue) : undefined,
133
+ })));
134
+ }
714
135
  // ============================================================================
715
- // Export Style Helpers
136
+ // Plugin Definition
716
137
  // ============================================================================
717
138
  /**
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.
139
+ * SQL Queries plugin - generates raw SQL query functions with tagged templates.
752
140
  *
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
- * ```
141
+ * Capabilities provided:
142
+ * - `queries:sql:EntityName:operation` - CRUD query functions
764
143
  */
765
144
  export function sqlQueries(config) {
766
- const parsed = S.decodeUnknownSync(SqlQueriesPluginConfigSchema)(config);
767
- // Resolve config with properly typed exportName
145
+ const schemaConfig = S.decodeSync(SqlQueriesConfigSchema)(config ?? {});
146
+ const queriesFile = normalizeFileNaming(config?.queriesFile, "queries.ts");
768
147
  const resolvedConfig = {
769
- ...parsed,
770
- exportName: config.exportName ?? defaultExportName,
148
+ ...schemaConfig,
149
+ queriesFile,
771
150
  };
772
- return definePlugin({
151
+ const queriesFilePath = typeof queriesFile === "string" ? queriesFile : "queries.ts";
152
+ return {
773
153
  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]);
154
+ provides: ["queries"],
155
+ consumes: [],
156
+ fileDefaults: [
157
+ {
158
+ pattern: "queries:sql:",
159
+ fileNaming: resolvedConfig.queriesFile,
160
+ },
161
+ ],
162
+ declare: Effect.gen(function* () {
163
+ const ir = yield* IR;
164
+ const inflection = yield* Inflection;
165
+ const declarations = [];
166
+ const tableEntities = getTableEntities(ir).filter(e => e.tags.omit !== true);
167
+ if (resolvedConfig.generateQueries) {
168
+ for (const entity of tableEntities) {
169
+ const entityName = entity.name;
170
+ let hasAnyMethods = false;
171
+ if (entity.permissions.canSelect && entity.primaryKey && entity.primaryKey.columns.length > 0) {
172
+ hasAnyMethods = true;
173
+ declarations.push({
174
+ name: buildQueryName(inflection, entityName, "FindById"),
175
+ capability: `queries:sql:${entityName}:findById`,
176
+ });
794
177
  }
795
- else {
796
- scalarFunctions.push(fn);
178
+ if (entity.kind === "table" && entity.permissions.canInsert && entity.shapes.insert) {
179
+ hasAnyMethods = true;
180
+ declarations.push({
181
+ name: buildQueryName(inflection, entityName, "Create"),
182
+ capability: `queries:sql:${entityName}:create`,
183
+ });
797
184
  }
798
- }
799
- }
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" },
185
+ if (entity.kind === "table" &&
186
+ entity.permissions.canUpdate &&
187
+ entity.shapes.update &&
188
+ entity.primaryKey &&
189
+ entity.primaryKey.columns.length > 0) {
190
+ hasAnyMethods = true;
191
+ declarations.push({
192
+ name: buildQueryName(inflection, entityName, "Update"),
193
+ capability: `queries:sql:${entityName}:update`,
842
194
  });
843
195
  }
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 },
196
+ if (entity.kind === "table" &&
197
+ entity.permissions.canDelete &&
198
+ entity.primaryKey &&
199
+ entity.primaryKey.columns.length > 0) {
200
+ hasAnyMethods = true;
201
+ declarations.push({
202
+ name: buildQueryName(inflection, entityName, "Delete"),
203
+ capability: `queries:sql:${entityName}:delete`,
854
204
  });
855
205
  }
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");
886
- });
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 },
206
+ if (entity.permissions.canSelect) {
207
+ const pkColumns = new Set(entity.primaryKey?.columns ?? []);
208
+ const processedColumns = new Set();
209
+ for (const index of entity.indexes) {
210
+ if (index.isPartial || index.hasExpressions || index.columns.length !== 1)
211
+ continue;
212
+ if (index.method === "gin" || index.method === "gist")
213
+ continue;
214
+ const columnName = index.columns[0];
215
+ if (pkColumns.has(columnName))
216
+ continue;
217
+ if (processedColumns.has(columnName))
218
+ continue;
219
+ processedColumns.add(columnName);
220
+ const pascalColumn = inflection.pascalCase(columnName);
221
+ hasAnyMethods = true;
222
+ declarations.push({
223
+ name: buildFindByName(inflection, entityName, columnName),
224
+ capability: `queries:sql:${entityName}:findBy${pascalColumn}`,
225
+ });
226
+ }
227
+ }
228
+ const cursorCandidates = getCursorPaginationCandidates(entity);
229
+ for (const candidate of cursorCandidates) {
230
+ const pascalColumn = inflection.pascalCase(candidate.cursorColumnName);
231
+ hasAnyMethods = true;
232
+ declarations.push({
233
+ name: buildListByName(inflection, entityName, candidate.cursorColumnName),
234
+ capability: `queries:sql:${entityName}:listBy${pascalColumn}`,
235
+ });
236
+ }
237
+ if (hasAnyMethods) {
238
+ declarations.push({
239
+ name: `${entityName}Queries`,
240
+ capability: `queries:sql:${entityName}`,
908
241
  });
909
242
  }
910
- file.ast(conjure.program(...statements)).emit();
911
243
  }
912
244
  }
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) {
925
- file.import({
926
- kind: "symbol",
927
- ref: { capability: "types", entity: typeName },
245
+ return declarations;
246
+ }),
247
+ render: Effect.gen(function* () {
248
+ const ir = yield* IR;
249
+ const inflection = yield* Inflection;
250
+ const symbols = [];
251
+ const tableEntities = getTableEntities(ir).filter(e => e.tags.omit !== true);
252
+ const defaultSchemas = ir.schemas;
253
+ // User module imports for sql client
254
+ const queryUserImports = resolvedConfig.sqlImport
255
+ ? [resolvedConfig.sqlImport]
256
+ : undefined;
257
+ if (resolvedConfig.generateQueries) {
258
+ for (const entity of tableEntities) {
259
+ const entityName = entity.name;
260
+ const tableName = buildTableName(entity, defaultSchemas);
261
+ const selectClause = buildSelectClause(entity, resolvedConfig.explicitColumns);
262
+ const entityMethods = [];
263
+ const fromClause = `from ${tableName}`;
264
+ const buildTemplateLiteral = (parts) => {
265
+ return conjure.taggedTemplate("sql", parts, []);
266
+ };
267
+ const buildTemplateLiteralWithParams = (parts, params) => {
268
+ return conjure.taggedTemplate("sql", parts, [...params]);
269
+ };
270
+ if (entity.permissions.canSelect && entity.primaryKey && entity.primaryKey.columns.length > 0) {
271
+ const pkColumn = entity.primaryKey.columns[0];
272
+ const pkField = entity.shapes.row.fields.find(f => f.columnName === pkColumn);
273
+ const pkParam = buildPkParam(pkField);
274
+ const method = {
275
+ name: buildQueryName(inflection, entityName, "FindById"),
276
+ kind: "read",
277
+ params: [pkParam],
278
+ returns: buildReturnType(entityName, false, true),
279
+ callSignature: { style: "named" },
280
+ };
281
+ entityMethods.push(method);
282
+ const templateLiteral = buildTemplateLiteral([
283
+ `${selectClause} ${fromClause} where ${pkColumn} = `,
284
+ "",
285
+ ]);
286
+ const destructuredParam = buildDestructuredParam([pkParam]);
287
+ const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
288
+ symbols.push({
289
+ name: method.name,
290
+ capability: `queries:sql:${entityName}:findById`,
291
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
292
+ exports: "named",
293
+ userImports: queryUserImports,
294
+ });
295
+ }
296
+ if (entity.kind === "table" && entity.permissions.canInsert && entity.shapes.insert) {
297
+ const bodyParam = buildBodyParam(entityName, "insert");
298
+ const method = {
299
+ name: buildQueryName(inflection, entityName, "Create"),
300
+ kind: "create",
301
+ params: [bodyParam],
302
+ returns: buildReturnType(entityName, false, false),
303
+ callSignature: { style: "named", bodyStyle: "property" },
304
+ };
305
+ entityMethods.push(method);
306
+ const insertableFields = entity.shapes.insert.fields.filter(f => f.permissions.canInsert);
307
+ const columnNames = insertableFields.map(f => f.columnName);
308
+ const columnList = columnNames.join(", ");
309
+ const templateParts = [`insert into ${tableName} (${columnList}) values (`];
310
+ for (let i = 0; i < insertableFields.length; i++) {
311
+ if (i === 0) {
312
+ templateParts.push("");
313
+ }
314
+ else {
315
+ templateParts.push(", ");
316
+ }
317
+ }
318
+ templateParts.push(") returning *");
319
+ const paramExprs = insertableFields.map(f => b.memberExpression(b.identifier("data"), b.identifier(f.name)));
320
+ const templateLiteral = buildTemplateLiteralWithParams(templateParts, paramExprs);
321
+ const destructuredParam = buildDestructuredParam([bodyParam]);
322
+ const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
323
+ symbols.push({
324
+ name: method.name,
325
+ capability: `queries:sql:${entityName}:create`,
326
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
327
+ exports: "named",
328
+ externalImports: [
329
+ {
330
+ from: queriesFilePath,
331
+ types: [entityName],
332
+ },
333
+ ],
334
+ userImports: queryUserImports,
335
+ });
336
+ }
337
+ if (entity.kind === "table" &&
338
+ entity.permissions.canUpdate &&
339
+ entity.shapes.update &&
340
+ entity.primaryKey &&
341
+ entity.primaryKey.columns.length > 0) {
342
+ const pkColumn = entity.primaryKey.columns[0];
343
+ const pkField = entity.shapes.row.fields.find(f => f.columnName === pkColumn);
344
+ const pkParam = buildPkParam(pkField);
345
+ const bodyParam = buildBodyParam(entityName, "update");
346
+ const method = {
347
+ name: buildQueryName(inflection, entityName, "Update"),
348
+ kind: "update",
349
+ params: [pkParam, bodyParam],
350
+ returns: buildReturnType(entityName, false, true),
351
+ callSignature: { style: "named", bodyStyle: "property" },
352
+ };
353
+ entityMethods.push(method);
354
+ const updatableFields = entity.shapes.update.fields.filter(f => f.permissions.canUpdate);
355
+ const templateParts = [`update ${tableName} set `];
356
+ for (let i = 0; i < updatableFields.length; i++) {
357
+ if (i === 0) {
358
+ templateParts.push(`${updatableFields[i].columnName} = `);
359
+ }
360
+ else {
361
+ templateParts.push(`, ${updatableFields[i].columnName} = `);
362
+ }
363
+ }
364
+ templateParts.push(` where ${pkColumn} = `);
365
+ templateParts.push(" returning *");
366
+ const paramExprs = [
367
+ ...updatableFields.map(f => b.memberExpression(b.identifier("data"), b.identifier(f.name))),
368
+ b.identifier(pkField.name),
369
+ ];
370
+ const templateLiteral = buildTemplateLiteralWithParams(templateParts, paramExprs);
371
+ const destructuredParam = buildDestructuredParam([pkParam, bodyParam]);
372
+ const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
373
+ symbols.push({
374
+ name: method.name,
375
+ capability: `queries:sql:${entityName}:update`,
376
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
377
+ exports: "named",
378
+ externalImports: [
379
+ {
380
+ from: queriesFilePath,
381
+ types: [entityName],
382
+ },
383
+ ],
384
+ userImports: queryUserImports,
385
+ });
386
+ }
387
+ if (entity.kind === "table" &&
388
+ entity.permissions.canDelete &&
389
+ entity.primaryKey &&
390
+ entity.primaryKey.columns.length > 0) {
391
+ const pkColumn = entity.primaryKey.columns[0];
392
+ const pkField = entity.shapes.row.fields.find(f => f.columnName === pkColumn);
393
+ const pkParam = buildPkParam(pkField);
394
+ const method = {
395
+ name: buildQueryName(inflection, entityName, "Delete"),
396
+ kind: "delete",
397
+ params: [pkParam],
398
+ returns: buildReturnType(entityName, false, false),
399
+ callSignature: { style: "named" },
400
+ };
401
+ entityMethods.push(method);
402
+ const templateLiteral = buildTemplateLiteral([
403
+ `delete from ${tableName} where ${pkColumn} = `,
404
+ "",
405
+ ]);
406
+ const destructuredParam = buildDestructuredParam([pkParam]);
407
+ const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
408
+ symbols.push({
409
+ name: method.name,
410
+ capability: `queries:sql:${entityName}:delete`,
411
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
412
+ exports: "named",
413
+ userImports: queryUserImports,
414
+ });
415
+ }
416
+ if (entity.permissions.canSelect) {
417
+ const pkColumns = new Set(entity.primaryKey?.columns ?? []);
418
+ const processedColumns = new Set();
419
+ for (const index of entity.indexes) {
420
+ if (index.isPartial || index.hasExpressions || index.columns.length !== 1)
421
+ continue;
422
+ if (index.method === "gin" || index.method === "gist")
423
+ continue;
424
+ const columnName = index.columns[0];
425
+ if (pkColumns.has(columnName))
426
+ continue;
427
+ if (processedColumns.has(columnName))
428
+ continue;
429
+ processedColumns.add(columnName);
430
+ const field = entity.shapes.row.fields.find(f => f.columnName === columnName);
431
+ if (!field)
432
+ continue;
433
+ const pascalColumn = inflection.pascalCase(columnName);
434
+ const isUnique = index.isUnique;
435
+ const lookupParam = buildLookupParam(field);
436
+ const method = {
437
+ name: buildFindByName(inflection, entityName, columnName),
438
+ kind: "lookup",
439
+ params: [lookupParam],
440
+ returns: buildReturnType(entityName, !isUnique, isUnique),
441
+ lookupField: field.name,
442
+ isUniqueLookup: isUnique,
443
+ callSignature: { style: "named" },
444
+ };
445
+ entityMethods.push(method);
446
+ const templateLiteral = buildTemplateLiteral([
447
+ `${selectClause} ${fromClause} where ${columnName} = `,
448
+ "",
449
+ ]);
450
+ const destructuredParam = buildDestructuredParam([lookupParam]);
451
+ const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
452
+ symbols.push({
453
+ name: method.name,
454
+ capability: `queries:sql:${entityName}:findBy${pascalColumn}`,
455
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
456
+ exports: "named",
457
+ userImports: queryUserImports,
458
+ });
459
+ }
460
+ }
461
+ const cursorCandidates = getCursorPaginationCandidates(entity);
462
+ for (const candidate of cursorCandidates) {
463
+ const pascalColumn = inflection.pascalCase(candidate.cursorColumnName);
464
+ const pkField = entity.shapes.row.fields.find(f => f.name === candidate.pkColumn);
465
+ if (!pkField)
466
+ continue;
467
+ const pkParam = {
468
+ name: candidate.pkColumn,
469
+ type: pgTypeToTsType(getPgType(pkField)),
470
+ required: false,
471
+ };
472
+ const limitParam = {
473
+ name: "limit",
474
+ type: "number",
475
+ required: false,
476
+ defaultValue: resolvedConfig.defaultLimit,
477
+ source: "pagination",
478
+ };
479
+ const cursorParam = {
480
+ name: "cursor",
481
+ type: `{ ${candidate.cursorColumn}: Date; ${candidate.pkColumn}: ${pkParam.type} }`,
482
+ required: false,
483
+ };
484
+ const operator = candidate.desc ? "<" : ">";
485
+ const orderDirection = candidate.desc ? "DESC" : "ASC";
486
+ const method = {
487
+ name: buildListByName(inflection, entityName, candidate.cursorColumnName),
488
+ kind: "list",
489
+ params: [cursorParam, limitParam],
490
+ returns: buildReturnType(entityName, true, false),
491
+ callSignature: { style: "named" },
492
+ };
493
+ entityMethods.push(method);
494
+ const templateParts = [
495
+ `${selectClause} ${fromClause} where ($`,
496
+ `::timestamptz IS NULL OR (${candidate.cursorColumnName}, ${candidate.pkColumnName}) ${operator} ($`,
497
+ `, `,
498
+ `)) order by ${candidate.cursorColumnName} ${orderDirection}, ${candidate.pkColumnName} ${orderDirection} limit `,
499
+ ];
500
+ const paramExprs = [
501
+ b.memberExpression(b.identifier("cursor"), b.identifier(candidate.cursorColumn)),
502
+ b.memberExpression(b.identifier("cursor"), b.identifier(candidate.pkColumn)),
503
+ b.identifier("limit"),
504
+ ];
505
+ const templateLiteral = buildTemplateLiteralWithParams(templateParts, paramExprs);
506
+ const destructuredParam = buildDestructuredParam([cursorParam, limitParam]);
507
+ const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
508
+ symbols.push({
509
+ name: method.name,
510
+ capability: `queries:sql:${entityName}:listBy${pascalColumn}`,
511
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
512
+ exports: "named",
513
+ userImports: queryUserImports,
514
+ });
515
+ }
516
+ const pkField = entity.primaryKey?.columns[0]
517
+ ? entity.shapes.row.fields.find(f => f.columnName === entity.primaryKey.columns[0])
518
+ : undefined;
519
+ const entityExtension = {
520
+ methods: entityMethods,
521
+ pkType: pkField ? pgTypeToTsType(getPgType(pkField)) : undefined,
522
+ hasCompositePk: (entity.primaryKey?.columns.length ?? 0) > 1,
523
+ };
524
+ symbols.push({
525
+ name: `${entityName}Queries`,
526
+ capability: `queries:sql:${entityName}`,
527
+ node: b.emptyStatement(),
528
+ metadata: entityExtension,
529
+ exports: false,
928
530
  });
929
531
  }
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
933
532
  }
934
- },
935
- });
533
+ return symbols;
534
+ }),
535
+ };
936
536
  }
937
537
  //# sourceMappingURL=sql-queries.js.map