@danielfgray/pg-sourcerer 0.3.0 → 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,55 +1,38 @@
1
1
  /**
2
- * Kysely Plugin - Generate Kysely-compatible types and query functions
2
+ * Kysely Plugin - Unified Kysely types and query functions
3
3
  *
4
- * Consolidates types and queries generation into a single plugin with unified config.
5
4
  * Generates:
6
- * - Types: DB interface, table interfaces with Generated<T> wrappers, enum types
7
- * - Queries: CRUD functions, index lookups, stored function wrappers
5
+ * 1. Kysely-compatible type definitions (DB interface, table types with Generated<T>)
6
+ * 2. Type-safe CRUD query functions using Kysely's fluent API
8
7
  *
9
- * @example
10
- * ```typescript
11
- * import { kysely } from "pg-sourcerer"
12
- *
13
- * export default defineConfig({
14
- * plugins: [
15
- * kysely({
16
- * outputDir: "db",
17
- * generateQueries: true,
18
- * }),
19
- * ],
20
- * })
21
- * ```
8
+ * This plugin is incompatible with other type-generation plugins (types, zod, etc.)
9
+ * since it provides its own type definitions optimized for Kysely.
22
10
  */
23
- import { Option, pipe, Schema as S } from "effect";
24
- import { definePlugin } from "../services/plugin.js";
25
- import { findEnumByPgName, findCompositeByPgName, PgTypeOid } from "../services/pg-types.js";
26
- import { getEnumEntities, getTableEntities, getCompositeEntities, } from "../ir/semantic-ir.js";
27
- import { conjure, cast } from "../lib/conjure.js";
28
- import { resolveFieldType, tsTypeToAst } from "../lib/field-utils.js";
29
- import { inflect } from "../services/inflection.js";
30
- import { isEnumType, getPgTypeName } from "../lib/field-utils.js";
31
- import { getExtensionTypeMapping } from "../services/pg-types.js";
32
- const { ts, exp, b, param, str } = conjure;
33
- const { toExpr } = cast;
11
+ import { Effect } from "effect";
12
+ import { Schema as S } from "effect";
13
+ import { normalizeFileNaming } from "../runtime/file-assignment.js";
14
+ import { IR } from "../services/ir.js";
15
+ import { Inflection } from "../services/inflection.js";
16
+ import { getTableEntities, getEnumEntities, getCompositeEntities, getCursorPaginationCandidates, } from "../ir/semantic-ir.js";
17
+ import { conjure, cast } from "../conjure/index.js";
18
+ const { fn, stmt, ts, param, str, exp, b, chain, arrExpr } = conjure;
19
+ // ============================================================================
20
+ // Configuration
21
+ // ============================================================================
34
22
  const KyselyConfigSchema = S.Struct({
35
- outputDir: S.optionalWith(S.String, { default: () => "db" }),
36
- dbTypeName: S.optionalWith(S.String, { default: () => "DB" }),
37
- camelCase: S.optionalWith(S.Boolean, { default: () => true }),
38
- runtimeEnums: S.optionalWith(S.Boolean, { default: () => false }),
39
- typeOnlyImports: S.optionalWith(S.Boolean, { default: () => true }),
23
+ /** Generate query functions (default: true) */
40
24
  generateQueries: S.optionalWith(S.Boolean, { default: () => true }),
41
- dbTypesPath: S.optionalWith(S.String, { default: () => "./db.js" }),
42
- executeQueries: S.optionalWith(S.Boolean, { default: () => true }),
43
- generateListMany: S.optionalWith(S.Boolean, { default: () => false }),
44
- generateFunctions: S.optionalWith(S.Boolean, { default: () => true }),
45
- functionsFile: S.optionalWith(S.String, { default: () => "functions.ts" }),
46
- exportName: S.optional(S.Any),
47
- exportStyle: S.optionalWith(S.Literal("flat", "namespace"), { default: () => "flat" }),
48
- explicitColumns: S.optionalWith(S.Boolean, { default: () => true }),
49
- dbAsParameter: S.optionalWith(S.Boolean, { default: () => true }),
50
- header: S.optional(S.String),
25
+ /** If true, db is passed as first parameter; if false, imported via dbImport */
26
+ dbAsParameter: S.optionalWith(S.Boolean, { default: () => false }),
27
+ /** Default limit for list queries (default: 50) */
51
28
  defaultLimit: S.optionalWith(S.Number, { default: () => 50 }),
52
29
  });
30
+ // ============================================================================
31
+ // Kysely Type Helpers (ported from kysely-codegen)
32
+ // ============================================================================
33
+ /**
34
+ * Helper type definitions to be emitted in the types file.
35
+ */
53
36
  const GENERATED_TYPE_DEF = `T extends ColumnType<infer S, infer I, infer U>
54
37
  ? ColumnType<S, I | undefined, U>
55
38
  : ColumnType<T, T | undefined, T>`;
@@ -59,13 +42,21 @@ const ARRAY_TYPE_DEF = `ArrayTypeImpl<T> extends (infer U)[]
59
42
  const ARRAY_TYPE_IMPL_DEF = `T extends ColumnType<infer S, infer I, infer U>
60
43
  ? ColumnType<S[], I[], U[]>
61
44
  : T[]`;
45
+ // ============================================================================
46
+ // PostgreSQL Type Mappings
47
+ // ============================================================================
48
+ /** Simple scalar types: PG type → TS type builder */
62
49
  const SCALAR_TYPES = {
50
+ // Boolean
63
51
  bool: ts.boolean,
52
+ boolean: ts.boolean,
53
+ // Integers → number
64
54
  int2: ts.number,
65
55
  int4: ts.number,
66
56
  float4: ts.number,
67
57
  float8: ts.number,
68
58
  oid: ts.number,
59
+ // Text types → string
69
60
  text: ts.string,
70
61
  varchar: ts.string,
71
62
  bpchar: ts.string,
@@ -74,35 +65,46 @@ const SCALAR_TYPES = {
74
65
  bit: ts.string,
75
66
  varbit: ts.string,
76
67
  xml: ts.string,
68
+ citext: ts.string,
69
+ // UUID → string
77
70
  uuid: ts.string,
71
+ // Network types → string
78
72
  inet: ts.string,
79
73
  cidr: ts.string,
80
74
  macaddr: ts.string,
81
75
  macaddr8: ts.string,
76
+ // Geometric types → string
82
77
  line: ts.string,
83
78
  lseg: ts.string,
84
79
  box: ts.string,
85
80
  path: ts.string,
86
81
  polygon: ts.string,
82
+ // Time without date → string
87
83
  time: ts.string,
88
84
  timetz: ts.string,
85
+ // Full-text search → string
89
86
  tsvector: ts.string,
90
87
  tsquery: ts.string,
91
88
  txid_snapshot: ts.string,
89
+ // Money → string
92
90
  money: ts.string,
91
+ // Binary → Buffer
93
92
  bytea: () => ts.ref("Buffer"),
94
93
  };
95
94
  const COMPLEX_TYPES = {
95
+ // int8/bigint: returns string, accepts string|number|bigint
96
96
  int8: {
97
97
  select: ts.string,
98
98
  insert: () => ts.union(ts.string(), ts.number(), ts.bigint()),
99
99
  update: () => ts.union(ts.string(), ts.number(), ts.bigint()),
100
100
  },
101
+ // numeric/decimal: returns string, accepts number|string
101
102
  numeric: {
102
103
  select: ts.string,
103
104
  insert: () => ts.union(ts.number(), ts.string()),
104
105
  update: () => ts.union(ts.number(), ts.string()),
105
106
  },
107
+ // Timestamps: returns Date, accepts Date|string
106
108
  date: {
107
109
  select: () => ts.ref("Date"),
108
110
  insert: () => ts.union(ts.ref("Date"), ts.string()),
@@ -118,11 +120,13 @@ const COMPLEX_TYPES = {
118
120
  insert: () => ts.union(ts.ref("Date"), ts.string()),
119
121
  update: () => ts.union(ts.ref("Date"), ts.string()),
120
122
  },
123
+ // Interval: string for now
121
124
  interval: {
122
125
  select: ts.string,
123
126
  insert: () => ts.union(ts.string(), ts.number()),
124
127
  update: () => ts.union(ts.string(), ts.number()),
125
128
  },
129
+ // JSON: JsonValue
126
130
  json: {
127
131
  select: () => ts.ref("JsonValue"),
128
132
  insert: () => ts.ref("JsonValue"),
@@ -133,6 +137,7 @@ const COMPLEX_TYPES = {
133
137
  insert: () => ts.ref("JsonValue"),
134
138
  update: () => ts.ref("JsonValue"),
135
139
  },
140
+ // Point: object with x, y
136
141
  point: {
137
142
  select: () => ts.objectType([
138
143
  { name: "x", type: ts.number() },
@@ -147,73 +152,34 @@ const COMPLEX_TYPES = {
147
152
  { name: "y", type: ts.number() },
148
153
  ]),
149
154
  },
150
- circle: {
151
- select: () => ts.objectType([
152
- { name: "x", type: ts.number() },
153
- { name: "y", type: ts.number() },
154
- { name: "radius", type: ts.number() },
155
- ]),
156
- insert: () => ts.objectType([
157
- { name: "x", type: ts.number() },
158
- { name: "y", type: ts.number() },
159
- { name: "radius", type: ts.number() },
160
- ]),
161
- update: () => ts.objectType([
162
- { name: "x", type: ts.number() },
163
- { name: "y", type: ts.number() },
164
- { name: "radius", type: ts.number() },
165
- ]),
166
- },
167
155
  };
168
- // ============================================================================
169
- // Helper Functions (Types)
170
- // ============================================================================
171
- const buildFieldMatch = (field, ctx) => ({
172
- schema: ctx.schemaName,
173
- table: ctx.tableName,
174
- column: field.columnName,
175
- pgType: field.isArray && field.elementTypeName
176
- ? field.elementTypeName
177
- : field.pgAttribute.getType()?.typname ?? "",
178
- });
179
- function resolveFieldTypeForKysely(field, ctx) {
156
+ /**
157
+ * Resolve a field to its Kysely type.
158
+ */
159
+ function resolveFieldType(field, ctx) {
180
160
  const pgType = field.pgAttribute.getType();
181
161
  const typeName = pgType?.typname ?? "";
182
- const fieldMatch = buildFieldMatch(field, ctx);
183
- const tsTypeHint = ctx.typeHints.getHint(fieldMatch, "tsType");
184
- if (Option.isSome(tsTypeHint)) {
185
- const typeName = tsTypeHint.value;
186
- const importPath = ctx.typeHints.getHint(fieldMatch, "import");
187
- return {
188
- selectType: ts.ref(typeName),
189
- needsColumnType: false,
190
- externalImport: pipe(importPath, Option.map(path => ({ name: typeName, from: path })), Option.getOrUndefined),
191
- };
192
- }
193
- if (isEnumType(field)) {
194
- const enumName = getPgTypeName(field);
195
- if (enumName) {
196
- const enumDef = findEnumByPgName(ctx.enums, enumName);
197
- if (Option.isSome(enumDef)) {
198
- return {
199
- selectType: ts.ref(enumDef.value.name),
200
- needsColumnType: false,
201
- };
202
- }
162
+ // Check if it's an enum
163
+ if (pgType?.typtype === "e") {
164
+ const enumDef = ctx.enums.find(e => e.pgType.typname === typeName);
165
+ if (enumDef) {
166
+ return {
167
+ selectType: ts.ref(enumDef.name),
168
+ needsColumnType: false,
169
+ };
203
170
  }
204
171
  }
172
+ // Check if it's a composite type
205
173
  if (pgType?.typtype === "c") {
206
- const compositeName = getPgTypeName(field);
207
- if (compositeName) {
208
- const compositeDef = findCompositeByPgName(ctx.composites, compositeName);
209
- if (Option.isSome(compositeDef)) {
210
- return {
211
- selectType: ts.ref(compositeDef.value.name),
212
- needsColumnType: false,
213
- };
214
- }
174
+ const compositeDef = ctx.composites.find(c => c.pgType.typname === typeName);
175
+ if (compositeDef) {
176
+ return {
177
+ selectType: ts.ref(compositeDef.name),
178
+ needsColumnType: false,
179
+ };
215
180
  }
216
181
  }
182
+ // Check complex types (need ColumnType wrapper)
217
183
  const complexType = COMPLEX_TYPES[typeName];
218
184
  if (complexType) {
219
185
  return {
@@ -221,9 +187,9 @@ function resolveFieldTypeForKysely(field, ctx) {
221
187
  insertType: complexType.insert(),
222
188
  updateType: complexType.update(),
223
189
  needsColumnType: true,
224
- externalImport: complexType.import,
225
190
  };
226
191
  }
192
+ // Check simple scalar types
227
193
  const scalarBuilder = SCALAR_TYPES[typeName];
228
194
  if (scalarBuilder) {
229
195
  return {
@@ -231,32 +197,51 @@ function resolveFieldTypeForKysely(field, ctx) {
231
197
  needsColumnType: false,
232
198
  };
233
199
  }
234
- if (pgType) {
235
- const extType = getExtensionTypeMapping(typeName, String(pgType.typnamespace), ctx.extensions);
236
- if (Option.isSome(extType)) {
237
- return {
238
- selectType: ts.fromString(extType.value),
239
- needsColumnType: false,
240
- };
241
- }
242
- }
200
+ // Default to string
243
201
  return {
244
202
  selectType: ts.string(),
245
203
  needsColumnType: false,
246
204
  };
247
205
  }
206
+ /**
207
+ * Determine if a field should be wrapped in Generated<T>.
208
+ *
209
+ * A field needs Generated<T> wrapper (making it optional in Insertable<T>) if:
210
+ * 1. The role cannot insert this field (permission-denied → treat as if generated)
211
+ * 2. The field has a database default and is an identity/generated column
212
+ * 3. The field has a default that will be used if not provided
213
+ *
214
+ * This ensures Kysely's Insertable<T> aligns with IR insert shapes which
215
+ * exclude fields the role cannot insert.
216
+ */
217
+ function isGeneratedField(field) {
218
+ // If role can't insert this field, make it optional in Insertable<T>
219
+ // This matches IR behavior where such fields are excluded from insert shape
220
+ if (!field.permissions.canInsert)
221
+ return true;
222
+ // Fields with any kind of default are optional on insert
223
+ // (identity columns, generated columns, or any DEFAULT value)
224
+ if (field.hasDefault)
225
+ return true;
226
+ if (field.isIdentity)
227
+ return true;
228
+ if (field.isGenerated)
229
+ return true;
230
+ return false;
231
+ }
232
+ /**
233
+ * Build the final field type with array/nullable/Generated wrappers.
234
+ */
248
235
  function buildFieldType(field, kyselyType, needsGenerated) {
249
236
  let baseType;
237
+ // If complex type, wrap in ColumnType<S, I, U>
250
238
  if (kyselyType.needsColumnType && kyselyType.insertType && kyselyType.updateType) {
251
- baseType = ts.ref("ColumnType", [
252
- kyselyType.selectType,
253
- kyselyType.insertType,
254
- kyselyType.updateType,
255
- ]);
239
+ baseType = ts.ref("ColumnType", [kyselyType.selectType, kyselyType.insertType, kyselyType.updateType]);
256
240
  }
257
241
  else {
258
242
  baseType = kyselyType.selectType;
259
243
  }
244
+ // Wrap in array if needed
260
245
  if (field.isArray) {
261
246
  if (kyselyType.needsColumnType) {
262
247
  baseType = ts.ref("ArrayType", [baseType]);
@@ -265,363 +250,81 @@ function buildFieldType(field, kyselyType, needsGenerated) {
265
250
  baseType = ts.array(baseType);
266
251
  }
267
252
  }
253
+ // Wrap in nullable if needed
268
254
  if (field.nullable) {
269
- baseType = ts.nullable(baseType);
255
+ baseType = ts.union(baseType, ts.null());
270
256
  }
257
+ // Wrap in Generated<T> if field has default and is not insertable
271
258
  if (needsGenerated) {
272
259
  baseType = ts.ref("Generated", [baseType]);
273
260
  }
274
261
  return baseType;
275
262
  }
276
- function isGeneratedField(field) {
277
- // Identity and generated columns are always "generated" regardless of hasDefault
278
- if (field.isIdentity)
279
- return true;
280
- if (field.isGenerated)
281
- return true;
282
- if (!field.hasDefault)
283
- return false;
284
- if (!field.permissions.canInsert)
285
- return true;
286
- const pgType = field.pgAttribute.getType();
287
- if (!pgType)
288
- return false;
289
- const typeOid = Number(pgType._id);
290
- const isIntegerType = typeOid === PgTypeOid.Int2
291
- || typeOid === PgTypeOid.Int4
292
- || typeOid === PgTypeOid.Int8;
293
- if (isIntegerType && field.hasDefault) {
294
- const constraints = field.pgAttribute.getClass()?.getConstraints() ?? [];
295
- const isPrimaryKey = constraints.some(c => c.contype === "p" && c.conkey?.includes(field.pgAttribute.attnum));
296
- if (isPrimaryKey)
297
- return true;
298
- }
299
- return false;
300
- }
301
263
  // ============================================================================
302
- // Helper Functions (Queries)
264
+ // Type Generation
303
265
  // ============================================================================
304
- const getTableTypeName = (entity) => entity.name;
305
- const getTableRef = (entity, defaultSchemas) => defaultSchemas.includes(entity.schemaName)
306
- ? entity.pgName
307
- : `${entity.schemaName}.${entity.pgName}`;
308
- const findRowField = (entity, columnName) => entity.shapes.row.fields.find(f => f.columnName === columnName);
309
- const getFieldTypeAst = (field, ctx) => {
310
- if (!field)
311
- return ts.string();
312
- const resolved = resolveFieldType(field, ctx.enums, ctx.ir.extensions);
313
- return resolved.enumDef ? ts.ref(resolved.enumDef.name) : tsTypeToAst(resolved.tsType);
314
- };
315
- const getFieldTypeString = (field, ctx) => {
316
- if (!field)
317
- return "string";
318
- const resolved = resolveFieldType(field, ctx.enums, ctx.ir.extensions);
319
- return resolved.enumDef ? resolved.enumDef.name : resolved.tsType;
320
- };
321
- const toPascalCase = (s) => inflect.pascalCase(s);
322
- const id = (name) => b.identifier(name);
323
- const call = (obj, method, args = []) => b.callExpression(b.memberExpression(toExpr(obj), id(method)), args.map(toExpr));
324
- const selectFrom = (tableRef) => call(id("db"), "selectFrom", [str(tableRef)]);
325
- const selectColumns = (base, entity, explicitColumns) => explicitColumns
326
- ? call(base, "select", [b.arrayExpression(entity.shapes.row.fields.map(f => str(f.columnName)))])
327
- : call(base, "selectAll");
328
- const insertInto = (tableRef) => call(id("db"), "insertInto", [str(tableRef)]);
329
- const updateTable = (tableRef) => call(id("db"), "updateTable", [str(tableRef)]);
330
- const deleteFrom = (tableRef) => call(id("db"), "deleteFrom", [str(tableRef)]);
331
- const chain = (expr, method, args = []) => call(expr, method, args);
332
- const arrowFn = (params, body) => {
333
- const fn = b.arrowFunctionExpression(params.map(p => p), toExpr(body));
334
- return fn;
335
- };
336
- const pgTypeNameToTs = (typeName) => {
337
- const baseName = typeName.includes(".") ? typeName.split(".").pop() : typeName;
338
- switch (baseName) {
339
- case "bool":
340
- case "boolean":
341
- return "boolean";
342
- case "int2":
343
- case "smallint":
344
- case "int4":
345
- case "integer":
346
- case "int":
347
- case "oid":
348
- case "float4":
349
- case "real":
350
- case "float8":
351
- case "double precision":
352
- return "number";
353
- case "int8":
354
- case "bigint":
355
- case "numeric":
356
- case "decimal":
357
- case "money":
358
- return "string";
359
- case "text":
360
- case "varchar":
361
- case "character varying":
362
- case "char":
363
- case "character":
364
- case "bpchar":
365
- case "name":
366
- case "xml":
367
- case "bit":
368
- case "varbit":
369
- case "bit varying":
370
- case "uuid":
371
- case "inet":
372
- case "cidr":
373
- case "macaddr":
374
- case "macaddr8":
375
- case "time":
376
- case "timetz":
377
- case "time with time zone":
378
- case "time without time zone":
379
- case "interval":
380
- return "string";
381
- case "date":
382
- case "timestamp":
383
- case "timestamptz":
384
- case "timestamp with time zone":
385
- case "timestamp without time zone":
386
- return "Date";
387
- case "json":
388
- case "jsonb":
389
- case "jsonpath":
390
- return "unknown";
391
- case "bytea":
392
- return "Buffer";
393
- case "void":
394
- return "void";
395
- default:
396
- return "unknown";
397
- }
398
- };
399
- // ============================================================================
400
- // Query Method Generators
401
- // ============================================================================
402
- const generateFindById = (ctx) => {
403
- const { entity, executeQueries, defaultSchemas, entityName, exportName, explicitColumns, dbAsParameter } = ctx;
404
- if (!entity.primaryKey || !entity.permissions.canSelect)
405
- return undefined;
406
- const pkColName = entity.primaryKey.columns[0];
407
- const pkField = findRowField(entity, pkColName);
408
- if (!pkField)
409
- return undefined;
410
- const tableRef = getTableRef(entity, defaultSchemas);
411
- const fieldName = pkField.name;
412
- const fieldType = getFieldTypeAst(pkField, ctx);
413
- let query = chain(selectColumns(selectFrom(tableRef), entity, explicitColumns), "where", [str(pkColName), str("="), id(fieldName)]);
414
- if (executeQueries) {
415
- query = chain(query, "executeTakeFirst");
416
- }
417
- const optionsParam = param.destructured([{ name: fieldName, type: fieldType }]);
418
- const dbParam = param.typed("db", ts.ref("Kysely", [ts.ref("DB")]));
419
- const params = dbAsParameter ? [optionsParam, dbParam] : [optionsParam];
420
- const fn = arrowFn(params, query);
421
- const name = exportName(entityName, "FindById");
422
- const rowType = entity.shapes.row.name;
423
- const meta = {
424
- name,
425
- kind: "read",
426
- params: [{
427
- name: fieldName,
428
- type: getFieldTypeString(pkField, ctx),
429
- required: true,
430
- columnName: pkColName,
431
- source: "pk",
432
- }],
433
- returns: { type: rowType, nullable: true, isArray: false },
434
- callSignature: { style: "named" },
435
- };
436
- return { name, fn, meta };
437
- };
438
- const generateListMany = (ctx) => {
439
- const { entity, executeQueries, defaultSchemas, entityName, exportName, explicitColumns, dbAsParameter, defaultLimit } = ctx;
440
- if (!entity.permissions.canSelect)
441
- return undefined;
442
- const tableRef = getTableRef(entity, defaultSchemas);
443
- let query = chain(chain(selectColumns(selectFrom(tableRef), entity, explicitColumns), "limit", [id("limit")]), "offset", [id("offset")]);
444
- if (executeQueries) {
445
- query = chain(query, "execute");
446
- }
447
- const optionsParam = param.destructured([
448
- { name: "limit", type: ts.number(), optional: true, defaultValue: b.numericLiteral(defaultLimit) },
449
- { name: "offset", type: ts.number(), optional: true, defaultValue: b.numericLiteral(0) },
450
- ]);
451
- const dbParam = param.typed("db", ts.ref("Kysely", [ts.ref("DB")]));
452
- const params = dbAsParameter ? [optionsParam, dbParam] : [optionsParam];
453
- const fn = arrowFn(params, query);
454
- const name = exportName(entityName, "ListMany");
455
- const rowType = entity.shapes.row.name;
456
- const meta = {
457
- name,
458
- kind: "list",
459
- params: [
460
- { name: "limit", type: "number", required: false, source: "pagination" },
461
- { name: "offset", type: "number", required: false, source: "pagination" },
462
- ],
463
- returns: { type: rowType, nullable: false, isArray: true },
464
- callSignature: { style: "named" },
465
- };
466
- return { name, fn, meta };
467
- };
468
- const generateCreate = (ctx) => {
469
- const { entity, executeQueries, defaultSchemas, entityName, exportName, dbAsParameter } = ctx;
470
- if (!entity.permissions.canInsert)
471
- return undefined;
472
- const tableRef = getTableRef(entity, defaultSchemas);
473
- const tableTypeName = getTableTypeName(entity);
474
- let query = chain(chain(insertInto(tableRef), "values", [id("data")]), "returningAll");
475
- if (executeQueries) {
476
- query = chain(query, "executeTakeFirstOrThrow");
266
+ /**
267
+ * Generate enum type alias: `export type Status = "active" | "inactive"`
268
+ */
269
+ function generateEnumType(enumEntity) {
270
+ return exp.typeAlias(enumEntity.name, { capability: "types:kysely", entity: enumEntity.name }, ts.union(...enumEntity.values.map(v => ts.literal(v)))).node;
271
+ }
272
+ /**
273
+ * Generate composite type interface.
274
+ */
275
+ function generateCompositeInterface(composite, ctx) {
276
+ const properties = [];
277
+ for (const field of composite.fields) {
278
+ const kyselyType = resolveFieldType(field, ctx);
279
+ const fieldType = buildFieldType(field, kyselyType, false);
280
+ properties.push({ name: field.name, type: fieldType });
477
281
  }
478
- const optionsParam = param.destructured([
479
- { name: "data", type: ts.ref("Insertable", [ts.ref(tableTypeName)]) },
480
- ]);
481
- const dbParam = param.typed("db", ts.ref("Kysely", [ts.ref("DB")]));
482
- const params = dbAsParameter ? [optionsParam, dbParam] : [optionsParam];
483
- const fn = arrowFn(params, query);
484
- const name = exportName(entityName, "Create");
485
- const rowType = entity.shapes.row.name;
486
- const insertType = `Insertable<${tableTypeName}>`;
487
- const meta = {
488
- name,
489
- kind: "create",
490
- params: [{
491
- name: "data",
492
- type: insertType,
493
- required: true,
494
- source: "body",
495
- }],
496
- returns: { type: rowType, nullable: false, isArray: false },
497
- callSignature: { style: "named", bodyStyle: "property" },
498
- };
499
- return { name, fn, meta };
500
- };
501
- const generateUpdate = (ctx) => {
502
- const { entity, executeQueries, defaultSchemas, entityName, exportName, dbAsParameter } = ctx;
503
- if (!entity.primaryKey || !entity.permissions.canUpdate)
504
- return undefined;
505
- const pkColName = entity.primaryKey.columns[0];
506
- const pkField = findRowField(entity, pkColName);
507
- if (!pkField)
508
- return undefined;
509
- const tableRef = getTableRef(entity, defaultSchemas);
510
- const fieldName = pkField.name;
511
- const fieldType = getFieldTypeAst(pkField, ctx);
512
- const tableTypeName = getTableTypeName(entity);
513
- let query = chain(chain(chain(updateTable(tableRef), "set", [id("data")]), "where", [
514
- str(pkColName),
515
- str("="),
516
- id(fieldName),
517
- ]), "returningAll");
518
- if (executeQueries) {
519
- query = chain(query, "executeTakeFirstOrThrow");
282
+ return exp.interface(composite.name, { capability: "types:kysely", entity: composite.name }, properties).node;
283
+ }
284
+ /**
285
+ * Generate table interface with all column types.
286
+ */
287
+ function generateTableInterface(entity, ctx) {
288
+ const properties = [];
289
+ for (const field of entity.shapes.row.fields) {
290
+ if (!field.permissions.canSelect)
291
+ continue;
292
+ const kyselyType = resolveFieldType(field, ctx);
293
+ const needsGenerated = isGeneratedField(field);
294
+ const fieldType = buildFieldType(field, kyselyType, needsGenerated);
295
+ properties.push({ name: field.name, type: fieldType });
520
296
  }
521
- const optionsParam = param.destructured([
522
- { name: fieldName, type: fieldType },
523
- { name: "data", type: ts.ref("Updateable", [ts.ref(tableTypeName)]) },
524
- ]);
525
- const dbParam = param.typed("db", ts.ref("Kysely", [ts.ref("DB")]));
526
- const params = dbAsParameter ? [optionsParam, dbParam] : [optionsParam];
527
- const fn = arrowFn(params, query);
528
- const name = exportName(entityName, "Update");
529
- const rowType = entity.shapes.row.name;
530
- const updateType = `Updateable<${tableTypeName}>`;
531
- const meta = {
532
- name,
533
- kind: "update",
534
- params: [
535
- {
536
- name: fieldName,
537
- type: getFieldTypeString(pkField, ctx),
538
- required: true,
539
- columnName: pkColName,
540
- source: "pk",
541
- },
542
- {
543
- name: "data",
544
- type: updateType,
545
- required: true,
546
- source: "body",
547
- },
548
- ],
549
- returns: { type: rowType, nullable: false, isArray: false },
550
- callSignature: { style: "named", bodyStyle: "property" },
551
- };
552
- return { name, fn, meta };
553
- };
554
- const generateDelete = (ctx) => {
555
- const { entity, executeQueries, defaultSchemas, entityName, exportName, dbAsParameter } = ctx;
556
- if (!entity.primaryKey || !entity.permissions.canDelete)
557
- return undefined;
558
- const pkColName = entity.primaryKey.columns[0];
559
- const pkField = findRowField(entity, pkColName);
560
- if (!pkField)
561
- return undefined;
562
- const tableRef = getTableRef(entity, defaultSchemas);
563
- const fieldName = pkField.name;
564
- const fieldType = getFieldTypeAst(pkField, ctx);
565
- let query = chain(deleteFrom(tableRef), "where", [
566
- str(pkColName),
567
- str("="),
568
- id(fieldName),
569
- ]);
570
- if (executeQueries) {
571
- query = chain(query, "execute");
297
+ return exp.interface(entity.name, { capability: "types:kysely", entity: entity.name }, properties).node;
298
+ }
299
+ /**
300
+ * Generate DB interface: `export interface DB { table_name: TableType }`
301
+ */
302
+ function generateDBInterface(entities, defaultSchemas) {
303
+ const properties = [];
304
+ for (const entity of entities) {
305
+ if (!entity.permissions.canSelect)
306
+ continue;
307
+ // Use schema-qualified key if not in default schema
308
+ const key = defaultSchemas.includes(entity.schemaName) ? entity.pgName : `${entity.schemaName}.${entity.pgName}`;
309
+ properties.push({ name: key, type: ts.ref(entity.name) });
572
310
  }
573
- const optionsParam = param.destructured([{ name: fieldName, type: fieldType }]);
574
- const dbParam = param.typed("db", ts.ref("Kysely", [ts.ref("DB")]));
575
- const params = dbAsParameter ? [optionsParam, dbParam] : [optionsParam];
576
- const fn = arrowFn(params, query);
577
- const name = exportName(entityName, "Remove");
578
- const meta = {
579
- name,
580
- kind: "delete",
581
- params: [{
582
- name: fieldName,
583
- type: getFieldTypeString(pkField, ctx),
584
- required: true,
585
- columnName: pkColName,
586
- source: "pk",
587
- }],
588
- returns: { type: "void", nullable: false, isArray: false },
589
- callSignature: { style: "named" },
590
- };
591
- return { name, fn, meta };
592
- };
593
- const generateCrudMethods = (ctx) => [
594
- generateFindById(ctx),
595
- ctx.generateListMany ? generateListMany(ctx) : undefined,
596
- generateCreate(ctx),
597
- generateUpdate(ctx),
598
- generateDelete(ctx),
599
- ].filter((p) => p != null);
600
- const collectImports = (entities, composites, ctx) => {
601
- const kyselyImports = new Set();
602
- const externalImports = new Map();
603
- let needsJsonTypes = false;
604
- let needsArrayType = false;
311
+ // Sort by key for stable output
312
+ properties.sort((a, b) => a.name.localeCompare(b.name));
313
+ return exp.interface("DB", { capability: "types:kysely", entity: "DB" }, properties).node;
314
+ }
315
+ function collectTypeImports(entities, composites, ctx) {
316
+ let needsColumnType = false;
605
317
  let needsGenerated = false;
606
- const processField = (field, schemaName, tableName, checkGenerated) => {
607
- const kyselyType = resolveFieldTypeForKysely(field, {
608
- ...ctx,
609
- schemaName,
610
- tableName,
611
- });
318
+ let needsArrayType = false;
319
+ let needsJsonTypes = false;
320
+ const processField = (field, checkGenerated) => {
321
+ const pgType = field.pgAttribute.getType();
322
+ const typeName = pgType?.typname ?? "";
323
+ const kyselyType = resolveFieldType(field, ctx);
612
324
  if (kyselyType.needsColumnType) {
613
- kyselyImports.add("ColumnType");
614
- }
615
- if (kyselyType.externalImport) {
616
- const { name, from } = kyselyType.externalImport;
617
- if (!externalImports.has(from)) {
618
- externalImports.set(from, new Set());
619
- }
620
- externalImports.get(from).add(name);
325
+ needsColumnType = true;
621
326
  }
622
- // Note: Enum types are generated in the same file, no import needed
623
- const pgType = field.pgAttribute.getType();
624
- if (pgType?.typname === "json" || pgType?.typname === "jsonb") {
327
+ if (typeName === "json" || typeName === "jsonb") {
625
328
  needsJsonTypes = true;
626
329
  }
627
330
  if (field.isArray && kyselyType.needsColumnType) {
@@ -637,270 +340,706 @@ const collectImports = (entities, composites, ctx) => {
637
340
  for (const field of entity.shapes.row.fields) {
638
341
  if (!field.permissions.canSelect)
639
342
  continue;
640
- processField(field, entity.schemaName, entity.pgName, true);
343
+ processField(field, true);
641
344
  }
642
345
  }
643
346
  for (const composite of composites) {
644
347
  for (const field of composite.fields) {
645
- processField(field, composite.schemaName, composite.pgName, false);
348
+ processField(field, false);
646
349
  }
647
350
  }
648
- return {
649
- kyselyImports,
650
- externalImports,
651
- needsJsonTypes,
652
- needsArrayType,
653
- needsGenerated,
654
- };
655
- };
656
- // ============================================================================
657
- // Statement Generators (Types)
658
- // ============================================================================
659
- const generateEnumStatement = (enumEntity) => {
660
- return exp.typeAlias(enumEntity.name, { capability: "types", entity: enumEntity.name }, ts.union(...enumEntity.values.map(v => ts.literal(v))));
661
- };
662
- const generateTableInterface = (entity, ctx) => {
663
- const properties = [];
664
- for (const field of entity.shapes.row.fields) {
665
- if (!field.permissions.canSelect)
666
- continue;
667
- const kyselyType = resolveFieldTypeForKysely(field, ctx);
668
- const needsGenerated = isGeneratedField(field);
669
- const fieldType = buildFieldType(field, kyselyType, needsGenerated);
670
- properties.push({
671
- name: field.name,
672
- type: fieldType,
673
- });
351
+ return { needsColumnType, needsGenerated, needsArrayType, needsJsonTypes };
352
+ }
353
+ /**
354
+ * Build the helper types header string.
355
+ */
356
+ function buildTypesHeader(imports) {
357
+ const lines = [];
358
+ if (imports.needsGenerated) {
359
+ lines.push(`export type Generated<T> = ${GENERATED_TYPE_DEF};`);
674
360
  }
675
- return exp.interface(entity.name, { capability: "types", entity: entity.name }, properties);
676
- };
677
- const generateCompositeInterface = (composite, ctx) => {
678
- const properties = [];
679
- for (const field of composite.fields) {
680
- const kyselyType = resolveFieldTypeForKysely(field, ctx);
681
- const fieldType = buildFieldType(field, kyselyType, false);
682
- properties.push({
683
- name: field.name,
684
- type: fieldType,
685
- });
361
+ if (imports.needsArrayType) {
362
+ lines.push(`export type ArrayType<T> = ${ARRAY_TYPE_DEF};`);
363
+ lines.push(`export type ArrayTypeImpl<T> = ${ARRAY_TYPE_IMPL_DEF};`);
686
364
  }
687
- return exp.interface(composite.name, { capability: "types", entity: composite.name }, properties);
688
- };
689
- const generateDBInterface = (entities, defaultSchemas, dbTypeName) => {
690
- const properties = [];
691
- for (const entity of entities) {
692
- if (!entity.permissions.canSelect)
693
- continue;
694
- const key = defaultSchemas.includes(entity.schemaName)
695
- ? entity.pgName
696
- : `${entity.schemaName}.${entity.pgName}`;
697
- properties.push({
698
- name: key,
699
- type: ts.ref(entity.name),
700
- });
365
+ if (imports.needsJsonTypes) {
366
+ lines.push(`export type JsonPrimitive = boolean | number | string | null;`);
367
+ lines.push(`export type JsonObject = { [x: string]: JsonValue | undefined };`);
368
+ lines.push(`export type JsonArray = JsonValue[];`);
369
+ lines.push(`export type JsonValue = JsonArray | JsonObject | JsonPrimitive;`);
701
370
  }
702
- properties.sort((a, b) => a.name.localeCompare(b.name));
703
- return exp.interface(dbTypeName, { capability: "types", entity: dbTypeName }, properties);
704
- };
371
+ return lines.join("\n\n");
372
+ }
705
373
  // ============================================================================
706
- // Export Style Helpers (Queries)
374
+ // Query Generation Helpers
707
375
  // ============================================================================
708
- const toFlatExports = (methods) => methods.map(m => conjure.export.const(m.name, m.fn));
709
- const toNamespaceExport = (entityName, methods) => {
710
- const properties = methods.map(m => b.objectProperty(id(m.name), m.fn));
711
- const obj = b.objectExpression(properties);
712
- return conjure.export.const(entityName, obj);
713
- };
714
- const toStatements = (methods, exportStyle, entityName) => {
715
- if (methods.length === 0)
716
- return [];
717
- return exportStyle === "namespace"
718
- ? [toNamespaceExport(entityName, methods)]
719
- : toFlatExports(methods);
720
- };
376
+ function buildColumnArray(fields) {
377
+ return conjure.arr(...fields.map(f => str(f.columnName))).build();
378
+ }
379
+ function buildQueryName(inflection, entityName, operation) {
380
+ return inflection.variableName(entityName, operation);
381
+ }
382
+ function buildFindByName(inflection, entityName, columnName) {
383
+ return inflection.variableName(entityName, `FindBy${inflection.pascalCase(columnName)}`);
384
+ }
385
+ function buildListByName(inflection, entityName, columnName) {
386
+ return inflection.variableName(entityName, `ListBy${inflection.pascalCase(columnName)}`);
387
+ }
388
+ function getPgType(field) {
389
+ const pgType = field.pgAttribute.getType();
390
+ return pgType?.typname ?? "unknown";
391
+ }
392
+ function pgTypeToTsType(pgType) {
393
+ const lower = pgType.toLowerCase();
394
+ if (["uuid", "text", "varchar", "char", "citext", "name"].includes(lower))
395
+ return "string";
396
+ if (["int2", "int4", "int8", "integer", "smallint", "bigint", "numeric", "decimal", "real", "float4", "float8"].includes(lower))
397
+ return "number";
398
+ if (["bool", "boolean"].includes(lower))
399
+ return "boolean";
400
+ if (["timestamp", "timestamptz", "date"].includes(lower))
401
+ return "Date";
402
+ if (["json", "jsonb"].includes(lower))
403
+ return "unknown";
404
+ return "string";
405
+ }
406
+ function buildPkParam(field) {
407
+ return {
408
+ name: field.name,
409
+ type: pgTypeToTsType(getPgType(field)),
410
+ required: true,
411
+ columnName: field.columnName,
412
+ source: "pk",
413
+ };
414
+ }
415
+ function buildLookupParam(field) {
416
+ return {
417
+ name: field.name,
418
+ type: pgTypeToTsType(getPgType(field)),
419
+ required: true,
420
+ columnName: field.columnName,
421
+ source: "lookup",
422
+ };
423
+ }
424
+ function buildBodyParam(entityName, shape) {
425
+ const wrapper = shape === "insert" ? "Insertable" : "Updateable";
426
+ return {
427
+ name: "data",
428
+ type: `${wrapper}<${entityName}>`,
429
+ wrapper,
430
+ entityType: entityName,
431
+ required: true,
432
+ source: "body",
433
+ };
434
+ }
435
+ function buildPaginationParams(defaultLimit) {
436
+ return [
437
+ { name: "limit", type: "number", required: false, defaultValue: defaultLimit, source: "pagination" },
438
+ { name: "offset", type: "number", required: false, defaultValue: 0, source: "pagination" },
439
+ ];
440
+ }
441
+ function buildReturnType(entityName, isArray, nullable) {
442
+ return {
443
+ type: entityName,
444
+ nullable,
445
+ isArray,
446
+ };
447
+ }
448
+ function isBodyParam(p) {
449
+ return "wrapper" in p;
450
+ }
451
+ function isPaginationParam(p) {
452
+ return "defaultValue" in p;
453
+ }
454
+ function buildParamType(p) {
455
+ if (isBodyParam(p)) {
456
+ return ts.ref(p.wrapper, [ts.ref(p.entityType)]);
457
+ }
458
+ return ts.ref(p.type);
459
+ }
460
+ function buildDestructuredParam(params) {
461
+ return param.destructured(params.map(p => ({
462
+ name: p.name,
463
+ type: buildParamType(p),
464
+ optional: "required" in p ? p.required === false : false,
465
+ defaultValue: isPaginationParam(p) ? conjure.num(p.defaultValue) : undefined,
466
+ })));
467
+ }
721
468
  // ============================================================================
722
- // Main Generation Function
469
+ // Plugin Definition
723
470
  // ============================================================================
471
+ /**
472
+ * Kysely plugin - generates Kysely-compatible types and query functions.
473
+ *
474
+ * Capabilities provided:
475
+ * - `types:kysely:DB` - the DB interface
476
+ * - `types:kysely:EntityName` - table/composite interfaces
477
+ * - `queries:kysely:EntityName:operation` - CRUD query functions
478
+ */
724
479
  export function kysely(config) {
725
- const parsed = S.decodeUnknownSync(KyselyConfigSchema)(config);
726
- const defaultExportName = (_entityName, methodName) => methodName.charAt(0).toLowerCase() + methodName.slice(1);
727
- // Compute dbTypesPath from dbTypeName if not explicitly provided
728
- // Both DB types and query files are in the same outputDir, so use "./" not "../"
729
- const dbTypesPath = config.dbTypesPath ?? `./${parsed.dbTypeName}.js`;
480
+ // Parse schema-validated options
481
+ const schemaConfig = S.decodeSync(KyselyConfigSchema)(config ?? {});
482
+ // Debug logging
483
+ // Resolve file naming
484
+ // typesFile is always a single static path (all types in one file)
485
+ // queriesFile can be dynamic per-entity
730
486
  const resolvedConfig = {
731
- ...parsed,
732
- dbTypesPath,
733
- exportName: parsed.exportName ?? defaultExportName,
487
+ ...schemaConfig,
488
+ typesFile: config?.typesFile ?? "db.ts",
489
+ queriesFile: normalizeFileNaming(config?.queriesFile, "queries.ts"),
490
+ dbImport: config?.dbImport,
734
491
  };
735
- return definePlugin({
492
+ return {
736
493
  name: "kysely",
737
- kind: "queries",
738
- singleton: true,
739
- canProvide: () => true,
740
- provide: (_params, _deps, ctx) => {
741
- const { ir, typeHints } = ctx;
494
+ provides: ["queries"],
495
+ consumes: [],
496
+ fileDefaults: [
497
+ {
498
+ // All types go to a single file
499
+ pattern: "types:kysely:",
500
+ fileNaming: () => resolvedConfig.typesFile,
501
+ },
502
+ {
503
+ pattern: "queries:kysely:",
504
+ fileNaming: resolvedConfig.queriesFile,
505
+ },
506
+ ],
507
+ declare: Effect.gen(function* () {
508
+ const ir = yield* IR;
509
+ const inflection = yield* Inflection;
510
+ const declarations = [];
742
511
  const enumEntities = getEnumEntities(ir);
743
512
  const compositeEntities = getCompositeEntities(ir).filter(e => e.tags.omit !== true);
744
513
  const tableEntities = getTableEntities(ir).filter(e => e.tags.omit !== true);
745
- const defaultSchemas = ir.schemas;
746
- const dbTypeName = parsed.dbTypeName;
747
- // ================================================================
748
- // Generate Types (DB.ts)
749
- // ================================================================
750
- const fieldCtx = {
751
- schemaName: "",
752
- tableName: "",
753
- enums: enumEntities,
754
- composites: compositeEntities,
755
- extensions: ir.extensions,
756
- typeHints,
757
- defaultSchemas,
758
- };
759
- const imports = collectImports(tableEntities, compositeEntities, fieldCtx);
760
- const statements = [];
514
+ // Declare types
761
515
  for (const enumEntity of enumEntities) {
762
516
  if (enumEntity.tags.omit === true)
763
517
  continue;
764
- statements.push(generateEnumStatement(enumEntity));
518
+ declarations.push({
519
+ name: enumEntity.name,
520
+ capability: `types:kysely:${enumEntity.name}`,
521
+ });
765
522
  }
766
523
  for (const composite of compositeEntities) {
767
- statements.push(generateCompositeInterface(composite, {
768
- ...fieldCtx,
769
- schemaName: composite.schemaName,
770
- tableName: composite.pgName,
771
- }));
772
- }
773
- for (const entity of tableEntities) {
774
- statements.push(generateTableInterface(entity, {
775
- ...fieldCtx,
776
- schemaName: entity.schemaName,
777
- tableName: entity.pgName,
778
- }));
779
- }
780
- statements.push(generateDBInterface(tableEntities, defaultSchemas, dbTypeName));
781
- const dbFilePath = `${parsed.outputDir}/${dbTypeName}.ts`;
782
- const dbFile = ctx.file(dbFilePath);
783
- if (imports.kyselyImports.size > 0) {
784
- dbFile.import({
785
- kind: "package",
786
- types: [...imports.kyselyImports],
787
- from: "kysely",
524
+ declarations.push({
525
+ name: composite.name,
526
+ capability: `types:kysely:${composite.name}`,
788
527
  });
789
528
  }
790
- for (const [from, names] of imports.externalImports) {
791
- dbFile.import({
792
- kind: "relative",
793
- types: [...names],
794
- from,
529
+ for (const entity of tableEntities) {
530
+ declarations.push({
531
+ name: entity.name,
532
+ capability: `types:kysely:${entity.name}`,
795
533
  });
796
534
  }
797
- const helperTypes = [];
798
- if (imports.needsGenerated) {
799
- helperTypes.push(`export type Generated<T> = ${GENERATED_TYPE_DEF};`);
800
- }
801
- if (imports.needsArrayType) {
802
- helperTypes.push(`export type ArrayType<T> = ${ARRAY_TYPE_DEF};`);
803
- helperTypes.push(`export type ArrayTypeImpl<T> = ${ARRAY_TYPE_IMPL_DEF};`);
535
+ declarations.push({
536
+ name: "DB",
537
+ capability: "types:kysely:DB",
538
+ });
539
+ // Declare queries if enabled
540
+ if (resolvedConfig.generateQueries) {
541
+ for (const entity of tableEntities) {
542
+ const entityName = entity.name;
543
+ let hasAnyMethods = false;
544
+ if (entity.permissions.canSelect && entity.primaryKey && entity.primaryKey.columns.length > 0) {
545
+ hasAnyMethods = true;
546
+ declarations.push({
547
+ name: buildQueryName(inflection, entityName, "FindById"),
548
+ capability: `queries:kysely:${entityName}:findById`,
549
+ dependsOn: [`types:kysely:${entityName}`],
550
+ });
551
+ }
552
+ // listByCursor for indexed timestamptz columns
553
+ const cursorCandidates = getCursorPaginationCandidates(entity);
554
+ for (const candidate of cursorCandidates) {
555
+ const listByName = buildListByName(inflection, entityName, candidate.cursorColumnName);
556
+ const pascalColumn = inflection.pascalCase(candidate.cursorColumnName);
557
+ hasAnyMethods = true;
558
+ declarations.push({
559
+ name: listByName,
560
+ capability: `queries:kysely:${entityName}:listBy${pascalColumn}`,
561
+ dependsOn: [`types:kysely:${entityName}`],
562
+ });
563
+ }
564
+ if (entity.kind === "table" && entity.permissions.canInsert && entity.shapes.insert) {
565
+ hasAnyMethods = true;
566
+ declarations.push({
567
+ name: buildQueryName(inflection, entityName, "Create"),
568
+ capability: `queries:kysely:${entityName}:create`,
569
+ dependsOn: [`types:kysely:${entityName}`],
570
+ });
571
+ }
572
+ if (entity.kind === "table" &&
573
+ entity.permissions.canUpdate &&
574
+ entity.shapes.update &&
575
+ entity.primaryKey &&
576
+ entity.primaryKey.columns.length > 0) {
577
+ hasAnyMethods = true;
578
+ declarations.push({
579
+ name: buildQueryName(inflection, entityName, "Update"),
580
+ capability: `queries:kysely:${entityName}:update`,
581
+ dependsOn: [`types:kysely:${entityName}`],
582
+ });
583
+ }
584
+ if (entity.kind === "table" &&
585
+ entity.permissions.canDelete &&
586
+ entity.primaryKey &&
587
+ entity.primaryKey.columns.length > 0) {
588
+ hasAnyMethods = true;
589
+ declarations.push({
590
+ name: buildQueryName(inflection, entityName, "Delete"),
591
+ capability: `queries:kysely:${entityName}:delete`,
592
+ dependsOn: [`types:kysely:${entityName}`],
593
+ });
594
+ }
595
+ // findBy queries for indexed columns
596
+ if (entity.permissions.canSelect) {
597
+ const pkColumns = new Set(entity.primaryKey?.columns ?? []);
598
+ const processedColumns = new Set();
599
+ for (const index of entity.indexes) {
600
+ if (index.isPartial || index.hasExpressions || index.columns.length !== 1)
601
+ continue;
602
+ if (index.method === "gin" || index.method === "gist")
603
+ continue;
604
+ const columnName = index.columns[0];
605
+ if (pkColumns.has(columnName))
606
+ continue;
607
+ if (processedColumns.has(columnName))
608
+ continue;
609
+ processedColumns.add(columnName);
610
+ const findByName = buildFindByName(inflection, entityName, columnName);
611
+ const pascalColumn = inflection.pascalCase(columnName);
612
+ hasAnyMethods = true;
613
+ declarations.push({
614
+ name: findByName,
615
+ capability: `queries:kysely:${entityName}:findBy${pascalColumn}`,
616
+ dependsOn: [`types:kysely:${entityName}`],
617
+ });
618
+ }
619
+ }
620
+ if (hasAnyMethods) {
621
+ declarations.push({
622
+ name: `${entityName}Queries`,
623
+ capability: `queries:kysely:${entityName}`,
624
+ });
625
+ }
626
+ }
804
627
  }
805
- if (imports.needsJsonTypes) {
806
- helperTypes.push(`export type JsonPrimitive = boolean | number | string | null;`);
807
- helperTypes.push(`export type JsonObject = { [x: string]: JsonValue | undefined };`);
808
- helperTypes.push(`export type JsonArray = JsonValue[];`);
809
- helperTypes.push(`export type JsonValue = JsonArray | JsonObject | JsonPrimitive;`);
628
+ return declarations;
629
+ }),
630
+ render: Effect.gen(function* () {
631
+ const ir = yield* IR;
632
+ const inflection = yield* Inflection;
633
+ const symbols = [];
634
+ const enumEntities = getEnumEntities(ir);
635
+ const compositeEntities = getCompositeEntities(ir).filter(e => e.tags.omit !== true);
636
+ const tableEntities = getTableEntities(ir).filter(e => e.tags.omit !== true);
637
+ const defaultSchemas = ir.schemas;
638
+ const typeCtx = {
639
+ enums: enumEntities,
640
+ composites: compositeEntities,
641
+ };
642
+ // Collect imports for types
643
+ const typeImports = collectTypeImports(tableEntities, compositeEntities, typeCtx);
644
+ // Build kysely imports for types file
645
+ const kyselyTypeImports = [];
646
+ if (typeImports.needsColumnType) {
647
+ kyselyTypeImports.push("ColumnType");
810
648
  }
811
- if (helperTypes.length > 0) {
812
- dbFile.header(helperTypes.join("\n\n"));
649
+ const typesHeader = buildTypesHeader(typeImports);
650
+ const typesExternalImports = kyselyTypeImports.length > 0 ? [{ from: "kysely", types: kyselyTypeImports }] : [];
651
+ // Generate enum types
652
+ for (const enumEntity of enumEntities) {
653
+ if (enumEntity.tags.omit === true)
654
+ continue;
655
+ symbols.push({
656
+ name: enumEntity.name,
657
+ capability: `types:kysely:${enumEntity.name}`,
658
+ node: generateEnumType(enumEntity),
659
+ exports: "named",
660
+ externalImports: typesExternalImports,
661
+ fileHeader: typesHeader,
662
+ });
813
663
  }
814
- dbFile.ast(conjure.symbolProgram(...statements)).emit();
815
- // ================================================================
816
- // Generate Queries
817
- // ================================================================
818
- if (!parsed.generateQueries) {
819
- return;
664
+ // Generate composite interfaces
665
+ for (const composite of compositeEntities) {
666
+ symbols.push({
667
+ name: composite.name,
668
+ capability: `types:kysely:${composite.name}`,
669
+ node: generateCompositeInterface(composite, typeCtx),
670
+ exports: "named",
671
+ externalImports: typesExternalImports,
672
+ fileHeader: typesHeader,
673
+ });
820
674
  }
821
- const exportName = resolvedConfig.exportName;
822
- const exportStyle = resolvedConfig.exportStyle;
675
+ // Generate table interfaces
823
676
  for (const entity of tableEntities) {
824
- const entityName = entity.name;
825
- const filePath = `${parsed.outputDir}/${entityName}.ts`;
826
- const file = ctx.file(filePath);
827
- const genCtx = {
828
- entity,
829
- enums: enumEntities,
830
- ir,
831
- defaultSchemas,
832
- dbTypesPath: resolvedConfig.dbTypesPath,
833
- executeQueries: parsed.executeQueries,
834
- generateListMany: parsed.generateListMany,
835
- entityName,
836
- exportName,
837
- explicitColumns: parsed.explicitColumns,
838
- dbAsParameter: parsed.dbAsParameter,
839
- defaultLimit: parsed.defaultLimit,
840
- };
841
- const methods = generateCrudMethods(genCtx);
842
- if (methods.length === 0) {
843
- continue;
844
- }
845
- if (parsed.header) {
846
- file.header(parsed.header);
847
- }
848
- // Build kysely imports based on what's actually used
849
- const kyselyTypes = [];
850
- if (parsed.dbAsParameter) {
851
- kyselyTypes.push("Kysely");
852
- }
853
- if (methods.some((m) => m.meta.kind === "create")) {
854
- kyselyTypes.push("Insertable");
855
- }
856
- if (methods.some((m) => m.meta.kind === "update")) {
857
- kyselyTypes.push("Updateable");
858
- }
859
- // Selectable would be used for explicit return types, but we use returningAll()
860
- if (kyselyTypes.length > 0) {
861
- file.import({
862
- kind: "package",
863
- types: kyselyTypes,
864
- from: "kysely",
677
+ symbols.push({
678
+ name: entity.name,
679
+ capability: `types:kysely:${entity.name}`,
680
+ node: generateTableInterface(entity, typeCtx),
681
+ exports: "named",
682
+ externalImports: typesExternalImports,
683
+ fileHeader: typesHeader,
684
+ });
685
+ }
686
+ // Generate DB interface
687
+ symbols.push({
688
+ name: "DB",
689
+ capability: "types:kysely:DB",
690
+ node: generateDBInterface(tableEntities, defaultSchemas),
691
+ exports: "named",
692
+ externalImports: typesExternalImports,
693
+ fileHeader: typesHeader,
694
+ });
695
+ // Generate queries if enabled
696
+ if (resolvedConfig.generateQueries) {
697
+ // User module imports for db instance (only if not using dbAsParameter)
698
+ const queryUserImports = !resolvedConfig.dbAsParameter && resolvedConfig.dbImport
699
+ ? [resolvedConfig.dbImport]
700
+ : undefined;
701
+ for (const entity of tableEntities) {
702
+ const entityName = entity.name;
703
+ const tableName = ir.schemas.includes(entity.schemaName)
704
+ ? entity.pgName
705
+ : `${entity.schemaName}.${entity.pgName}`;
706
+ const entityMethods = [];
707
+ // findById
708
+ if (entity.permissions.canSelect && entity.primaryKey && entity.primaryKey.columns.length > 0) {
709
+ const pkColumn = entity.primaryKey.columns[0];
710
+ const pkField = entity.shapes.row.fields.find(f => f.columnName === pkColumn);
711
+ const pkParam = buildPkParam(pkField);
712
+ const method = {
713
+ name: buildQueryName(inflection, entityName, "FindById"),
714
+ kind: "read",
715
+ params: [pkParam],
716
+ returns: buildReturnType(entityName, false, true),
717
+ callSignature: { style: "named" },
718
+ };
719
+ entityMethods.push(method);
720
+ const queryExpr = chain(b.identifier("db"))
721
+ .method("selectFrom", [str(tableName)])
722
+ .method("select", [buildColumnArray(entity.shapes.row.fields)])
723
+ .method("where", [
724
+ str(pkColumn),
725
+ str("="),
726
+ b.identifier(pkField.name),
727
+ ])
728
+ .build();
729
+ const destructuredParam = buildDestructuredParam([pkParam]);
730
+ let fnBuilder = fn();
731
+ if (resolvedConfig.dbAsParameter) {
732
+ fnBuilder = fnBuilder.param("db", ts.ref("Kysely"));
733
+ }
734
+ const fnExpr = fnBuilder.rawParam(destructuredParam).arrow().body(stmt.return(queryExpr)).build();
735
+ symbols.push({
736
+ name: method.name,
737
+ capability: `queries:kysely:${entityName}:findById`,
738
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
739
+ exports: "named",
740
+ externalImports: resolvedConfig.dbAsParameter ? [{ from: "kysely", names: ["Kysely"] }] : [],
741
+ userImports: queryUserImports,
742
+ });
743
+ }
744
+ // listByCursor for indexed timestamptz columns
745
+ const cursorCandidates = getCursorPaginationCandidates(entity);
746
+ for (const candidate of cursorCandidates) {
747
+ const pascalColumn = inflection.pascalCase(candidate.cursorColumnName);
748
+ const pkField = entity.shapes.row.fields.find(f => f.name === candidate.pkColumn);
749
+ if (!pkField)
750
+ continue;
751
+ const cursorType = ts.objectType([
752
+ { name: candidate.cursorColumn, type: ts.ref("Date") },
753
+ { name: candidate.pkColumn, type: ts.ref(pgTypeToTsType(getPgType(pkField))) },
754
+ ]);
755
+ const method = {
756
+ name: buildListByName(inflection, entityName, candidate.cursorColumnName),
757
+ kind: "list",
758
+ params: [],
759
+ returns: buildReturnType(entityName, true, false),
760
+ callSignature: { style: "named" },
761
+ };
762
+ entityMethods.push(method);
763
+ const cursorParam = param.destructured([
764
+ { name: "cursor", type: ts.union(cursorType, ts.undefined()), optional: true },
765
+ { name: "limit", type: ts.number(), optional: true, defaultValue: conjure.num(resolvedConfig.defaultLimit) },
766
+ ]);
767
+ const cursorComparisonOp = candidate.desc ? "<" : ">";
768
+ const orderDirection = candidate.desc ? "desc" : "asc";
769
+ const cursorCondition = b.callExpression(b.identifier("eb"), [
770
+ str(candidate.cursorColumnName),
771
+ str(cursorComparisonOp),
772
+ b.memberExpression(b.identifier("cursor"), b.identifier(candidate.cursorColumn)),
773
+ ]);
774
+ const pkCondition = b.callExpression(b.identifier("eb"), [
775
+ str(candidate.pkColumnName),
776
+ str(cursorComparisonOp),
777
+ b.memberExpression(b.identifier("cursor"), b.identifier(candidate.pkColumn)),
778
+ ]);
779
+ const equalityCondition = b.callExpression(b.identifier("eb"), [
780
+ str(candidate.cursorColumnName),
781
+ str("="),
782
+ b.memberExpression(b.identifier("cursor"), b.identifier(candidate.cursorColumn)),
783
+ ]);
784
+ const andClause = chain(b.identifier("eb"))
785
+ .method("and", [
786
+ arrExpr(equalityCondition, pkCondition),
787
+ ])
788
+ .build();
789
+ const whereClause = chain(b.identifier("eb"))
790
+ .method("or", [
791
+ arrExpr(cursorCondition, andClause),
792
+ ])
793
+ .build();
794
+ const queryExpr = chain(b.identifier("db"))
795
+ .method("selectFrom", [str(tableName)])
796
+ .method("select", [buildColumnArray(entity.shapes.row.fields)])
797
+ .method("$if", [
798
+ b.binaryExpression("!==", b.identifier("cursor"), b.identifier("undefined")),
799
+ fn()
800
+ .param("qb")
801
+ .arrow()
802
+ .body(stmt.return(chain(b.identifier("qb"))
803
+ .method("where", [b.arrowFunctionExpression([b.identifier("eb")], cast.toExpr(whereClause))])
804
+ .build()))
805
+ .build(),
806
+ ])
807
+ .method("orderBy", [str(candidate.cursorColumnName), str(orderDirection)])
808
+ .method("orderBy", [str(candidate.pkColumnName), str(orderDirection)])
809
+ .method("limit", [b.identifier("limit")])
810
+ .build();
811
+ let fnBuilder = fn();
812
+ if (resolvedConfig.dbAsParameter) {
813
+ fnBuilder = fnBuilder.param("db", ts.ref("Kysely"));
814
+ }
815
+ const fnExpr = fnBuilder.rawParam(cursorParam).arrow().body(stmt.return(queryExpr)).build();
816
+ symbols.push({
817
+ name: method.name,
818
+ capability: `queries:kysely:${entityName}:listBy${pascalColumn}`,
819
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
820
+ exports: "named",
821
+ externalImports: resolvedConfig.dbAsParameter ? [{ from: "kysely", names: ["Kysely"] }] : [],
822
+ userImports: queryUserImports,
823
+ });
824
+ }
825
+ // create
826
+ if (entity.kind === "table" && entity.permissions.canInsert && entity.shapes.insert) {
827
+ const bodyParam = buildBodyParam(entityName, "insert");
828
+ const method = {
829
+ name: buildQueryName(inflection, entityName, "Create"),
830
+ kind: "create",
831
+ params: [bodyParam],
832
+ returns: buildReturnType(entityName, false, false),
833
+ callSignature: { style: "named", bodyStyle: "spread" },
834
+ };
835
+ entityMethods.push(method);
836
+ const queryExpr = chain(b.identifier("db"))
837
+ .method("insertInto", [str(tableName)])
838
+ .method("values", [b.identifier("data")])
839
+ .method("returningAll", [])
840
+ .build();
841
+ // Simple typed parameter: (data: Insertable<Entity>)
842
+ let fnBuilder = fn();
843
+ if (resolvedConfig.dbAsParameter) {
844
+ fnBuilder = fnBuilder.param("db", ts.ref("Kysely"));
845
+ }
846
+ const fnExpr = fnBuilder
847
+ .param("data", ts.ref("Insertable", [ts.ref(entityName)]))
848
+ .arrow()
849
+ .body(stmt.return(queryExpr))
850
+ .build();
851
+ symbols.push({
852
+ name: method.name,
853
+ capability: `queries:kysely:${entityName}:create`,
854
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
855
+ exports: "named",
856
+ externalImports: [
857
+ {
858
+ from: "kysely",
859
+ names: resolvedConfig.dbAsParameter ? ["Kysely"] : [],
860
+ types: ["Insertable"],
861
+ },
862
+ {
863
+ from: resolvedConfig.typesFile,
864
+ types: [entityName],
865
+ },
866
+ ],
867
+ userImports: queryUserImports,
868
+ });
869
+ }
870
+ // update
871
+ if (entity.kind === "table" &&
872
+ entity.permissions.canUpdate &&
873
+ entity.shapes.update &&
874
+ entity.primaryKey &&
875
+ entity.primaryKey.columns.length > 0) {
876
+ const pkColumn = entity.primaryKey.columns[0];
877
+ const pkField = entity.shapes.row.fields.find(f => f.columnName === pkColumn);
878
+ const pkParam = buildPkParam(pkField);
879
+ const bodyParam = buildBodyParam(entityName, "update");
880
+ const method = {
881
+ name: buildQueryName(inflection, entityName, "Update"),
882
+ kind: "update",
883
+ params: [pkParam, bodyParam],
884
+ returns: buildReturnType(entityName, false, true),
885
+ callSignature: { style: "named", bodyStyle: "spread" },
886
+ };
887
+ entityMethods.push(method);
888
+ const queryExpr = chain(b.identifier("db"))
889
+ .method("updateTable", [str(tableName)])
890
+ .method("set", [b.identifier("data")])
891
+ .method("where", [
892
+ str(pkColumn),
893
+ str("="),
894
+ b.identifier(pkField.name),
895
+ ])
896
+ .method("returningAll", [])
897
+ .build();
898
+ // Use param.withRest for flat destructuring: ({ id, ...data }: { id: string } & Omit<Updateable<Entity>, 'id'>)
899
+ // Using Omit ensures `data` doesn't include the PK field
900
+ const destructuredParam = param.withRest([{ name: pkField.name, type: ts.ref(pkParam.type) }], "data", ts.ref("Omit", [
901
+ ts.ref("Updateable", [ts.ref(entityName)]),
902
+ ts.literal(pkField.name),
903
+ ]));
904
+ let fnBuilder = fn();
905
+ if (resolvedConfig.dbAsParameter) {
906
+ fnBuilder = fnBuilder.param("db", ts.ref("Kysely"));
907
+ }
908
+ const fnExpr = fnBuilder.rawParam(destructuredParam).arrow().body(stmt.return(queryExpr)).build();
909
+ symbols.push({
910
+ name: method.name,
911
+ capability: `queries:kysely:${entityName}:update`,
912
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
913
+ exports: "named",
914
+ externalImports: [
915
+ {
916
+ from: "kysely",
917
+ names: resolvedConfig.dbAsParameter ? ["Kysely"] : [],
918
+ types: ["Updateable"],
919
+ },
920
+ {
921
+ from: resolvedConfig.typesFile,
922
+ types: [entityName],
923
+ },
924
+ ],
925
+ userImports: queryUserImports,
926
+ });
927
+ }
928
+ // delete
929
+ if (entity.kind === "table" &&
930
+ entity.permissions.canDelete &&
931
+ entity.primaryKey &&
932
+ entity.primaryKey.columns.length > 0) {
933
+ const pkColumn = entity.primaryKey.columns[0];
934
+ const pkField = entity.shapes.row.fields.find(f => f.columnName === pkColumn);
935
+ const pkParam = buildPkParam(pkField);
936
+ const method = {
937
+ name: buildQueryName(inflection, entityName, "Delete"),
938
+ kind: "delete",
939
+ params: [pkParam],
940
+ returns: buildReturnType(entityName, false, false),
941
+ callSignature: { style: "named" },
942
+ };
943
+ entityMethods.push(method);
944
+ const queryExpr = chain(b.identifier("db"))
945
+ .method("deleteFrom", [str(tableName)])
946
+ .method("where", [
947
+ str(pkColumn),
948
+ str("="),
949
+ b.identifier(pkField.name),
950
+ ])
951
+ .build();
952
+ const destructuredParam = buildDestructuredParam([pkParam]);
953
+ let fnBuilder = fn();
954
+ if (resolvedConfig.dbAsParameter) {
955
+ fnBuilder = fnBuilder.param("db", ts.ref("Kysely"));
956
+ }
957
+ const fnExpr = fnBuilder.rawParam(destructuredParam).arrow().body(stmt.return(queryExpr)).build();
958
+ symbols.push({
959
+ name: method.name,
960
+ capability: `queries:kysely:${entityName}:delete`,
961
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
962
+ exports: "named",
963
+ externalImports: resolvedConfig.dbAsParameter ? [{ from: "kysely", names: ["Kysely"] }] : [],
964
+ userImports: queryUserImports,
965
+ });
966
+ }
967
+ // findBy queries for indexed columns
968
+ if (entity.permissions.canSelect) {
969
+ const pkColumns = new Set(entity.primaryKey?.columns ?? []);
970
+ const processedColumns = new Set();
971
+ for (const index of entity.indexes) {
972
+ if (index.isPartial || index.hasExpressions || index.columns.length !== 1)
973
+ continue;
974
+ if (index.method === "gin" || index.method === "gist")
975
+ continue;
976
+ const columnName = index.columns[0];
977
+ if (pkColumns.has(columnName))
978
+ continue;
979
+ if (processedColumns.has(columnName))
980
+ continue;
981
+ processedColumns.add(columnName);
982
+ const field = entity.shapes.row.fields.find(f => f.columnName === columnName);
983
+ if (!field)
984
+ continue;
985
+ const pascalColumn = inflection.pascalCase(columnName);
986
+ const isUnique = index.isUnique;
987
+ const lookupParam = buildLookupParam(field);
988
+ const method = {
989
+ name: buildFindByName(inflection, entityName, columnName),
990
+ kind: "lookup",
991
+ params: [lookupParam],
992
+ returns: buildReturnType(entityName, !isUnique, isUnique),
993
+ lookupField: field.name,
994
+ isUniqueLookup: isUnique,
995
+ callSignature: { style: "named" },
996
+ };
997
+ entityMethods.push(method);
998
+ const queryExpr = chain(b.identifier("db"))
999
+ .method("selectFrom", [str(tableName)])
1000
+ .method("select", [buildColumnArray(entity.shapes.row.fields)])
1001
+ .method("where", [
1002
+ str(columnName),
1003
+ str("="),
1004
+ b.identifier(field.name),
1005
+ ])
1006
+ .build();
1007
+ const destructuredParam = buildDestructuredParam([lookupParam]);
1008
+ let fnBuilder = fn();
1009
+ if (resolvedConfig.dbAsParameter) {
1010
+ fnBuilder = fnBuilder.param("db", ts.ref("Kysely"));
1011
+ }
1012
+ const fnExpr = fnBuilder.rawParam(destructuredParam).arrow().body(stmt.return(queryExpr)).build();
1013
+ symbols.push({
1014
+ name: method.name,
1015
+ capability: `queries:kysely:${entityName}:findBy${pascalColumn}`,
1016
+ node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
1017
+ exports: "named",
1018
+ externalImports: resolvedConfig.dbAsParameter ? [{ from: "kysely", names: ["Kysely"] }] : [],
1019
+ userImports: queryUserImports,
1020
+ });
1021
+ }
1022
+ }
1023
+ // Entity queries metadata (no importPath - emit phase handles file resolution)
1024
+ const pkField = entity.primaryKey?.columns[0]
1025
+ ? entity.shapes.row.fields.find(f => f.columnName === entity.primaryKey.columns[0])
1026
+ : undefined;
1027
+ const entityExtension = {
1028
+ methods: entityMethods,
1029
+ pkType: pkField ? pgTypeToTsType(getPgType(pkField)) : undefined,
1030
+ hasCompositePk: (entity.primaryKey?.columns.length ?? 0) > 1,
1031
+ };
1032
+ symbols.push({
1033
+ name: `${entityName}Queries`,
1034
+ capability: `queries:kysely:${entityName}`,
1035
+ node: b.stringLiteral(""),
1036
+ metadata: entityExtension,
1037
+ exports: false,
865
1038
  });
866
1039
  }
867
- // Only import DB if dbAsParameter is true
868
- const dbImportTypes = parsed.dbAsParameter ? [dbTypeName, entityName] : [entityName];
869
- file.import({
870
- kind: "relative",
871
- types: dbImportTypes,
872
- from: resolvedConfig.dbTypesPath,
873
- });
874
- const stmts = toStatements(methods, exportStyle, entityName);
875
- file.ast(conjure.program(...stmts)).emit();
876
- const pkType = entity.primaryKey
877
- ? getFieldTypeString(findRowField(entity, entity.primaryKey.columns[0]), genCtx)
878
- : "unknown";
879
- ctx.symbols.registerEntityMethods({
880
- entity: entityName,
881
- importPath: filePath,
882
- pkType,
883
- hasCompositePk: (entity.primaryKey?.columns.length ?? 0) > 1,
884
- methods: methods.map(m => ({
885
- name: m.meta.name,
886
- file: filePath,
887
- entity: entityName,
888
- kind: m.meta.kind,
889
- params: m.meta.params.map(p => ({
890
- name: p.name,
891
- type: p.type,
892
- required: p.required,
893
- columnName: p.columnName,
894
- source: p.source,
895
- })),
896
- returns: m.meta.returns,
897
- lookupField: m.meta.lookupField,
898
- isUniqueLookup: m.meta.isUniqueLookup,
899
- callSignature: m.meta.callSignature,
900
- })),
901
- }, "kysely");
902
1040
  }
903
- },
904
- });
1041
+ return symbols;
1042
+ }),
1043
+ };
905
1044
  }
906
1045
  //# sourceMappingURL=kysely.js.map