@danielfgray/pg-sourcerer 0.3.0 → 0.5.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,1169 +0,0 @@
1
- // @ts-nocheck - TODO: migrate to new plugin API
2
- /**
3
- * Kysely Queries Generation
4
- *
5
- * Generates permission-aware CRUD query functions using Kysely's query builder.
6
- * Uses object namespace style with explicit `db: Kysely<DB>` first parameter.
7
- */
8
- import { Schema as S } from "effect";
9
- import { getTableEntities, getEnumEntities, getFunctionEntities, getCompositeEntities, } from "../../ir/semantic-ir.js";
10
- import { conjure, cast } from "../../lib/conjure.js";
11
- import { resolveFieldType, tsTypeToAst } from "../../lib/field-utils.js";
12
- import { inflect } from "../../services/inflection.js";
13
- import { pgTypeNameToTs } from "./shared.js";
14
- const { ts, b, param, str } = conjure;
15
- const { toExpr } = cast;
16
- // ============================================================================
17
- // Query Artifact Schema (plugin-defined)
18
- // ============================================================================
19
- // This schema defines what kysely-queries emits as its artifact.
20
- // HTTP plugins consuming "queries:kysely" should decode using a compatible schema.
21
- /** How to call a query function */
22
- const CallSignature = S.Struct({
23
- /** "named" = fn({ a, b }), "positional" = fn(a, b) */
24
- style: S.Union(S.Literal("named"), S.Literal("positional")),
25
- /** For named + body: "property" = { data: body }, "spread" = { field1, field2 } */
26
- bodyStyle: S.optional(S.Union(S.Literal("property"), S.Literal("spread"))),
27
- });
28
- const QueryMethodParam = S.Struct({
29
- name: S.String,
30
- type: S.String,
31
- required: S.Boolean,
32
- columnName: S.optional(S.String),
33
- source: S.optional(S.Union(S.Literal("pk"), S.Literal("fk"), S.Literal("lookup"), S.Literal("body"), S.Literal("pagination"))),
34
- });
35
- const QueryMethodReturn = S.Struct({
36
- type: S.String,
37
- nullable: S.Boolean,
38
- isArray: S.Boolean,
39
- });
40
- const QueryMethodKind = S.Union(S.Literal("read"), S.Literal("list"), S.Literal("create"), S.Literal("update"), S.Literal("delete"), S.Literal("lookup"), S.Literal("function"));
41
- const QueryMethod = S.Struct({
42
- name: S.String,
43
- kind: QueryMethodKind,
44
- params: S.Array(QueryMethodParam),
45
- returns: QueryMethodReturn,
46
- lookupField: S.optional(S.String),
47
- isUniqueLookup: S.optional(S.Boolean),
48
- callSignature: S.optional(CallSignature),
49
- });
50
- const EntityQueryMethods = S.Struct({
51
- entityName: S.String,
52
- tableName: S.String,
53
- schemaName: S.String,
54
- pkType: S.optional(S.String),
55
- hasCompositePk: S.optional(S.Boolean),
56
- methods: S.Array(QueryMethod),
57
- });
58
- const FunctionQueryMethod = S.Struct({
59
- functionName: S.String,
60
- exportName: S.String,
61
- schemaName: S.String,
62
- volatility: S.Union(S.Literal("immutable"), S.Literal("stable"), S.Literal("volatile")),
63
- params: S.Array(QueryMethodParam),
64
- returns: QueryMethodReturn,
65
- callSignature: S.optional(CallSignature),
66
- });
67
- const QueryArtifact = S.Struct({
68
- entities: S.Array(EntityQueryMethods),
69
- functions: S.Array(FunctionQueryMethod),
70
- sourcePlugin: S.String,
71
- outputDir: S.String,
72
- });
73
- /** Default export name: camelCase method name (e.g., "findById") */
74
- const defaultExportName = (_entityName, methodName) => methodName.charAt(0).toLowerCase() + methodName.slice(1);
75
- /**
76
- * Schema for serializable config options (JSON/YAML compatible).
77
- * Function options are typed separately in KyselyQueriesConfigInput.
78
- */
79
- const KyselyQueriesPluginConfigSchema = S.Struct({
80
- outputDir: S.optionalWith(S.String, { default: () => "kysely-queries" }),
81
- /**
82
- * Path to import DB type from (relative to outputDir).
83
- * Defaults to "../db.js" which works with kysely-types plugin output.
84
- * For node16/nodenext module resolution, use ".js" extension even for .ts files.
85
- */
86
- dbTypesPath: S.optionalWith(S.String, { default: () => "../db.js" }),
87
- /**
88
- * Whether to call .execute() / .executeTakeFirst() on queries.
89
- * When true (default), methods return Promise<Row> or Promise<Row[]>.
90
- * When false, methods return the query builder for further customization.
91
- */
92
- executeQueries: S.optionalWith(S.Boolean, { default: () => true }),
93
- /**
94
- * Whether to generate listMany() method for unfiltered table scans.
95
- * Disabled by default since unfiltered scans don't use indexes.
96
- * When enabled, generates: listMany(db, limit = 50, offset = 0)
97
- */
98
- generateListMany: S.optionalWith(S.Boolean, { default: () => false }),
99
- /**
100
- * Whether to generate function wrappers for stored functions.
101
- * When true (default), generates queries/mutations namespaces in functions.ts.
102
- */
103
- generateFunctions: S.optionalWith(S.Boolean, { default: () => true }),
104
- /**
105
- * Output file name for function wrappers (relative to outputDir).
106
- */
107
- functionsFile: S.optionalWith(S.String, { default: () => "functions.ts" }),
108
- /**
109
- * Export name function (validated as Any, properly typed in KyselyQueriesConfigInput)
110
- */
111
- exportName: S.optional(S.Any),
112
- /**
113
- * Export style for generated query functions.
114
- * - "flat": Individual exports (e.g., `export const findById = ...`)
115
- * - "namespace": Single object export (e.g., `export const User = { findById: ... }`)
116
- */
117
- exportStyle: S.optionalWith(S.Literal("flat", "namespace"), { default: () => "flat" }),
118
- /**
119
- * Use explicit column lists instead of .selectAll().
120
- * When true, generates .select(['col1', 'col2']) which excludes omitted fields at runtime.
121
- * Defaults to true.
122
- */
123
- explicitColumns: S.optionalWith(S.Boolean, { default: () => true }),
124
- /**
125
- * Whether to pass db as first parameter to each function.
126
- * When true (default), functions take `db: Kysely<DB>` as first arg.
127
- * When false, functions use a module-level `db` variable - use `header`
128
- * to provide the import statement.
129
- */
130
- dbAsParameter: S.optionalWith(S.Boolean, { default: () => true }),
131
- /**
132
- * Header content to prepend to each generated file.
133
- * Use this to provide imports when dbAsParameter is false.
134
- * Example: `import { db } from "../db.js"`
135
- */
136
- header: S.optional(S.String),
137
- /**
138
- * Default limit for listMany queries.
139
- * @default 50
140
- */
141
- defaultLimit: S.optionalWith(S.Number, { default: () => 50 }),
142
- });
143
- /** Default config values for queries */
144
- const defaultQueriesConfig = {
145
- outputDir: "kysely-queries",
146
- dbTypesPath: "../db.js",
147
- executeQueries: true,
148
- generateListMany: false,
149
- generateFunctions: true,
150
- functionsFile: "functions.ts",
151
- exportStyle: "flat",
152
- explicitColumns: true,
153
- dbAsParameter: true,
154
- defaultLimit: 50,
155
- };
156
- /**
157
- * Get the Kysely table interface name from the entity.
158
- * Uses entity.name which is already PascalCase from inflection (e.g., Users).
159
- */
160
- const getTableTypeName = (entity) => entity.name;
161
- /**
162
- * Get the table reference for Kysely queries.
163
- * Uses schema-qualified name only if the schema is NOT in defaultSchemas.
164
- * This matches the keys in the DB interface from kysely-types plugin.
165
- */
166
- const getTableRef = (entity, defaultSchemas) => defaultSchemas.includes(entity.schemaName)
167
- ? entity.pgName
168
- : `${entity.schemaName}.${entity.pgName}`;
169
- /** Find a field in the row shape by column name */
170
- const findRowField = (entity, columnName) => entity.shapes.row.fields.find(f => f.columnName === columnName);
171
- /** Get the TypeScript type AST for a field */
172
- const getFieldTypeAst = (field, ctx) => {
173
- if (!field)
174
- return ts.string();
175
- const resolved = resolveFieldType(field, ctx.enums, ctx.ir.extensions);
176
- return resolved.enumDef ? ts.ref(resolved.enumDef.name) : tsTypeToAst(resolved.tsType);
177
- };
178
- /** Get TypeScript type string for a field */
179
- const getFieldTypeString = (field, ctx) => {
180
- if (!field)
181
- return "string";
182
- const resolved = resolveFieldType(field, ctx.enums, ctx.ir.extensions);
183
- return resolved.enumDef ? resolved.enumDef.name : resolved.tsType;
184
- };
185
- // ============================================================================
186
- // FK Semantic Naming Helpers
187
- // ============================================================================
188
- /**
189
- * Find a belongsTo relation that uses the given column as its local FK column.
190
- * For single-column indexes only.
191
- */
192
- const findRelationForColumn = (entity, columnName) => entity.relations.find(r => r.kind === "belongsTo" && r.columns.length === 1 && r.columns[0]?.local === columnName);
193
- /**
194
- * Derive semantic name for an FK-based lookup.
195
- * Priority: @fieldName tag → column minus _id suffix → target entity name
196
- */
197
- const deriveSemanticName = (relation, columnName) => {
198
- // 1. Check for @fieldName smart tag
199
- if (relation.tags.fieldName && typeof relation.tags.fieldName === "string") {
200
- return relation.tags.fieldName;
201
- }
202
- // 2. Strip common FK suffixes from column name
203
- const suffixes = ["_id", "_fk", "Id", "Fk"];
204
- for (const suffix of suffixes) {
205
- if (columnName.endsWith(suffix)) {
206
- const stripped = columnName.slice(0, -suffix.length);
207
- if (stripped.length > 0)
208
- return stripped;
209
- }
210
- }
211
- // 3. Fall back to target entity name (lowercased first char)
212
- const target = relation.targetEntity;
213
- return target.charAt(0).toLowerCase() + target.slice(1);
214
- };
215
- /**
216
- * Convert to PascalCase for use in method names.
217
- * Handles snake_case (created_at → CreatedAt) and regular strings.
218
- */
219
- const toPascalCase = (s) => inflect.pascalCase(s);
220
- // ============================================================================
221
- // AST Building Helpers
222
- // ============================================================================
223
- /** Create identifier */
224
- const id = (name) => b.identifier(name);
225
- /** Create method call: obj.method(args) */
226
- const call = (obj, method, args = []) => b.callExpression(b.memberExpression(toExpr(obj), id(method)), args.map(toExpr));
227
- /**
228
- * Build Kysely query chain starting with db.selectFrom('table')
229
- */
230
- const selectFrom = (tableRef) => call(id("db"), "selectFrom", [str(tableRef)]);
231
- /**
232
- * Build column selection: .selectAll() or .select(['col1', 'col2'])
233
- * When explicitColumns is true, uses explicit column list from row shape.
234
- */
235
- const selectColumns = (base, entity, explicitColumns) => explicitColumns
236
- ? call(base, "select", [
237
- b.arrayExpression(entity.shapes.row.fields.map(f => str(f.columnName))),
238
- ])
239
- : call(base, "selectAll");
240
- /**
241
- * Build Kysely query chain: db.insertInto('table')
242
- */
243
- const insertInto = (tableRef) => call(id("db"), "insertInto", [str(tableRef)]);
244
- /**
245
- * Build Kysely query chain: db.updateTable('table')
246
- */
247
- const updateTable = (tableRef) => call(id("db"), "updateTable", [str(tableRef)]);
248
- /**
249
- * Build Kysely query chain: db.deleteFrom('table')
250
- */
251
- const deleteFrom = (tableRef) => call(id("db"), "deleteFrom", [str(tableRef)]);
252
- /**
253
- * Chain method call onto existing expression
254
- */
255
- const chain = (expr, method, args = []) => call(expr, method, args);
256
- /**
257
- * Build arrow function expression: (params) => body
258
- */
259
- const arrowFn = (params, body) => {
260
- const fn = b.arrowFunctionExpression(params.map(p => p), toExpr(body));
261
- return fn;
262
- };
263
- /**
264
- * Build object property: key: value
265
- */
266
- const objProp = (key, value) => {
267
- const prop = b.objectProperty(id(key), toExpr(value));
268
- return prop;
269
- };
270
- /**
271
- * Check if a function argument type matches a table/view entity (row type argument).
272
- * Functions with row-type arguments are computed fields (e.g., posts_short_body(posts))
273
- * and should be excluded from function wrapper generation.
274
- */
275
- const hasRowTypeArg = (arg, ir) => {
276
- const tableEntities = getTableEntities(ir);
277
- // Check if arg.typeName matches a table entity's qualified name
278
- // Format: "schema.tablename" or just "tablename" for public schema
279
- return tableEntities.some(entity => {
280
- const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
281
- return arg.typeName === qualifiedName || arg.typeName === entity.pgName;
282
- });
283
- };
284
- /**
285
- * Check if a function should be included in generated wrappers.
286
- *
287
- * Includes functions that:
288
- * - Have canExecute permission
289
- * - Are not trigger functions
290
- * - Are not from extensions
291
- * - Are not @omit tagged
292
- * - Don't have row-type arguments (computed fields)
293
- */
294
- const isGeneratableFunction = (fn, ir) => {
295
- if (!fn.canExecute)
296
- return false;
297
- if (fn.returnTypeName === "trigger")
298
- return false;
299
- if (fn.isFromExtension)
300
- return false;
301
- if (fn.tags.omit === true)
302
- return false;
303
- // Check for row-type args (computed field pattern)
304
- if (fn.args.some(arg => hasRowTypeArg(arg, ir)))
305
- return false;
306
- return true;
307
- };
308
- /**
309
- * Categorize functions by volatility.
310
- * Volatile functions go in mutations namespace, stable/immutable in queries.
311
- */
312
- const categorizeFunction = (fn) => fn.volatility === "volatile" ? "mutations" : "queries";
313
- /**
314
- * Get all generatable functions from the IR, categorized by volatility.
315
- */
316
- const getGeneratableFunctions = (ir) => {
317
- const all = getFunctionEntities(ir).filter(fn => isGeneratableFunction(fn, ir));
318
- return {
319
- queries: all.filter(fn => categorizeFunction(fn) === "queries"),
320
- mutations: all.filter(fn => categorizeFunction(fn) === "mutations"),
321
- };
322
- };
323
- /**
324
- * Resolve a function's return type to TypeScript type information.
325
- */
326
- const resolveReturnType = (fn, ir) => {
327
- const returnTypeName = fn.returnTypeName;
328
- const isArray = fn.returnsSet;
329
- // 1. Check if it's a table return type
330
- const tableEntities = getTableEntities(ir);
331
- const tableMatch = tableEntities.find(entity => {
332
- const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
333
- return returnTypeName === qualifiedName || returnTypeName === entity.pgName;
334
- });
335
- if (tableMatch) {
336
- return {
337
- tsType: tableMatch.name,
338
- isArray,
339
- isScalar: false,
340
- needsImport: tableMatch.name,
341
- returnEntity: tableMatch,
342
- };
343
- }
344
- // 2. Check if it's a composite type return
345
- const compositeEntities = getCompositeEntities(ir);
346
- const compositeMatch = compositeEntities.find(entity => {
347
- const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
348
- return returnTypeName === qualifiedName || returnTypeName === entity.pgName;
349
- });
350
- if (compositeMatch) {
351
- return {
352
- tsType: compositeMatch.name,
353
- isArray,
354
- isScalar: false,
355
- needsImport: compositeMatch.name,
356
- returnEntity: compositeMatch,
357
- };
358
- }
359
- // 3. It's a scalar type - map via type name
360
- // Handle "schema.typename" format by extracting just the type name
361
- const baseTypeName = returnTypeName.includes(".")
362
- ? returnTypeName.split(".").pop()
363
- : returnTypeName;
364
- const tsType = pgTypeNameToTs(baseTypeName);
365
- return {
366
- tsType,
367
- isArray,
368
- isScalar: true,
369
- };
370
- };
371
- /**
372
- * Resolve a function argument to TypeScript type information.
373
- */
374
- const resolveArg = (arg, ir) => {
375
- const typeName = arg.typeName;
376
- // Check if it's an array type (ends with [])
377
- const isArrayType = typeName.endsWith("[]");
378
- const baseTypeName = isArrayType ? typeName.slice(0, -2) : typeName;
379
- // Check enums
380
- const enums = getEnumEntities(ir);
381
- const enumMatch = enums.find(e => {
382
- const qualifiedName = `${e.schemaName}.${e.pgName}`;
383
- return baseTypeName === qualifiedName || baseTypeName === e.pgName;
384
- });
385
- if (enumMatch) {
386
- const tsType = isArrayType ? `${enumMatch.name}[]` : enumMatch.name;
387
- return {
388
- name: arg.name || "arg",
389
- tsType,
390
- isOptional: arg.hasDefault,
391
- needsImport: enumMatch.name,
392
- };
393
- }
394
- // Check composites
395
- const composites = getCompositeEntities(ir);
396
- const compositeMatch = composites.find(e => {
397
- const qualifiedName = `${e.schemaName}.${e.pgName}`;
398
- return baseTypeName === qualifiedName || baseTypeName === e.pgName;
399
- });
400
- if (compositeMatch) {
401
- const tsType = isArrayType ? `${compositeMatch.name}[]` : compositeMatch.name;
402
- return {
403
- name: arg.name || "arg",
404
- tsType,
405
- isOptional: arg.hasDefault,
406
- needsImport: compositeMatch.name,
407
- };
408
- }
409
- // Scalar type - map via type name
410
- // Handle "schema.typename" format
411
- const scalarBase = baseTypeName.includes(".") ? baseTypeName.split(".").pop() : baseTypeName;
412
- const scalarTs = pgTypeNameToTs(scalarBase);
413
- const tsType = isArrayType ? `${scalarTs}[]` : scalarTs;
414
- return {
415
- name: arg.name || "arg",
416
- tsType,
417
- isOptional: arg.hasDefault,
418
- };
419
- };
420
- /**
421
- * Resolve all arguments for a function.
422
- */
423
- const resolveArgs = (fn, ir) => fn.args.map(arg => resolveArg(arg, ir));
424
- // ============================================================================
425
- // Function Wrapper AST Generation
426
- // ============================================================================
427
- /**
428
- * Generate a typed parameter with explicit type annotation from type string.
429
- */
430
- const typedParamFromString = (name, typeStr) => param.typed(name, ts.fromString(typeStr));
431
- /**
432
- * Get the fully qualified function name for use in eb.fn call.
433
- */
434
- const getFunctionQualifiedName = (fn) => `${fn.schemaName}.${fn.pgName}`;
435
- /**
436
- * Generate a function wrapper method.
437
- *
438
- * Patterns:
439
- * - SETOF/table return: db.selectFrom(eb => eb.fn<Type>(...).as('f')).selectAll().execute()
440
- * - Single row return: db.selectFrom(eb => eb.fn<Type>(...).as('f')).selectAll().executeTakeFirst()
441
- * - Scalar return: db.selectNoFrom(eb => eb.fn<Type>(...).as('result')).executeTakeFirst().then(r => r?.result)
442
- */
443
- const generateFunctionWrapper = (fn, ir, executeQueries, dbAsParameter) => {
444
- const resolvedReturn = resolveReturnType(fn, ir);
445
- const resolvedArgs = resolveArgs(fn, ir);
446
- const qualifiedName = getFunctionQualifiedName(fn);
447
- // Build eb.val(arg) for each argument
448
- const fnArgs = resolvedArgs.map(arg => call(id("eb"), "val", [id(arg.name)]));
449
- // Build eb.fn<Type>('schema.fn_name', [args]).as('alias')
450
- // The type parameter is the return type
451
- const returnTypeAst = resolvedReturn.isScalar
452
- ? typedParamFromString("_", resolvedReturn.tsType).typeAnnotation.typeAnnotation
453
- : ts.ref(resolvedReturn.tsType);
454
- // Create eb.fn with type parameter: eb.fn<Type>
455
- const fnMember = b.memberExpression(id("eb"), id("fn"));
456
- const fnWithType = b.tsInstantiationExpression(fnMember, b.tsTypeParameterInstantiation([cast.toTSType(returnTypeAst)]));
457
- // Call it: eb.fn<Type>(name, args)
458
- const fnCallBase = b.callExpression(fnWithType, [
459
- str(qualifiedName),
460
- b.arrayExpression(fnArgs.map(toExpr)),
461
- ]);
462
- // .as('f') or .as('result') for scalar
463
- const alias = resolvedReturn.isScalar ? "result" : "f";
464
- const fnCallWithAlias = call(fnCallBase, "as", [str(alias)]);
465
- // Arrow function for selectFrom callback: eb => eb.fn<...>(...).as('f')
466
- const selectCallback = arrowFn([id("eb")], fnCallWithAlias);
467
- // Build the query chain
468
- let query;
469
- if (resolvedReturn.isScalar) {
470
- // Scalar: db.selectNoFrom(eb => ...).executeTakeFirst()
471
- // Returns { result: T } | undefined - caller accesses .result
472
- query = call(id("db"), "selectNoFrom", [selectCallback]);
473
- if (executeQueries) {
474
- query = chain(query, "executeTakeFirst");
475
- }
476
- }
477
- else {
478
- // Table/composite: db.selectFrom(eb => ...).selectAll()
479
- query = chain(call(id("db"), "selectFrom", [selectCallback]), "selectAll");
480
- if (executeQueries) {
481
- // SETOF → .execute(), single row → .executeTakeFirst()
482
- query = chain(query, resolvedReturn.isArray ? "execute" : "executeTakeFirst");
483
- }
484
- }
485
- // Build the parameters using destructured object pattern
486
- // When dbAsParameter is true: ({ arg1, arg2 }: { arg1: Type1; arg2?: Type2 }, db: Kysely<DB>) => ...
487
- // When dbAsParameter is false: ({ arg1, arg2 }: { arg1: Type1; arg2?: Type2 }) => ...
488
- const params = [];
489
- // Add destructured options param if there are any args
490
- if (resolvedArgs.length > 0) {
491
- const optionsParam = param.destructured(resolvedArgs.map(arg => ({
492
- name: arg.name,
493
- type: ts.fromString(arg.tsType),
494
- optional: arg.isOptional,
495
- })));
496
- params.push(optionsParam);
497
- }
498
- // Add db param last if enabled
499
- if (dbAsParameter) {
500
- params.push(param.typed("db", ts.ref("Kysely", [ts.ref("DB")])));
501
- }
502
- const wrapperFn = arrowFn(params, query);
503
- // Build metadata for QueryArtifact
504
- const meta = {
505
- name: fn.name,
506
- kind: "function",
507
- params: resolvedArgs.map(arg => ({
508
- name: arg.name,
509
- type: arg.tsType,
510
- required: !arg.isOptional,
511
- })),
512
- returns: {
513
- type: resolvedReturn.tsType,
514
- nullable: resolvedReturn.isScalar || !resolvedReturn.isArray,
515
- isArray: resolvedReturn.isArray,
516
- },
517
- callSignature: { style: "named" },
518
- };
519
- return { name: fn.name, fn: wrapperFn, meta };
520
- };
521
- /**
522
- * Collect all type imports needed for function wrappers.
523
- */
524
- const collectFunctionTypeImports = (functions, ir) => {
525
- const imports = new Set();
526
- for (const fn of functions) {
527
- const resolvedReturn = resolveReturnType(fn, ir);
528
- if (resolvedReturn.needsImport) {
529
- imports.add(resolvedReturn.needsImport);
530
- }
531
- for (const arg of resolveArgs(fn, ir)) {
532
- if (arg.needsImport) {
533
- imports.add(arg.needsImport);
534
- }
535
- }
536
- }
537
- return imports;
538
- };
539
- // ============================================================================
540
- // CRUD Method Generators
541
- // ============================================================================
542
- /**
543
- * Generate findById method if entity has a primary key and canSelect permission:
544
- * export const findById = ({ id }: { id: number }, db: Kysely<DB>) => db.selectFrom('table').select([...]).where('id', '=', id).executeTakeFirst()
545
- */
546
- const generateFindById = (ctx) => {
547
- const { entity, executeQueries, defaultSchemas, entityName, exportName, explicitColumns, dbAsParameter, } = ctx;
548
- if (!entity.primaryKey || !entity.permissions.canSelect)
549
- return undefined;
550
- const pkColName = entity.primaryKey.columns[0];
551
- const pkField = findRowField(entity, pkColName);
552
- if (!pkField)
553
- return undefined;
554
- const tableRef = getTableRef(entity, defaultSchemas);
555
- const fieldName = pkField.name;
556
- const fieldType = getFieldTypeAst(pkField, ctx);
557
- // db.selectFrom('table').select([...]).where('col', '=', id)
558
- let query = chain(selectColumns(selectFrom(tableRef), entity, explicitColumns), "where", [str(pkColName), str("="), id(fieldName)]);
559
- if (executeQueries) {
560
- query = chain(query, "executeTakeFirst");
561
- }
562
- // Destructured param: { id }: { id: number }
563
- const optionsParam = param.destructured([{ name: fieldName, type: fieldType }]);
564
- const dbParam = param.typed("db", ts.ref("Kysely", [ts.ref("DB")]));
565
- // Options first, db last
566
- const params = dbAsParameter ? [optionsParam, dbParam] : [optionsParam];
567
- const fn = arrowFn(params, query);
568
- const name = exportName(entityName, "FindById");
569
- const rowType = entity.shapes.row.name;
570
- const meta = {
571
- name,
572
- kind: "read",
573
- params: [{
574
- name: fieldName,
575
- type: getFieldTypeString(pkField, ctx),
576
- required: true,
577
- columnName: pkColName,
578
- source: "pk",
579
- }],
580
- returns: { type: rowType, nullable: true, isArray: false },
581
- callSignature: { style: "named" },
582
- };
583
- return { name, fn, meta };
584
- };
585
- /** Default offset for findMany queries */
586
- const DEFAULT_OFFSET = 0;
587
- /**
588
- * Generate listMany method with pagination defaults:
589
- * export const listMany = ({ limit = 50, offset = 0 }: { limit?: number; offset?: number }, db: Kysely<DB>) => ...
590
- */
591
- const generateListMany = (ctx) => {
592
- const { entity, executeQueries, defaultSchemas, entityName, exportName, explicitColumns, dbAsParameter, defaultLimit, } = ctx;
593
- if (!entity.permissions.canSelect)
594
- return undefined;
595
- const tableRef = getTableRef(entity, defaultSchemas);
596
- // Build query: db.selectFrom('table').select([...]).limit(limit).offset(offset)
597
- let query = chain(chain(selectColumns(selectFrom(tableRef), entity, explicitColumns), "limit", [id("limit")]), "offset", [id("offset")]);
598
- // Add .execute() if executeQueries is true
599
- if (executeQueries) {
600
- query = chain(query, "execute");
601
- }
602
- // Destructured param with defaults: { limit = 50, offset = 0 }: { limit?: number; offset?: number }
603
- const optionsParam = param.destructured([
604
- {
605
- name: "limit",
606
- type: ts.number(),
607
- optional: true,
608
- defaultValue: b.numericLiteral(defaultLimit),
609
- },
610
- {
611
- name: "offset",
612
- type: ts.number(),
613
- optional: true,
614
- defaultValue: b.numericLiteral(DEFAULT_OFFSET),
615
- },
616
- ]);
617
- const dbParam = param.typed("db", ts.ref("Kysely", [ts.ref("DB")]));
618
- const params = dbAsParameter ? [optionsParam, dbParam] : [optionsParam];
619
- const fn = arrowFn(params, query);
620
- const name = exportName(entityName, "ListMany");
621
- const rowType = entity.shapes.row.name;
622
- const meta = {
623
- name,
624
- kind: "list",
625
- params: [
626
- { name: "limit", type: "number", required: false, source: "pagination" },
627
- { name: "offset", type: "number", required: false, source: "pagination" },
628
- ],
629
- returns: { type: rowType, nullable: false, isArray: true },
630
- callSignature: { style: "named" },
631
- };
632
- return { name, fn, meta };
633
- };
634
- /**
635
- * Generate create method:
636
- * export const create = ({ data }: { data: Insertable<Users> }, db: Kysely<DB>) => ...
637
- */
638
- const generateCreate = (ctx) => {
639
- const { entity, executeQueries, defaultSchemas, entityName, exportName, dbAsParameter } = ctx;
640
- if (!entity.permissions.canInsert)
641
- return undefined;
642
- const tableRef = getTableRef(entity, defaultSchemas);
643
- const tableTypeName = getTableTypeName(entity);
644
- // db.insertInto('table').values(data).returningAll()
645
- let query = chain(chain(insertInto(tableRef), "values", [id("data")]), "returningAll");
646
- if (executeQueries) {
647
- query = chain(query, "executeTakeFirstOrThrow");
648
- }
649
- // Destructured param: { data }: { data: Insertable<Users> }
650
- const optionsParam = param.destructured([
651
- { name: "data", type: ts.ref("Insertable", [ts.ref(tableTypeName)]) },
652
- ]);
653
- const dbParam = param.typed("db", ts.ref("Kysely", [ts.ref("DB")]));
654
- const params = dbAsParameter ? [optionsParam, dbParam] : [optionsParam];
655
- const fn = arrowFn(params, query);
656
- const name = exportName(entityName, "Create");
657
- const rowType = entity.shapes.row.name;
658
- const insertType = `Insertable<${tableTypeName}>`;
659
- const meta = {
660
- name,
661
- kind: "create",
662
- params: [{
663
- name: "data",
664
- type: insertType,
665
- required: true,
666
- source: "body",
667
- }],
668
- returns: { type: rowType, nullable: false, isArray: false },
669
- callSignature: { style: "named", bodyStyle: "property" },
670
- };
671
- return { name, fn, meta };
672
- };
673
- /**
674
- * Generate update method:
675
- * export const update = ({ id, data }: { id: number; data: Updateable<Users> }, db: Kysely<DB>) => ...
676
- */
677
- const generateUpdate = (ctx) => {
678
- const { entity, executeQueries, defaultSchemas, entityName, exportName, dbAsParameter } = ctx;
679
- if (!entity.primaryKey || !entity.permissions.canUpdate)
680
- return undefined;
681
- const pkColName = entity.primaryKey.columns[0];
682
- const pkField = findRowField(entity, pkColName);
683
- if (!pkField)
684
- return undefined;
685
- const tableRef = getTableRef(entity, defaultSchemas);
686
- const fieldName = pkField.name;
687
- const fieldType = getFieldTypeAst(pkField, ctx);
688
- const tableTypeName = getTableTypeName(entity);
689
- // db.updateTable('table').set(data).where('id', '=', id).returningAll()
690
- let query = chain(chain(chain(updateTable(tableRef), "set", [id("data")]), "where", [
691
- str(pkColName),
692
- str("="),
693
- id(fieldName),
694
- ]), "returningAll");
695
- if (executeQueries) {
696
- query = chain(query, "executeTakeFirstOrThrow");
697
- }
698
- // Destructured param: { id, data }: { id: number; data: Updateable<Users> }
699
- const optionsParam = param.destructured([
700
- { name: fieldName, type: fieldType },
701
- { name: "data", type: ts.ref("Updateable", [ts.ref(tableTypeName)]) },
702
- ]);
703
- const dbParam = param.typed("db", ts.ref("Kysely", [ts.ref("DB")]));
704
- const params = dbAsParameter ? [optionsParam, dbParam] : [optionsParam];
705
- const fn = arrowFn(params, query);
706
- const name = exportName(entityName, "Update");
707
- const rowType = entity.shapes.row.name;
708
- const updateType = `Updateable<${tableTypeName}>`;
709
- const meta = {
710
- name,
711
- kind: "update",
712
- params: [
713
- {
714
- name: fieldName,
715
- type: getFieldTypeString(pkField, ctx),
716
- required: true,
717
- columnName: pkColName,
718
- source: "pk",
719
- },
720
- {
721
- name: "data",
722
- type: updateType,
723
- required: true,
724
- source: "body",
725
- },
726
- ],
727
- returns: { type: rowType, nullable: false, isArray: false },
728
- callSignature: { style: "named", bodyStyle: "property" },
729
- };
730
- return { name, fn, meta };
731
- };
732
- /**
733
- * Generate delete method:
734
- * export const remove = ({ id }: { id: number }, db: Kysely<DB>) => ...
735
- */
736
- const generateDelete = (ctx) => {
737
- const { entity, executeQueries, defaultSchemas, entityName, exportName, dbAsParameter } = ctx;
738
- if (!entity.primaryKey || !entity.permissions.canDelete)
739
- return undefined;
740
- const pkColName = entity.primaryKey.columns[0];
741
- const pkField = findRowField(entity, pkColName);
742
- if (!pkField)
743
- return undefined;
744
- const tableRef = getTableRef(entity, defaultSchemas);
745
- const fieldName = pkField.name;
746
- const fieldType = getFieldTypeAst(pkField, ctx);
747
- // db.deleteFrom('table').where('id', '=', id)
748
- let query = chain(deleteFrom(tableRef), "where", [
749
- str(pkColName),
750
- str("="),
751
- id(fieldName),
752
- ]);
753
- if (executeQueries) {
754
- query = chain(query, "execute");
755
- }
756
- // Destructured param: { id }: { id: number }
757
- const optionsParam = param.destructured([{ name: fieldName, type: fieldType }]);
758
- const dbParam = param.typed("db", ts.ref("Kysely", [ts.ref("DB")]));
759
- const params = dbAsParameter ? [optionsParam, dbParam] : [optionsParam];
760
- const fn = arrowFn(params, query);
761
- const name = exportName(entityName, "Remove");
762
- const meta = {
763
- name,
764
- kind: "delete",
765
- params: [{
766
- name: fieldName,
767
- type: getFieldTypeString(pkField, ctx),
768
- required: true,
769
- columnName: pkColName,
770
- source: "pk",
771
- }],
772
- returns: { type: "void", nullable: false, isArray: false },
773
- callSignature: { style: "named" },
774
- };
775
- return { name, fn, meta };
776
- };
777
- /** Generate all CRUD methods for an entity */
778
- const generateCrudMethods = (ctx) => [
779
- generateFindById(ctx),
780
- ctx.generateListMany ? generateListMany(ctx) : undefined,
781
- generateCreate(ctx),
782
- generateUpdate(ctx),
783
- generateDelete(ctx),
784
- ].filter((p) => p != null);
785
- // ============================================================================
786
- // Index-based Lookup Functions
787
- // ============================================================================
788
- /** Check if an index should generate a lookup function */
789
- const shouldGenerateLookup = (index) => !index.isPartial &&
790
- !index.hasExpressions &&
791
- index.columns.length === 1 &&
792
- index.method !== "gin" &&
793
- index.method !== "gist";
794
- /**
795
- * Generate the method name portion for an index-based lookup.
796
- * Uses semantic naming when the column corresponds to an FK relation.
797
- */
798
- const generateLookupMethodName = (entity, index, relation) => {
799
- // Use semantic name if FK relation exists, otherwise fall back to column name
800
- const columnName = index.columnNames[0];
801
- const byName = relation ? deriveSemanticName(relation, columnName) : index.columns[0];
802
- return `FindBy${toPascalCase(byName)}`;
803
- };
804
- /**
805
- * Generate a lookup method for a single-column index.
806
- * Uses semantic parameter naming when the column corresponds to an FK relation.
807
- */
808
- const generateLookupMethod = (index, ctx) => {
809
- const { entity, executeQueries, defaultSchemas, entityName, exportName, explicitColumns, dbAsParameter, } = ctx;
810
- const tableRef = getTableRef(entity, defaultSchemas);
811
- const columnName = index.columnNames[0];
812
- const field = findRowField(entity, columnName);
813
- const fieldName = field?.name ?? index.columns[0];
814
- const isUnique = isUniqueLookup(entity, index);
815
- // Check if this index column corresponds to an FK relation
816
- const relation = findRelationForColumn(entity, columnName);
817
- // Use semantic param name if FK relation exists, otherwise use field name
818
- const paramName = relation ? deriveSemanticName(relation, columnName) : fieldName;
819
- // For FK columns, use indexed access on Selectable<TableType> to get the unwrapped type
820
- // (Kysely's Generated<T> types need Selectable to unwrap for use in where clauses)
821
- // For regular columns, use the field's type directly
822
- // Both cases wrap in NonNullable since lookups require a concrete value
823
- const useSemanticNaming = relation !== undefined && paramName !== fieldName;
824
- const tableTypeName = getTableTypeName(entity);
825
- const selectableType = ts.ref("Selectable", [ts.ref(tableTypeName)]);
826
- const indexedType = ts.indexedAccess(selectableType, ts.literal(fieldName));
827
- // Lookup params must be non-nullable (you're searching FOR a value, not handling null)
828
- const paramType = ts.ref("NonNullable", [
829
- useSemanticNaming ? indexedType : getFieldTypeAst(field, ctx),
830
- ]);
831
- // db.selectFrom('table').select([...]).where('col', '=', value)
832
- let query = chain(selectColumns(selectFrom(tableRef), entity, explicitColumns), "where", [str(columnName), str("="), id(paramName)]);
833
- if (executeQueries) {
834
- query = chain(query, isUnique ? "executeTakeFirst" : "execute");
835
- }
836
- // Destructured param: { author }: { author: Selectable<Posts>["authorId"] }
837
- const optionsParam = param.destructured([{ name: paramName, type: paramType }]);
838
- const dbParam = param.typed("db", ts.ref("Kysely", [ts.ref("DB")]));
839
- const params = dbAsParameter ? [optionsParam, dbParam] : [optionsParam];
840
- const fn = arrowFn(params, query);
841
- const methodName = generateLookupMethodName(entity, index, relation);
842
- const name = exportName(entityName, methodName);
843
- const rowType = entity.shapes.row.name;
844
- const meta = {
845
- name,
846
- kind: "lookup",
847
- params: [{
848
- name: paramName,
849
- type: getFieldTypeString(field, ctx),
850
- required: true,
851
- columnName,
852
- source: relation ? "fk" : "lookup",
853
- }],
854
- returns: {
855
- type: rowType,
856
- nullable: isUnique,
857
- isArray: !isUnique,
858
- },
859
- lookupField: fieldName,
860
- isUniqueLookup: isUnique,
861
- callSignature: { style: "named" },
862
- };
863
- return { name, fn, meta };
864
- };
865
- /**
866
- * Check if a column is covered by a unique constraint (not just unique index).
867
- * This helps determine if a non-unique B-tree index on the column still
868
- * returns at most one row.
869
- */
870
- const columnHasUniqueConstraint = (entity, columnName) => {
871
- const constraints = entity.pgClass.getConstraints();
872
- return constraints.some(c => {
873
- // 'u' = unique constraint, 'p' = primary key
874
- if (c.contype !== "u" && c.contype !== "p")
875
- return false;
876
- // Single-column constraint on our column?
877
- const conkey = c.conkey ?? [];
878
- if (conkey.length !== 1)
879
- return false;
880
- // Find the attribute with this attnum
881
- const attrs = entity.pgClass.getAttributes();
882
- const attr = attrs.find(a => a.attnum === conkey[0]);
883
- return attr?.attname === columnName;
884
- });
885
- };
886
- /**
887
- * Determine if a lookup should be treated as unique (returns one row).
888
- * True if: index is unique, index is primary, OR column has unique constraint.
889
- */
890
- const isUniqueLookup = (entity, index) => {
891
- if (index.isUnique || index.isPrimary)
892
- return true;
893
- // Check if the single column has a unique constraint
894
- const columnName = index.columnNames[0];
895
- return columnName ? columnHasUniqueConstraint(entity, columnName) : false;
896
- };
897
- /** Generate lookup methods for all eligible indexes, deduplicating by column */
898
- const generateLookupMethods = (ctx) => {
899
- const eligibleIndexes = ctx.entity.indexes.filter(index => shouldGenerateLookup(index) && !index.isPrimary && ctx.entity.permissions.canSelect);
900
- // Group by column name, keeping only one index per column
901
- // Prefer unique indexes, but also consider columns with unique constraints
902
- const byColumn = new Map();
903
- for (const index of eligibleIndexes) {
904
- const columnName = index.columnNames[0];
905
- const existing = byColumn.get(columnName);
906
- if (!existing) {
907
- byColumn.set(columnName, index);
908
- }
909
- else {
910
- // Prefer explicitly unique index over non-unique
911
- if (index.isUnique && !existing.isUnique) {
912
- byColumn.set(columnName, index);
913
- }
914
- }
915
- }
916
- return Array.from(byColumn.values()).map(index => generateLookupMethod(index, ctx));
917
- };
918
- // ============================================================================
919
- // Export Style Helpers
920
- // ============================================================================
921
- /**
922
- * Convert MethodDef array to flat export statements.
923
- * Each method becomes: export const methodName = (db, ...) => ...
924
- */
925
- const toFlatExports = (methods) => methods.map(m => conjure.export.const(m.name, m.fn));
926
- /**
927
- * Convert MethodDef array to a single namespace object export.
928
- * All methods become: export const EntityName = { methodName: (db, ...) => ..., ... }
929
- */
930
- const toNamespaceExport = (entityName, methods) => {
931
- const properties = methods.map(m => b.objectProperty(id(m.name), m.fn));
932
- const obj = b.objectExpression(properties);
933
- return conjure.export.const(entityName, obj);
934
- };
935
- /**
936
- * Convert MethodDef array to statements based on export style.
937
- */
938
- const toStatements = (methods, exportStyle, entityName) => {
939
- if (methods.length === 0)
940
- return [];
941
- return exportStyle === "namespace"
942
- ? [toNamespaceExport(entityName, methods)]
943
- : toFlatExports(methods);
944
- };
945
- // ============================================================================
946
- // Core Generation Function
947
- // ============================================================================
948
- /**
949
- * Generate Kysely query functions from the IR.
950
- *
951
- * This is the core generation logic, extracted for use by both:
952
- * - The standalone kyselyQueriesPlugin
953
- * - The unified kyselyPlugin
954
- *
955
- * @param ctx - Plugin context for file emission and services
956
- * @param config - Queries configuration (with defaults applied)
957
- */
958
- export function generateKyselyQueries(ctx, config = {}) {
959
- // Apply defaults
960
- const resolvedConfig = {
961
- ...defaultQueriesConfig,
962
- exportName: defaultExportName,
963
- ...config,
964
- };
965
- const enums = getEnumEntities(ctx.ir);
966
- const defaultSchemas = ctx.ir.schemas;
967
- const { outputDir, dbTypesPath, executeQueries, generateListMany, generateFunctions, functionsFile, exportName, exportStyle, explicitColumns, dbAsParameter, header, defaultLimit, } = resolvedConfig;
968
- // Collectors for QueryArtifact emission
969
- const entityMethodsCollector = [];
970
- const functionMethodsCollector = [];
971
- // Pre-compute function groupings by return entity name
972
- // Functions returning entities go in that entity's file; scalars go in functions.ts
973
- const functionsByEntity = new Map();
974
- const scalarFunctions = [];
975
- if (generateFunctions) {
976
- const { queries, mutations } = getGeneratableFunctions(ctx.ir);
977
- const allFunctions = [...queries, ...mutations];
978
- for (const fn of allFunctions) {
979
- const resolved = resolveReturnType(fn, ctx.ir);
980
- if (resolved.returnEntity) {
981
- const entityName = resolved.returnEntity.name;
982
- const existing = functionsByEntity.get(entityName) ?? [];
983
- functionsByEntity.set(entityName, [...existing, fn]);
984
- }
985
- else {
986
- scalarFunctions.push(fn);
987
- }
988
- }
989
- }
990
- getTableEntities(ctx.ir)
991
- .filter(entity => entity.tags.omit !== true)
992
- .forEach(entity => {
993
- const entityName = ctx.inflection.entityName(entity.pgClass, entity.tags);
994
- const genCtx = {
995
- entity,
996
- enums,
997
- ir: ctx.ir,
998
- defaultSchemas,
999
- dbTypesPath,
1000
- executeQueries,
1001
- generateListMany,
1002
- entityName,
1003
- exportName,
1004
- explicitColumns,
1005
- dbAsParameter,
1006
- defaultLimit,
1007
- };
1008
- // Collect all methods for this entity
1009
- const methods = [
1010
- ...generateCrudMethods(genCtx),
1011
- ...generateLookupMethods(genCtx),
1012
- ];
1013
- // Get functions that return this entity
1014
- const entityFunctions = functionsByEntity.get(entity.name) ?? [];
1015
- for (const fn of entityFunctions) {
1016
- methods.push(generateFunctionWrapper(fn, ctx.ir, executeQueries, dbAsParameter));
1017
- }
1018
- if (methods.length === 0)
1019
- return;
1020
- // Use inline file naming: ${entityName}.ts
1021
- const filePath = `${outputDir}/${entityName}.ts`;
1022
- // Convert methods to statements based on export style
1023
- const statements = toStatements(methods, exportStyle, entityName);
1024
- const file = ctx.file(filePath);
1025
- // Add user-provided header if specified
1026
- if (header) {
1027
- file.header(header);
1028
- }
1029
- // Import Kysely type only when db is passed as parameter
1030
- if (dbAsParameter) {
1031
- file.import({ kind: "package", types: ["Kysely"], from: "kysely" });
1032
- file.import({ kind: "relative", types: ["DB"], from: dbTypesPath });
1033
- }
1034
- // Import Insertable/Updateable helper types and table type if we generate create/update
1035
- const tableTypeName = getTableTypeName(entity);
1036
- // Check if any lookup methods use semantic naming (FK with Selectable indexed access)
1037
- const hasSemanticLookups = entity.indexes.some(index => {
1038
- if (!shouldGenerateLookup(index) || index.isPrimary)
1039
- return false;
1040
- const columnName = index.columnNames[0];
1041
- const relation = findRelationForColumn(entity, columnName);
1042
- if (!relation)
1043
- return false;
1044
- const paramName = deriveSemanticName(relation, columnName);
1045
- const field = findRowField(entity, columnName);
1046
- const fieldName = field?.name ?? index.columns[0];
1047
- return paramName !== fieldName;
1048
- });
1049
- // Import table type if needed for Insertable/Updateable or semantic lookups
1050
- const needsTableType = entity.permissions.canInsert || entity.permissions.canUpdate || hasSemanticLookups;
1051
- if (needsTableType) {
1052
- file.import({ kind: "relative", types: [tableTypeName], from: dbTypesPath });
1053
- }
1054
- // Import Selectable if we have semantic lookups (for unwrapping Generated<T>)
1055
- if (hasSemanticLookups) {
1056
- file.import({ kind: "package", types: ["Selectable"], from: "kysely" });
1057
- }
1058
- if (entity.permissions.canInsert) {
1059
- file.import({ kind: "package", types: ["Insertable"], from: "kysely" });
1060
- }
1061
- if (entity.permissions.canUpdate) {
1062
- file.import({ kind: "package", types: ["Updateable"], from: "kysely" });
1063
- }
1064
- // Import types needed by function args (for functions grouped into this file)
1065
- if (entityFunctions.length > 0) {
1066
- const fnTypeImports = collectFunctionTypeImports(entityFunctions, ctx.ir);
1067
- // Remove the entity's own type (already in scope or self-referential)
1068
- fnTypeImports.delete(entity.name);
1069
- if (fnTypeImports.size > 0) {
1070
- file.import({ kind: "relative", types: [...fnTypeImports], from: dbTypesPath });
1071
- }
1072
- }
1073
- file.ast(conjure.program(...statements)).emit();
1074
- // Collect metadata for QueryArtifact
1075
- const pkField = entity.primaryKey?.columns[0]
1076
- ? findRowField(entity, entity.primaryKey.columns[0])
1077
- : undefined;
1078
- const pkType = pkField ? getFieldTypeString(pkField, genCtx) : undefined;
1079
- entityMethodsCollector.push({
1080
- entityName,
1081
- tableName: entity.pgName,
1082
- schemaName: entity.schemaName,
1083
- pkType,
1084
- hasCompositePk: (entity.primaryKey?.columns.length ?? 0) > 1,
1085
- methods: methods.map(m => m.meta),
1086
- });
1087
- });
1088
- // Generate files for composite types that have functions returning them
1089
- if (generateFunctions) {
1090
- const composites = getCompositeEntities(ctx.ir);
1091
- for (const composite of composites) {
1092
- const compositeFunctions = functionsByEntity.get(composite.name) ?? [];
1093
- if (compositeFunctions.length === 0)
1094
- continue;
1095
- const filePath = `${outputDir}/${composite.name}.ts`;
1096
- const methods = compositeFunctions.map(fn => generateFunctionWrapper(fn, ctx.ir, executeQueries, dbAsParameter));
1097
- const statements = toStatements(methods, exportStyle, composite.name);
1098
- const file = ctx.file(filePath);
1099
- // Add user-provided header if specified
1100
- if (header) {
1101
- file.header(header);
1102
- }
1103
- // Import Kysely type only when db is passed as parameter
1104
- if (dbAsParameter) {
1105
- file.import({ kind: "package", types: ["Kysely"], from: "kysely" });
1106
- file.import({ kind: "relative", types: ["DB"], from: dbTypesPath });
1107
- }
1108
- // Import the composite type and any types needed by function args
1109
- const fnTypeImports = collectFunctionTypeImports(compositeFunctions, ctx.ir);
1110
- fnTypeImports.add(composite.name); // Always import the composite type
1111
- file.import({ kind: "relative", types: [...fnTypeImports], from: dbTypesPath });
1112
- file.ast(conjure.program(...statements)).emit();
1113
- }
1114
- }
1115
- // Generate functions.ts for scalar-returning functions only
1116
- if (generateFunctions && scalarFunctions.length > 0) {
1117
- const filePath = `${outputDir}/${functionsFile}`;
1118
- const methods = scalarFunctions.map(fn => generateFunctionWrapper(fn, ctx.ir, executeQueries, dbAsParameter));
1119
- // For scalar functions, use "functions" as the namespace name
1120
- const statements = toStatements(methods, exportStyle, "functions");
1121
- const file = ctx.file(filePath);
1122
- // Add user-provided header if specified
1123
- if (header) {
1124
- file.header(header);
1125
- }
1126
- // Import Kysely type only when db is passed as parameter
1127
- if (dbAsParameter) {
1128
- file.import({ kind: "package", types: ["Kysely"], from: "kysely" });
1129
- file.import({ kind: "relative", types: ["DB"], from: dbTypesPath });
1130
- }
1131
- // Import any types needed for function args (scalars don't need return type imports)
1132
- const typeImports = collectFunctionTypeImports(scalarFunctions, ctx.ir);
1133
- if (typeImports.size > 0) {
1134
- file.import({ kind: "relative", types: [...typeImports], from: dbTypesPath });
1135
- }
1136
- file.ast(conjure.program(...statements)).emit();
1137
- // Collect scalar function metadata for QueryArtifact
1138
- for (const fn of scalarFunctions) {
1139
- const resolvedArgs = resolveArgs(fn, ctx.ir);
1140
- const resolvedReturn = resolveReturnType(fn, ctx.ir);
1141
- functionMethodsCollector.push({
1142
- functionName: fn.pgName,
1143
- exportName: fn.name,
1144
- schemaName: fn.schemaName,
1145
- volatility: fn.volatility,
1146
- params: resolvedArgs.map(arg => ({
1147
- name: arg.name,
1148
- type: arg.tsType,
1149
- required: !arg.isOptional,
1150
- })),
1151
- returns: {
1152
- type: resolvedReturn.tsType,
1153
- nullable: resolvedReturn.isScalar || !resolvedReturn.isArray,
1154
- isArray: resolvedReturn.isArray,
1155
- },
1156
- callSignature: { style: "named" },
1157
- });
1158
- }
1159
- }
1160
- // Emit the QueryArtifact for downstream plugins (e.g., http-elysia)
1161
- const artifact = {
1162
- entities: entityMethodsCollector,
1163
- functions: functionMethodsCollector,
1164
- sourcePlugin: "kysely-queries",
1165
- outputDir,
1166
- };
1167
- ctx.setArtifact("queries:kysely", artifact);
1168
- }
1169
- //# sourceMappingURL=queries.js.map