@danielfgray/pg-sourcerer 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/bin/pgsourcerer +2 -0
  2. package/dist/__tests__/fixtures/index.d.ts +15 -0
  3. package/dist/__tests__/fixtures/index.d.ts.map +1 -0
  4. package/dist/__tests__/fixtures/index.js +19 -0
  5. package/dist/__tests__/fixtures/index.js.map +1 -0
  6. package/dist/__tests__/fixtures/introspection.json +40522 -0
  7. package/dist/cli.d.ts +0 -1
  8. package/dist/cli.js +7 -46
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +38 -5
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +13 -2
  13. package/dist/config.js.map +1 -1
  14. package/dist/{lib/conjure.d.ts → conjure/index.d.ts} +62 -3
  15. package/dist/conjure/index.d.ts.map +1 -0
  16. package/dist/{lib/conjure.js → conjure/index.js} +124 -3
  17. package/dist/conjure/index.js.map +1 -0
  18. package/dist/conjure/signature.d.ts +85 -0
  19. package/dist/conjure/signature.d.ts.map +1 -0
  20. package/dist/conjure/signature.js +130 -0
  21. package/dist/conjure/signature.js.map +1 -0
  22. package/dist/conjure/types.d.ts +97 -0
  23. package/dist/conjure/types.d.ts.map +1 -0
  24. package/dist/conjure/types.js +206 -0
  25. package/dist/conjure/types.js.map +1 -0
  26. package/dist/errors.d.ts +114 -139
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js +82 -36
  29. package/dist/errors.js.map +1 -1
  30. package/dist/generate.d.ts +45 -46
  31. package/dist/generate.d.ts.map +1 -1
  32. package/dist/generate.js +86 -59
  33. package/dist/generate.js.map +1 -1
  34. package/dist/hex/builder.d.ts +12 -0
  35. package/dist/hex/builder.d.ts.map +1 -0
  36. package/dist/hex/builder.js +64 -0
  37. package/dist/hex/builder.js.map +1 -0
  38. package/dist/hex/ddl.d.ts +53 -0
  39. package/dist/hex/ddl.d.ts.map +1 -0
  40. package/dist/hex/ddl.js +306 -0
  41. package/dist/hex/ddl.js.map +1 -0
  42. package/dist/hex/index.d.ts +105 -0
  43. package/dist/hex/index.d.ts.map +1 -0
  44. package/dist/hex/index.js +81 -0
  45. package/dist/hex/index.js.map +1 -0
  46. package/dist/hex/primitives.d.ts +23 -0
  47. package/dist/hex/primitives.d.ts.map +1 -0
  48. package/dist/hex/primitives.js +38 -0
  49. package/dist/hex/primitives.js.map +1 -0
  50. package/dist/hex/query.d.ts +116 -0
  51. package/dist/hex/query.d.ts.map +1 -0
  52. package/dist/hex/query.js +219 -0
  53. package/dist/hex/query.js.map +1 -0
  54. package/dist/hex/types.d.ts +287 -0
  55. package/dist/hex/types.d.ts.map +1 -0
  56. package/dist/hex/types.js +431 -0
  57. package/dist/hex/types.js.map +1 -0
  58. package/dist/index.d.ts +17 -25
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +33 -44
  61. package/dist/index.js.map +1 -1
  62. package/dist/init.d.ts.map +1 -1
  63. package/dist/init.js +76 -140
  64. package/dist/init.js.map +1 -1
  65. package/dist/ir/extensions/queries.d.ts +6 -6
  66. package/dist/ir/extensions/queries.d.ts.map +1 -1
  67. package/dist/ir/extensions/queries.js +6 -4
  68. package/dist/ir/extensions/queries.js.map +1 -1
  69. package/dist/ir/extensions/schema-builder.d.ts.map +1 -1
  70. package/dist/ir/extensions/schema-builder.js.map +1 -1
  71. package/dist/ir/index.d.ts.map +1 -1
  72. package/dist/ir/index.js.map +1 -1
  73. package/dist/ir/relation-graph.d.ts.map +1 -1
  74. package/dist/ir/relation-graph.js +8 -8
  75. package/dist/ir/relation-graph.js.map +1 -1
  76. package/dist/ir/semantic-ir.d.ts +38 -0
  77. package/dist/ir/semantic-ir.d.ts.map +1 -1
  78. package/dist/ir/semantic-ir.js +50 -2
  79. package/dist/ir/semantic-ir.js.map +1 -1
  80. package/dist/ir/smart-tags.d.ts.map +1 -1
  81. package/dist/ir/smart-tags.js.map +1 -1
  82. package/dist/lib/field-utils.d.ts.map +1 -1
  83. package/dist/lib/field-utils.js +7 -7
  84. package/dist/lib/field-utils.js.map +1 -1
  85. package/dist/lib/join-graph.d.ts +95 -0
  86. package/dist/lib/join-graph.d.ts.map +1 -0
  87. package/dist/lib/join-graph.js +305 -0
  88. package/dist/lib/join-graph.js.map +1 -0
  89. package/dist/lib/picker.d.ts +60 -0
  90. package/dist/lib/picker.d.ts.map +1 -0
  91. package/dist/lib/picker.js +325 -0
  92. package/dist/lib/picker.js.map +1 -0
  93. package/dist/plugins/arktype.d.ts +20 -24
  94. package/dist/plugins/arktype.d.ts.map +1 -1
  95. package/dist/plugins/arktype.js +462 -386
  96. package/dist/plugins/arktype.js.map +1 -1
  97. package/dist/plugins/effect/http.d.ts +7 -0
  98. package/dist/plugins/effect/http.d.ts.map +1 -0
  99. package/dist/plugins/effect/http.js +460 -0
  100. package/dist/plugins/effect/http.js.map +1 -0
  101. package/dist/plugins/effect/index.d.ts +22 -0
  102. package/dist/plugins/effect/index.d.ts.map +1 -0
  103. package/dist/plugins/effect/index.js +65 -0
  104. package/dist/plugins/effect/index.js.map +1 -0
  105. package/dist/plugins/effect/models.d.ts +6 -0
  106. package/dist/plugins/effect/models.d.ts.map +1 -0
  107. package/dist/plugins/effect/models.js +116 -0
  108. package/dist/plugins/effect/models.js.map +1 -0
  109. package/dist/plugins/effect/repos.d.ts +21 -0
  110. package/dist/plugins/effect/repos.d.ts.map +1 -0
  111. package/dist/plugins/effect/repos.js +131 -0
  112. package/dist/plugins/effect/repos.js.map +1 -0
  113. package/dist/plugins/effect/schemas.d.ts +7 -0
  114. package/dist/plugins/effect/schemas.d.ts.map +1 -0
  115. package/dist/plugins/effect/schemas.js +75 -0
  116. package/dist/plugins/effect/schemas.js.map +1 -0
  117. package/dist/plugins/effect/shared.d.ts +116 -0
  118. package/dist/plugins/effect/shared.d.ts.map +1 -0
  119. package/dist/plugins/effect/shared.js +164 -0
  120. package/dist/plugins/effect/shared.js.map +1 -0
  121. package/dist/plugins/http-elysia.d.ts +20 -27
  122. package/dist/plugins/http-elysia.d.ts.map +1 -1
  123. package/dist/plugins/http-elysia.js +350 -475
  124. package/dist/plugins/http-elysia.js.map +1 -1
  125. package/dist/plugins/http-express.d.ts +20 -31
  126. package/dist/plugins/http-express.d.ts.map +1 -1
  127. package/dist/plugins/http-express.js +281 -268
  128. package/dist/plugins/http-express.js.map +1 -1
  129. package/dist/plugins/http-hono.d.ts +17 -33
  130. package/dist/plugins/http-hono.d.ts.map +1 -1
  131. package/dist/plugins/http-hono.js +317 -341
  132. package/dist/plugins/http-hono.js.map +1 -1
  133. package/dist/plugins/http-orpc.d.ts +34 -33
  134. package/dist/plugins/http-orpc.d.ts.map +1 -1
  135. package/dist/plugins/http-orpc.js +345 -257
  136. package/dist/plugins/http-orpc.js.map +1 -1
  137. package/dist/plugins/http-trpc.d.ts +33 -35
  138. package/dist/plugins/http-trpc.d.ts.map +1 -1
  139. package/dist/plugins/http-trpc.js +337 -241
  140. package/dist/plugins/http-trpc.js.map +1 -1
  141. package/dist/plugins/kysely.d.ts +54 -59
  142. package/dist/plugins/kysely.d.ts.map +1 -1
  143. package/dist/plugins/kysely.js +826 -687
  144. package/dist/plugins/kysely.js.map +1 -1
  145. package/dist/plugins/sql-queries.d.ts +38 -44
  146. package/dist/plugins/sql-queries.d.ts.map +1 -1
  147. package/dist/plugins/sql-queries.js +497 -897
  148. package/dist/plugins/sql-queries.js.map +1 -1
  149. package/dist/plugins/types.d.ts +12 -20
  150. package/dist/plugins/types.d.ts.map +1 -1
  151. package/dist/plugins/types.js +84 -227
  152. package/dist/plugins/types.js.map +1 -1
  153. package/dist/plugins/valibot.d.ts +7 -44
  154. package/dist/plugins/valibot.d.ts.map +1 -1
  155. package/dist/plugins/valibot.js +376 -382
  156. package/dist/plugins/valibot.js.map +1 -1
  157. package/dist/plugins/zod.d.ts +20 -24
  158. package/dist/plugins/zod.d.ts.map +1 -1
  159. package/dist/plugins/zod.js +370 -367
  160. package/dist/plugins/zod.js.map +1 -1
  161. package/dist/runtime/emit.d.ts +64 -0
  162. package/dist/runtime/emit.d.ts.map +1 -0
  163. package/dist/runtime/emit.js +445 -0
  164. package/dist/runtime/emit.js.map +1 -0
  165. package/dist/runtime/errors.d.ts +36 -0
  166. package/dist/runtime/errors.d.ts.map +1 -0
  167. package/dist/runtime/errors.js +29 -0
  168. package/dist/runtime/errors.js.map +1 -0
  169. package/dist/runtime/file-assignment.d.ts +161 -0
  170. package/dist/runtime/file-assignment.d.ts.map +1 -0
  171. package/dist/runtime/file-assignment.js +195 -0
  172. package/dist/runtime/file-assignment.js.map +1 -0
  173. package/dist/runtime/orchestrator.d.ts +62 -0
  174. package/dist/runtime/orchestrator.d.ts.map +1 -0
  175. package/dist/runtime/orchestrator.js +99 -0
  176. package/dist/runtime/orchestrator.js.map +1 -0
  177. package/dist/runtime/registry.d.ts +268 -0
  178. package/dist/runtime/registry.d.ts.map +1 -0
  179. package/dist/runtime/registry.js +436 -0
  180. package/dist/runtime/registry.js.map +1 -0
  181. package/dist/runtime/types.d.ts +182 -0
  182. package/dist/runtime/types.d.ts.map +1 -0
  183. package/dist/runtime/types.js +2 -0
  184. package/dist/runtime/types.js.map +1 -0
  185. package/dist/runtime/validation.d.ts +41 -0
  186. package/dist/runtime/validation.d.ts.map +1 -0
  187. package/dist/runtime/validation.js +70 -0
  188. package/dist/runtime/validation.js.map +1 -0
  189. package/dist/services/config-loader.d.ts.map +1 -1
  190. package/dist/services/config-loader.js +15 -6
  191. package/dist/services/config-loader.js.map +1 -1
  192. package/dist/services/config.d.ts +55 -25
  193. package/dist/services/config.d.ts.map +1 -1
  194. package/dist/services/config.js +60 -34
  195. package/dist/services/config.js.map +1 -1
  196. package/dist/services/file-writer.d.ts +3 -3
  197. package/dist/services/file-writer.d.ts.map +1 -1
  198. package/dist/services/file-writer.js +6 -8
  199. package/dist/services/file-writer.js.map +1 -1
  200. package/dist/services/inflection.d.ts +126 -27
  201. package/dist/services/inflection.d.ts.map +1 -1
  202. package/dist/services/inflection.js +300 -72
  203. package/dist/services/inflection.js.map +1 -1
  204. package/dist/services/introspection.d.ts.map +1 -1
  205. package/dist/services/introspection.js +6 -6
  206. package/dist/services/introspection.js.map +1 -1
  207. package/dist/services/ir-builder.d.ts.map +1 -1
  208. package/dist/services/ir-builder.js +73 -77
  209. package/dist/services/ir-builder.js.map +1 -1
  210. package/dist/services/ir.d.ts.map +1 -1
  211. package/dist/services/ir.js.map +1 -1
  212. package/dist/services/pg-types.d.ts.map +1 -1
  213. package/dist/services/pg-types.js +3 -3
  214. package/dist/services/pg-types.js.map +1 -1
  215. package/dist/services/smart-tags-parser.d.ts.map +1 -1
  216. package/dist/services/smart-tags-parser.js +4 -4
  217. package/dist/services/smart-tags-parser.js.map +1 -1
  218. package/dist/services/type-hints.d.ts.map +1 -1
  219. package/dist/services/type-hints.js +1 -1
  220. package/dist/services/type-hints.js.map +1 -1
  221. package/dist/services/user-module-parser.d.ts +46 -0
  222. package/dist/services/user-module-parser.d.ts.map +1 -0
  223. package/dist/services/user-module-parser.js +181 -0
  224. package/dist/services/user-module-parser.js.map +1 -0
  225. package/dist/shared/converters.d.ts +60 -0
  226. package/dist/shared/converters.d.ts.map +1 -0
  227. package/dist/shared/converters.js +168 -0
  228. package/dist/shared/converters.js.map +1 -0
  229. package/dist/shared/query-types.d.ts +95 -0
  230. package/dist/shared/query-types.d.ts.map +1 -0
  231. package/dist/shared/query-types.js +9 -0
  232. package/dist/shared/query-types.js.map +1 -0
  233. package/dist/testing.d.ts +125 -37
  234. package/dist/testing.d.ts.map +1 -1
  235. package/dist/testing.js +134 -42
  236. package/dist/testing.js.map +1 -1
  237. package/dist/user-module.d.ts +86 -0
  238. package/dist/user-module.d.ts.map +1 -0
  239. package/dist/user-module.js +55 -0
  240. package/dist/user-module.js.map +1 -0
  241. package/package.json +10 -6
  242. package/dist/lib/conjure.d.ts.map +0 -1
  243. package/dist/lib/conjure.js.map +0 -1
  244. package/dist/lib/hex.d.ts +0 -119
  245. package/dist/lib/hex.d.ts.map +0 -1
  246. package/dist/lib/hex.js +0 -188
  247. package/dist/lib/hex.js.map +0 -1
  248. package/dist/plugins/effect.d.ts +0 -53
  249. package/dist/plugins/effect.d.ts.map +0 -1
  250. package/dist/plugins/effect.js +0 -1074
  251. package/dist/plugins/effect.js.map +0 -1
  252. package/dist/plugins/kysely/queries.d.ts +0 -92
  253. package/dist/plugins/kysely/queries.d.ts.map +0 -1
  254. package/dist/plugins/kysely/queries.js +0 -1169
  255. package/dist/plugins/kysely/queries.js.map +0 -1
  256. package/dist/plugins/kysely/shared.d.ts +0 -59
  257. package/dist/plugins/kysely/shared.d.ts.map +0 -1
  258. package/dist/plugins/kysely/shared.js +0 -247
  259. package/dist/plugins/kysely/shared.js.map +0 -1
  260. package/dist/plugins/kysely/types.d.ts +0 -22
  261. package/dist/plugins/kysely/types.d.ts.map +0 -1
  262. package/dist/plugins/kysely/types.js +0 -428
  263. package/dist/plugins/kysely/types.js.map +0 -1
  264. package/dist/services/artifact-store.d.ts +0 -65
  265. package/dist/services/artifact-store.d.ts.map +0 -1
  266. package/dist/services/artifact-store.js +0 -57
  267. package/dist/services/artifact-store.js.map +0 -1
  268. package/dist/services/core-providers.d.ts +0 -15
  269. package/dist/services/core-providers.d.ts.map +0 -1
  270. package/dist/services/core-providers.js +0 -23
  271. package/dist/services/core-providers.js.map +0 -1
  272. package/dist/services/emissions.d.ts +0 -103
  273. package/dist/services/emissions.d.ts.map +0 -1
  274. package/dist/services/emissions.js +0 -241
  275. package/dist/services/emissions.js.map +0 -1
  276. package/dist/services/execution.d.ts +0 -35
  277. package/dist/services/execution.d.ts.map +0 -1
  278. package/dist/services/execution.js +0 -86
  279. package/dist/services/execution.js.map +0 -1
  280. package/dist/services/file-builder.d.ts +0 -85
  281. package/dist/services/file-builder.d.ts.map +0 -1
  282. package/dist/services/file-builder.js +0 -112
  283. package/dist/services/file-builder.js.map +0 -1
  284. package/dist/services/plugin-meta.d.ts +0 -33
  285. package/dist/services/plugin-meta.d.ts.map +0 -1
  286. package/dist/services/plugin-meta.js +0 -24
  287. package/dist/services/plugin-meta.js.map +0 -1
  288. package/dist/services/plugin-runner.d.ts +0 -42
  289. package/dist/services/plugin-runner.d.ts.map +0 -1
  290. package/dist/services/plugin-runner.js +0 -84
  291. package/dist/services/plugin-runner.js.map +0 -1
  292. package/dist/services/plugin.d.ts +0 -421
  293. package/dist/services/plugin.d.ts.map +0 -1
  294. package/dist/services/plugin.js +0 -197
  295. package/dist/services/plugin.js.map +0 -1
  296. package/dist/services/resolution.d.ts +0 -38
  297. package/dist/services/resolution.d.ts.map +0 -1
  298. package/dist/services/resolution.js +0 -242
  299. package/dist/services/resolution.js.map +0 -1
  300. package/dist/services/service-registry.d.ts +0 -74
  301. package/dist/services/service-registry.d.ts.map +0 -1
  302. package/dist/services/service-registry.js +0 -61
  303. package/dist/services/service-registry.js.map +0 -1
  304. package/dist/services/symbols.d.ts +0 -144
  305. package/dist/services/symbols.d.ts.map +0 -1
  306. package/dist/services/symbols.js +0 -144
  307. package/dist/services/symbols.js.map +0 -1
@@ -1,48 +1,73 @@
1
1
  /**
2
- * HTTP Elysia Plugin - Generate Elysia route handlers from query plugins
2
+ * Elysia HTTP Plugin - Generates Elysia route handlers from query symbols
3
3
  *
4
- * Consumes method symbols from sql-queries or kysely-queries via the symbol registry
5
- * and generates type-safe Elysia HTTP route handlers.
4
+ * Consumes "queries" and "schema" capabilities (provider-agnostic).
5
+ * Works with any queries provider (kysely, drizzle, effect-sql, etc.)
6
+ * and any schema provider (zod, arktype, effect, etc.).
7
+ *
8
+ * Uses the SymbolRegistry to resolve query functions and optionally
9
+ * schema symbols for request validation.
10
+ *
11
+ * Imports are resolved via the cross-reference system:
12
+ * - Calls registry.import(queryCapability).ref() during render
13
+ * - Emit phase generates imports from the recorded references
14
+ */
15
+ import { Effect, Schema as S } from "effect";
16
+ import { IR } from "../services/ir.js";
17
+ import { Inflection } from "../services/inflection.js";
18
+ import { SymbolRegistry } from "../runtime/registry.js";
19
+ import { isTableEntity } from "../ir/semantic-ir.js";
20
+ import { conjure, cast } from "../conjure/index.js";
21
+ import { normalizeFileNaming } from "../runtime/file-assignment.js";
22
+ const b = conjure.b;
23
+ const PLUGIN_NAME = "elysia-http";
24
+ /**
25
+ * Coerce a URL param (always string) to the expected type.
26
+ * Returns an expression that wraps the identifier with the appropriate coercion.
27
+ */
28
+ function coerceParam(paramName, paramType) {
29
+ const ident = b.identifier(paramName);
30
+ const lowerType = paramType.toLowerCase();
31
+ // Numeric types
32
+ if (lowerType === "number" || lowerType === "int" || lowerType === "integer" || lowerType === "bigint") {
33
+ return b.callExpression(b.identifier("Number"), [ident]);
34
+ }
35
+ // Date types
36
+ if (lowerType === "date" || lowerType.includes("timestamp") || lowerType.includes("datetime")) {
37
+ return b.newExpression(b.identifier("Date"), [ident]);
38
+ }
39
+ // Boolean
40
+ if (lowerType === "boolean" || lowerType === "bool") {
41
+ // "true" -> true, anything else -> false
42
+ return b.binaryExpression("===", ident, b.stringLiteral("true"));
43
+ }
44
+ // String, UUID, and other types - no coercion needed
45
+ return ident;
46
+ }
47
+ /**
48
+ * Check if a param needs coercion (comes from URL string).
49
+ */
50
+ function needsCoercion(param) {
51
+ return (param.source === "pk" ||
52
+ param.source === "fk" ||
53
+ param.source === "lookup" ||
54
+ param.source === "pagination");
55
+ }
56
+ const DEFAULT_OUTPUT_DIR = "";
57
+ const DEFAULT_ROUTES_FILE = "routes.ts";
58
+ const DEFAULT_APP_FILE = "routes.ts";
59
+ /**
60
+ * Schema-validated portion of the config (simple types only).
61
+ * FileNaming functions are handled separately since Schema can't validate functions.
6
62
  */
7
- import { Option, pipe, Schema as S } from "effect";
8
- import { definePlugin } from "../services/plugin.js";
9
- import { conjure, cast } from "../lib/conjure.js";
10
- import { inflect } from "../services/inflection.js";
11
- import { SCHEMA_BUILDER_KIND, } from "../ir/extensions/schema-builder.js";
12
- import { getTableEntities, getEnumEntities, } from "../ir/semantic-ir.js";
13
- import { resolveFieldType, isUuidType, isDateType, isEnumType, getPgTypeName, } from "../lib/field-utils.js";
14
- import { findEnumByPgName, TsType } from "../services/pg-types.js";
15
- const { b, stmt } = conjure;
16
- // ============================================================================
17
- // Configuration
18
- // ============================================================================
19
63
  const HttpElysiaConfigSchema = S.Struct({
20
- /** Output directory for generated route files. Default: "routes" */
21
- outputDir: S.optionalWith(S.String, { default: () => "routes" }),
22
- /** Base path for all routes. Default: "/api" */
23
- basePath: S.optionalWith(S.String, { default: () => "/api" }),
24
- /** Header content to prepend to each generated file */
25
- header: S.optional(S.String),
26
- /**
27
- * Name of the aggregated router export.
28
- * Default: "api"
29
- */
30
- aggregatorName: S.optionalWith(S.String, { default: () => "api" }),
64
+ outputDir: S.optionalWith(S.String, { default: () => DEFAULT_OUTPUT_DIR }),
65
+ basePath: S.optionalWith(S.String, { default: () => "" }),
31
66
  });
32
- // ============================================================================
33
- // String Helpers
34
- // ============================================================================
35
- /** Convert PascalCase/camelCase to kebab-case */
36
- const toKebabCase = (str) => str
37
- .replace(/([a-z])([A-Z])/g, "$1-$2")
38
- .replace(/_/g, "-")
39
- .toLowerCase();
40
- /** Convert entity name to URL path segment (kebab-case plural) */
41
- const entityToPathSegment = (entityName) => inflect.pluralize(toKebabCase(entityName));
42
- // ============================================================================
43
- // Route Generation Helpers
44
- // ============================================================================
45
- /** Map query method kind to HTTP method */
67
+ // Manual casing helpers removed - now using inflection service:
68
+ // - inflection.kebabCase(), inflection.pascalCase() for primitives
69
+ // - inflection.elysiaRoutesName() for route variable names
70
+ // - inflection.entityRoutePath() for entity path segments
46
71
  const kindToHttpMethod = (kind) => {
47
72
  switch (kind) {
48
73
  case "read":
@@ -52,15 +77,14 @@ const kindToHttpMethod = (kind) => {
52
77
  case "create":
53
78
  return "post";
54
79
  case "update":
55
- return "put";
80
+ return "patch";
56
81
  case "delete":
57
82
  return "delete";
58
83
  case "function":
59
84
  return "post";
60
85
  }
61
86
  };
62
- /** Get the route path for a method */
63
- const getRoutePath = (method) => {
87
+ const getRoutePath = (method, entityName, inflection) => {
64
88
  switch (method.kind) {
65
89
  case "read":
66
90
  case "update":
@@ -70,208 +94,73 @@ const getRoutePath = (method) => {
70
94
  return `/:${paramName}`;
71
95
  }
72
96
  case "list":
97
+ // Check if this is a cursor pagination method (listBy{Column})
98
+ // These methods have names like: "postListByCreatedAt" -> should be "/by-created-at"
99
+ // Regular list methods (if any existed) would be just: "postList" -> "/"
100
+ if (/ListBy/i.test(method.name) || /listBy/i.test(method.name)) {
101
+ // Extract column name after the list pattern
102
+ // Handles both "postListByCreatedAt" and "postlistByCreatedAt"
103
+ const match = method.name.match(/(?:ListBy|listBy)(.+)/i);
104
+ if (match && match[1]) {
105
+ const columnKebab = inflection.kebabCase(match[1]);
106
+ return `/by-${columnKebab}`;
107
+ }
108
+ }
109
+ // Standard list route
110
+ return "/";
73
111
  case "create":
74
112
  return "/";
75
113
  case "lookup": {
76
114
  const field = method.lookupField ?? "field";
77
- const fieldKebab = toKebabCase(field);
115
+ const fieldKebab = inflection.kebabCase(field);
78
116
  const lookupParam = method.params.find((p) => p.source === "lookup" || p.source === "fk");
79
117
  const paramName = lookupParam?.name ?? field;
80
118
  return `/by-${fieldKebab}/:${paramName}`;
81
119
  }
82
120
  case "function": {
83
- return `/${toKebabCase(method.name)}`;
121
+ return `/${inflection.kebabCase(method.name)}`;
84
122
  }
85
123
  }
86
124
  };
87
- /**
88
- * Build a TypeBox type call: t.String(), t.Number(), etc.
89
- */
90
- const buildTypeBoxCall = (methodName, args = []) => b.callExpression(b.memberExpression(b.identifier("t"), b.identifier(methodName)), args.map(cast.toExpr));
91
- /**
92
- * Map TypeScript type string to TypeBox method name.
93
- * For path params (strings from URL), numeric types use t.Numeric() for coercion.
94
- */
95
- const tsTypeToTypeBoxMethod = (tsType, forPathParam) => {
96
- switch (tsType) {
97
- case TsType.String:
98
- return "String";
99
- case TsType.Number:
100
- // t.Numeric() coerces string to number (for path/query params)
101
- return forPathParam ? "Numeric" : "Number";
102
- case TsType.Boolean:
103
- return "Boolean";
104
- case TsType.BigInt:
105
- // BigInt comes as string from URL params
106
- return forPathParam ? "String" : "BigInt";
107
- case TsType.Date:
108
- // Dates come as strings, need string schema
109
- return "String";
110
- case TsType.Buffer:
111
- case TsType.Unknown:
112
- default:
113
- return "Unknown";
114
- }
115
- };
116
- /**
117
- * Build TypeBox enum schema: t.Union([t.Literal('a'), t.Literal('b'), ...])
118
- */
119
- const buildTypeBoxEnum = (values) => {
120
- const literals = values.map((v) => buildTypeBoxCall("Literal", [b.stringLiteral(v)]));
121
- return buildTypeBoxCall("Union", [b.arrayExpression(literals.map(cast.toExpr))]);
122
- };
123
- /**
124
- * Build TypeBox array schema: t.Array(<inner>)
125
- */
126
- const buildTypeBoxArray = (inner) => buildTypeBoxCall("Array", [cast.toExpr(inner)]);
127
- /**
128
- * Build TypeBox optional schema: t.Optional(<inner>)
129
- */
130
- const buildTypeBoxOptional = (inner) => buildTypeBoxCall("Optional", [cast.toExpr(inner)]);
131
- /**
132
- * Build TypeBox union with null: t.Union([<inner>, t.Null()])
133
- */
134
- const buildTypeBoxNullable = (inner) => buildTypeBoxCall("Union", [
135
- b.arrayExpression([cast.toExpr(inner), cast.toExpr(buildTypeBoxCall("Null"))]),
136
- ]);
137
- /**
138
- * Resolve a Field to its TypeBox schema expression.
139
- *
140
- * @param field - The IR field to resolve
141
- * @param ctx - Context with enums and extensions
142
- * @param forInsert - True if generating for insert shape (fields with defaults are optional)
143
- */
144
- const resolveFieldTypeBoxSchema = (field, ctx, forInsert) => {
145
- let baseSchema;
146
- // 1. UUID types - still strings
147
- if (isUuidType(field)) {
148
- baseSchema = buildTypeBoxCall("String");
149
- }
150
- // 2. Date types - accept string or Date
151
- else if (isDateType(field)) {
152
- baseSchema = buildTypeBoxCall("Union", [
153
- b.arrayExpression([
154
- cast.toExpr(buildTypeBoxCall("String")),
155
- cast.toExpr(buildTypeBoxCall("Date")),
156
- ]),
157
- ]);
158
- }
159
- // 3. Enum types - union of literals
160
- else if (isEnumType(field)) {
161
- const pgTypeName = getPgTypeName(field);
162
- const enumDef = pgTypeName
163
- ? pipe(findEnumByPgName(ctx.enums, pgTypeName), Option.getOrUndefined)
164
- : undefined;
165
- if (enumDef) {
166
- baseSchema = buildTypeBoxEnum(enumDef.values);
167
- }
168
- else {
169
- baseSchema = buildTypeBoxCall("Unknown");
170
- }
171
- }
172
- // 4. Fallback to resolved TypeScript type
173
- else {
174
- const resolved = resolveFieldType(field, ctx.enums, ctx.extensions);
175
- const typeBoxMethod = tsTypeToTypeBoxMethod(resolved.tsType, false);
176
- baseSchema = buildTypeBoxCall(typeBoxMethod);
177
- }
178
- // Wrap with array if needed
179
- if (field.isArray) {
180
- baseSchema = buildTypeBoxArray(baseSchema);
181
- }
182
- // Apply nullable
183
- if (field.nullable) {
184
- baseSchema = buildTypeBoxNullable(baseSchema);
185
- }
186
- // Apply optional (for insert shapes: fields with defaults are optional)
187
- if (forInsert && field.optional) {
188
- baseSchema = buildTypeBoxOptional(baseSchema);
189
- }
190
- return baseSchema;
191
- };
192
- /**
193
- * Build TypeBox object schema for an entity shape.
194
- *
195
- * @param entity - The entity to build schema for
196
- * @param shapeKind - Which shape to use (insert or update)
197
- * @param ctx - Context with enums and extensions
198
- */
199
- const buildEntityTypeBoxSchema = (entity, shapeKind, ctx) => {
200
- const shape = shapeKind === "insert" ? entity.shapes.insert : entity.shapes.update;
201
- if (!shape) {
202
- // Fallback to row shape if specific shape doesn't exist
203
- return buildTypeBoxCall("Unknown");
204
- }
205
- let objBuilder = conjure.obj();
206
- for (const field of shape.fields) {
207
- // For update shapes, ALL fields are optional (patch semantics)
208
- // For insert shapes, only fields with defaults/nullable are optional (already marked in IR)
209
- const applyOptional = shapeKind === "update" || (shapeKind === "insert" && field.optional);
210
- let fieldSchema = resolveFieldTypeBoxSchema(field, ctx, false); // Don't apply optional inside
211
- if (applyOptional) {
212
- fieldSchema = buildTypeBoxOptional(fieldSchema);
213
- }
214
- objBuilder = objBuilder.prop(field.name, fieldSchema);
215
- }
216
- return buildTypeBoxCall("Object", [objBuilder.build()]);
217
- };
218
- /**
219
- * Build TypeBox schema for a path/query parameter based on its type string.
220
- *
221
- * @param param - The query method param
222
- */
223
- const buildParamTypeBoxSchema = (param) => {
224
- const typeBoxMethod = tsTypeToTypeBoxMethod(param.type, true);
225
- return buildTypeBoxCall(typeBoxMethod);
226
- };
227
- /**
228
- * Build the handler function body for a query method.
229
- * Params/query are destructured in the handler signature, so we reference them directly.
230
- */
231
- const buildHandlerBody = (method) => {
125
+ function buildHandlerBody(method) {
232
126
  const callSig = method.callSignature ?? { style: "named" };
233
- // Build the function call arguments based on callSignature
234
127
  const args = [];
235
128
  if (callSig.style === "positional") {
236
- // Positional: fn(a, b, c) - params are already destructured
237
129
  for (const param of method.params) {
238
- if (param.source === "pk" || param.source === "fk" || param.source === "lookup" || param.source === "pagination") {
239
- args.push(b.identifier(param.name));
130
+ if (needsCoercion(param)) {
131
+ // URL params need coercion to their expected type
132
+ args.push(coerceParam(param.name, param.type));
240
133
  }
241
134
  else if (param.source === "body") {
242
135
  args.push(b.identifier("body"));
243
136
  }
244
137
  else {
245
- // No source specified - get from body (body is not destructured)
246
138
  args.push(b.memberExpression(b.identifier("body"), b.identifier(param.name)));
247
139
  }
248
140
  }
249
141
  }
250
142
  else {
251
- // Named: fn({ a, b, c }) or fn({ id, data: body })
252
143
  const bodyParam = method.params.find((p) => p.source === "body");
253
144
  if (bodyParam && callSig.bodyStyle === "spread") {
254
- // Body fields spread directly: fn(body)
255
145
  args.push(b.identifier("body"));
256
146
  }
257
147
  else if (bodyParam && callSig.bodyStyle === "property") {
258
- // Body wrapped in property: fn({ id, data: body })
259
148
  let objBuilder = conjure.obj();
260
149
  for (const param of method.params) {
261
- if (param.source === "pk" || param.source === "fk" || param.source === "lookup" || param.source === "pagination") {
262
- // Params are destructured, use shorthand: { id }
263
- objBuilder = objBuilder.shorthand(param.name);
150
+ if (needsCoercion(param)) {
151
+ // URL params need coercion - use prop() instead of shorthand()
152
+ objBuilder = objBuilder.prop(param.name, coerceParam(param.name, param.type));
264
153
  }
265
154
  }
266
155
  objBuilder = objBuilder.prop(bodyParam.name, b.identifier("body"));
267
156
  args.push(objBuilder.build());
268
157
  }
269
158
  else {
270
- // Build object from path/pagination params using shorthand
271
159
  let objBuilder = conjure.obj();
272
160
  for (const param of method.params) {
273
- if (param.source === "pk" || param.source === "fk" || param.source === "lookup" || param.source === "pagination") {
274
- objBuilder = objBuilder.shorthand(param.name);
161
+ if (needsCoercion(param)) {
162
+ // URL params need coercion - use prop() instead of shorthand()
163
+ objBuilder = objBuilder.prop(param.name, coerceParam(param.name, param.type));
275
164
  }
276
165
  }
277
166
  if (method.params.length > 0) {
@@ -279,129 +168,47 @@ const buildHandlerBody = (method) => {
279
168
  }
280
169
  }
281
170
  }
282
- // Build: const result = await Queries.queryFn(args)
283
- const queryCall = b.callExpression(b.memberExpression(b.identifier("Queries"), b.identifier(method.name)), args.map(cast.toExpr));
284
- const awaitExpr = b.awaitExpression(queryCall);
171
+ // Call the query function directly since we use named imports
172
+ const queryCall = b.callExpression(b.identifier(method.name), args.map(cast.toExpr));
173
+ // Add the appropriate .execute*() method based on query kind
174
+ // - read/lookup(unique): .executeTakeFirst() - returns single row or undefined
175
+ // - list/lookup(non-unique): .execute() - returns array
176
+ // - create/update: .executeTakeFirstOrThrow() - returns single row, throws if not found
177
+ // - delete: .execute() - just executes
178
+ const executeMethod = method.kind === "read" || (method.kind === "lookup" && method.isUniqueLookup)
179
+ ? "executeTakeFirst"
180
+ : method.kind === "create" || method.kind === "update"
181
+ ? "executeTakeFirstOrThrow"
182
+ : "execute";
183
+ const queryWithExecute = b.callExpression(b.memberExpression(queryCall, b.identifier(executeMethod)), []);
184
+ const awaitExpr = b.awaitExpression(queryWithExecute);
285
185
  const resultDecl = stmt.const("result", awaitExpr);
286
- // Handle 404 for read/lookup that returns null
287
186
  if (method.kind === "read" || (method.kind === "lookup" && method.isUniqueLookup)) {
288
- // if (!result) return status(404, 'Not found');
289
187
  const statusCall = b.callExpression(b.identifier("status"), [b.numericLiteral(404), b.stringLiteral("Not found")]);
290
188
  const notFoundCheck = b.ifStatement(b.unaryExpression("!", b.identifier("result")), b.returnStatement(statusCall));
291
189
  return [resultDecl, notFoundCheck, b.returnStatement(b.identifier("result"))];
292
190
  }
293
191
  return [resultDecl, b.returnStatement(b.identifier("result"))];
294
- };
192
+ }
193
+ const stmt = conjure.stmt;
295
194
  /**
296
- * Determine if a method needs body validation and which schema to use.
297
- * Returns the schema import info or null if no body validation needed.
195
+ * Get the body schema name for a method if it needs validation.
298
196
  */
299
- const getBodySchemaImport = (method, entityName) => {
197
+ function getBodySchemaName(method, entityName) {
300
198
  if (method.kind === "create") {
301
- return { entity: entityName, shape: "insert", schemaName: `${entityName}Insert` };
199
+ return `${entityName}Insert`;
302
200
  }
303
201
  if (method.kind === "update") {
304
- return { entity: entityName, shape: "update", schemaName: `${entityName}Update` };
202
+ return `${entityName}Update`;
305
203
  }
306
204
  return null;
307
- };
308
- /**
309
- * Build the route options object (params/body/query validation schemas).
310
- * Returns the options object expression, whether it needs 't' import, body schema info,
311
- * and any schema builder imports needed.
312
- *
313
- * @param hasSchemaProvider - Whether a schema provider is registered for body validation
314
- * @param entity - The entity for inline TypeBox body schema generation (when no schema provider)
315
- * @param typeBoxCtx - Context for TypeBox field resolution
316
- */
317
- const buildRouteOptions = (method, entityName, requestSchema, hasSchemaProvider, entity, typeBoxCtx) => {
318
- let objBuilder = conjure.obj();
319
- let hasOptions = false;
320
- let needsElysiaT = false;
321
- let schemaBuilderImport = null;
322
- let inlineBodySchema = null;
323
- // Build params schema for path parameters
324
- const pathParams = method.params.filter((p) => p.source === "pk" || p.source === "fk" || p.source === "lookup");
325
- if (pathParams.length > 0) {
326
- const schemaResult = requestSchema(pathParams);
327
- if (schemaResult) {
328
- objBuilder = objBuilder.prop("params", schemaResult.ast);
329
- schemaBuilderImport = schemaResult.importSpec;
330
- }
331
- else {
332
- // Fallback to TypeBox t.Object({ ... }) with proper type coercion
333
- needsElysiaT = true;
334
- let paramsBuilder = conjure.obj();
335
- for (const param of pathParams) {
336
- // Use buildParamTypeBoxSchema to get correct type (t.Numeric for numbers)
337
- paramsBuilder = paramsBuilder.prop(param.name, buildParamTypeBoxSchema(param));
338
- }
339
- const paramsSchema = b.callExpression(b.memberExpression(b.identifier("t"), b.identifier("Object")), [paramsBuilder.build()]);
340
- objBuilder = objBuilder.prop("params", paramsSchema);
341
- }
342
- hasOptions = true;
343
- }
344
- // Build query schema for pagination params
345
- const queryParams = method.params.filter((p) => p.source === "pagination");
346
- if (queryParams.length > 0) {
347
- const schemaResult = requestSchema(queryParams);
348
- if (schemaResult) {
349
- objBuilder = objBuilder.prop("query", schemaResult.ast);
350
- schemaBuilderImport = schemaResult.importSpec;
351
- }
352
- else {
353
- // Fallback to TypeBox t.Object({ ... })
354
- needsElysiaT = true;
355
- let queryBuilder = conjure.obj();
356
- for (const param of queryParams) {
357
- const tNumeric = b.callExpression(b.memberExpression(b.identifier("t"), b.identifier("Numeric")), []);
358
- const tOptional = b.callExpression(b.memberExpression(b.identifier("t"), b.identifier("Optional")), [tNumeric]);
359
- queryBuilder = queryBuilder.prop(param.name, tOptional);
360
- }
361
- const querySchema = b.callExpression(b.memberExpression(b.identifier("t"), b.identifier("Object")), [queryBuilder.build()]);
362
- objBuilder = objBuilder.prop("query", querySchema);
363
- }
364
- hasOptions = true;
365
- }
366
- // Body validation
367
- const bodySchemaInfo = getBodySchemaImport(method, entityName);
368
- let bodySchema = null;
369
- if (bodySchemaInfo) {
370
- if (hasSchemaProvider) {
371
- // Use imported schema from schema provider (Zod, Valibot, etc.)
372
- bodySchema = bodySchemaInfo;
373
- objBuilder = objBuilder.prop("body", b.identifier(bodySchema.schemaName));
374
- hasOptions = true;
375
- }
376
- else if (entity) {
377
- // Generate inline TypeBox schema from IR
378
- needsElysiaT = true;
379
- inlineBodySchema = buildEntityTypeBoxSchema(entity, bodySchemaInfo.shape, typeBoxCtx);
380
- objBuilder = objBuilder.prop("body", inlineBodySchema);
381
- hasOptions = true;
382
- }
383
- // If neither schema provider nor entity, skip body validation (body will be unknown)
384
- }
385
- return {
386
- options: hasOptions ? objBuilder.build() : null,
387
- needsElysiaT,
388
- bodySchema,
389
- schemaBuilderImport,
390
- inlineBodySchema,
391
- };
392
- };
393
- /**
394
- * Build a single route method call: .get('/path', handler, options)
395
- */
396
- const buildRouteCall = (method, entityName, requestSchema, hasSchemaProvider, entity, typeBoxCtx) => {
205
+ }
206
+ function buildRouteCall(method, entityName, inflection) {
397
207
  const httpMethod = kindToHttpMethod(method.kind);
398
- const path = getRoutePath(method);
399
- // Build handler: async ({ params: { id }, body, query: { limit }, status }) => { ... }
208
+ const path = getRoutePath(method, entityName, inflection);
400
209
  const handlerProps = [];
401
- // Collect path params (pk, fk, lookup) for destructuring
402
210
  const pathParams = method.params.filter((p) => p.source === "pk" || p.source === "fk" || p.source === "lookup");
403
211
  if (pathParams.length > 0) {
404
- // params: { id, slug, ... }
405
212
  const paramsPattern = b.objectPattern(pathParams.map((p) => {
406
213
  const prop = b.property("init", b.identifier(p.name), b.identifier(p.name));
407
214
  prop.shorthand = true;
@@ -409,7 +216,6 @@ const buildRouteCall = (method, entityName, requestSchema, hasSchemaProvider, en
409
216
  }));
410
217
  handlerProps.push(b.property("init", b.identifier("params"), paramsPattern));
411
218
  }
412
- // Add body if: explicit body param, create/update method, or function with params (no source = from body)
413
219
  const needsBody = method.params.some((p) => p.source === "body") ||
414
220
  method.kind === "create" ||
415
221
  method.kind === "update" ||
@@ -419,10 +225,8 @@ const buildRouteCall = (method, entityName, requestSchema, hasSchemaProvider, en
419
225
  prop.shorthand = true;
420
226
  handlerProps.push(prop);
421
227
  }
422
- // Collect pagination params for destructuring
423
228
  const paginationParams = method.params.filter((p) => p.source === "pagination");
424
229
  if (paginationParams.length > 0) {
425
- // query: { limit, offset, ... }
426
230
  const queryPattern = b.objectPattern(paginationParams.map((p) => {
427
231
  const prop = b.property("init", b.identifier(p.name), b.identifier(p.name));
428
232
  prop.shorthand = true;
@@ -439,175 +243,246 @@ const buildRouteCall = (method, entityName, requestSchema, hasSchemaProvider, en
439
243
  const handlerBody = buildHandlerBody(method);
440
244
  const handler = b.arrowFunctionExpression([handlerParamPattern], b.blockStatement(handlerBody.map(cast.toStmt)));
441
245
  handler.async = true;
442
- const { options, needsElysiaT, bodySchema, schemaBuilderImport } = buildRouteOptions(method, entityName, requestSchema, hasSchemaProvider, entity, typeBoxCtx);
443
- return { httpMethod, path, handler, options, needsElysiaT, bodySchema, schemaBuilderImport };
444
- };
445
- // ============================================================================
446
- // Plugin Definition
447
- // ============================================================================
448
- export function httpElysia(config) {
449
- const parsed = S.decodeUnknownSync(HttpElysiaConfigSchema)(config);
450
- return definePlugin({
451
- name: "http-elysia",
452
- kind: "http-routes",
453
- singleton: true,
454
- canProvide: () => true,
455
- // Declare dependencies: queries required, schemas optional (falls back to TypeBox)
456
- requires: () => [
457
- { kind: "queries", params: {} },
458
- ],
459
- optionalRequires: () => [
460
- { kind: "schemas", params: {} },
461
- ],
462
- provide: (_params, _deps, ctx) => {
463
- const { outputDir, basePath, header, aggregatorName } = parsed;
464
- // Get all entities with registered query methods
465
- const entityNames = ctx.symbols.getEntitiesWithMethods();
466
- if (entityNames.length === 0) {
467
- // No query methods registered - nothing to generate
468
- return;
246
+ // Build route options with body schema validation
247
+ const bodySchemaName = getBodySchemaName(method, entityName);
248
+ let options = null;
249
+ if (bodySchemaName) {
250
+ // { body: EntityInsert } or { body: EntityUpdate }
251
+ options = conjure
252
+ .obj()
253
+ .prop("body", b.identifier(bodySchemaName))
254
+ .build();
255
+ }
256
+ return { httpMethod, path, handler, needsBody, bodySchemaName, options };
257
+ }
258
+ /**
259
+ * Generate Elysia routes for an entity.
260
+ *
261
+ * @param entityName - The entity name
262
+ * @param queries - Query extension metadata
263
+ * @param config - Plugin config
264
+ * @param registry - Symbol registry for recording cross-references
265
+ */
266
+ function generateElysiaRoutes(entityName, queries, config, registry, inflection) {
267
+ // Use inflection.entityRoutePath which handles pluralization and kebab-casing
268
+ const prefix = inflection.entityRoutePath(entityName);
269
+ // Use same name as the symbol declaration for consistency with cross-references
270
+ const routesVarName = inflection.variableName(entityName, "ElysiaRoutes");
271
+ // Build prefix: ensure proper slashes with basePath
272
+ const basePath = config.basePath.replace(/^\/+|\/+$/g, ""); // trim slashes
273
+ const fullPrefix = basePath ? `/${basePath}${prefix}` : prefix;
274
+ let chainExpr = b.newExpression(b.identifier("Elysia"), [
275
+ conjure
276
+ .obj()
277
+ .prop("prefix", b.stringLiteral(fullPrefix))
278
+ .build(),
279
+ ]);
280
+ for (const method of queries.methods) {
281
+ // Record cross-reference for this query method via registry
282
+ // This allows emit phase to generate the import automatically
283
+ // Use generic prefix - registry resolves to implementation (e.g., queries:kysely:...)
284
+ const methodCapability = `queries:${entityName}:${getMethodCapabilitySuffix(method, inflection)}`;
285
+ if (registry.has(methodCapability)) {
286
+ registry.import(methodCapability).ref();
287
+ }
288
+ const { httpMethod, path, handler, bodySchemaName, options } = buildRouteCall(method, entityName, inflection);
289
+ if (bodySchemaName) {
290
+ // Use registry to import schema - this ensures correct relative path resolution
291
+ // Use generic prefix - registry resolves to implementation (e.g., schema:zod:...)
292
+ const schemaCapability = `schema:${bodySchemaName}`;
293
+ if (registry.has(schemaCapability)) {
294
+ registry.import(schemaCapability).ref();
469
295
  }
470
- // Track generated routes for aggregator index
471
- const generatedRoutes = [];
472
- // Build TypeBox context for inline schema generation (when no schema plugin)
473
- const enumEntities = getEnumEntities(ctx.ir);
474
- const tableEntities = getTableEntities(ctx.ir);
475
- const typeBoxCtx = {
476
- enums: enumEntities,
477
- extensions: ctx.ir.extensions,
478
- };
479
- // Generate routes for each entity
480
- for (const entityName of entityNames) {
481
- const entityMethods = ctx.symbols.getEntityMethods(entityName);
482
- if (!entityMethods || entityMethods.methods.length === 0)
296
+ }
297
+ // Build route call: .get(path, handler) or .post(path, handler, { body: Schema })
298
+ const callArgs = [b.stringLiteral(path), handler];
299
+ if (options) {
300
+ callArgs.push(options);
301
+ }
302
+ chainExpr = b.callExpression(b.memberExpression(cast.toExpr(chainExpr), b.identifier(httpMethod)), callArgs.map(cast.toExpr));
303
+ }
304
+ const variableDeclarator = b.variableDeclarator(b.identifier(routesVarName), cast.toExpr(chainExpr));
305
+ const variableDeclaration = b.variableDeclaration("const", [variableDeclarator]);
306
+ // Only external package imports go here; query and schema imports are handled via cross-references
307
+ const externalImports = [
308
+ { from: "elysia", names: ["Elysia"] },
309
+ ];
310
+ return {
311
+ statements: [variableDeclaration],
312
+ externalImports,
313
+ };
314
+ }
315
+ /**
316
+ * Get the capability suffix for a query method.
317
+ * E.g., "findById", "list", "create", "update", "delete", "findByEmail"
318
+ */
319
+ function getMethodCapabilitySuffix(method, inflection) {
320
+ // The capability suffix is derived from the method name
321
+ // e.g., "userFindById" -> "findById", "userList" -> "list"
322
+ // We use generic capabilities that the registry resolves to the implementation.
323
+ // Generic capabilities (resolved by registry):
324
+ // - queries:User:findById → queries:kysely:User:findById (if kysely provides)
325
+ // - queries:User:list → queries:kysely:User:list
326
+ // - queries:User:create → queries:kysely:User:create
327
+ // - queries:User:update → queries:kysely:User:update
328
+ // - queries:User:delete → queries:kysely:User:delete
329
+ // - queries:User:findByEmail → queries:kysely:User:findByEmail
330
+ // The method.name is like "userFindById", "userList", etc.
331
+ // We need to extract just the operation part
332
+ // Method kinds map to capability suffixes:
333
+ switch (method.kind) {
334
+ case "read":
335
+ return "findById";
336
+ case "list":
337
+ return "list";
338
+ case "create":
339
+ return "create";
340
+ case "update":
341
+ return "update";
342
+ case "delete":
343
+ return "delete";
344
+ case "lookup":
345
+ // For lookups, the suffix is like "findByEmail" where Email is the field
346
+ if (method.lookupField) {
347
+ const pascalField = inflection.pascalCase(method.lookupField);
348
+ return `findBy${pascalField}`;
349
+ }
350
+ return "lookup";
351
+ case "function":
352
+ return method.name;
353
+ }
354
+ }
355
+ function generateAggregator(entities, config, registry, inflection) {
356
+ const entityEntries = Array.from(entities.entries());
357
+ if (entityEntries.length === 0) {
358
+ return { statements: [], externalImports: [] };
359
+ }
360
+ let chainExpr = b.newExpression(b.identifier("Elysia"), []);
361
+ const externalImports = [{ from: "elysia", names: ["Elysia"] }];
362
+ for (const [entityName, queries] of entityEntries) {
363
+ // Use same name as the symbol declaration for consistency with cross-references
364
+ const routesVarName = inflection.variableName(entityName, "ElysiaRoutes");
365
+ chainExpr = b.callExpression(b.memberExpression(cast.toExpr(chainExpr), b.identifier("use")), [b.identifier(routesVarName)]);
366
+ // Record cross-reference to the entity's routes capability
367
+ // The emit phase will generate the import automatically
368
+ const routeCapability = `http-routes:elysia:${entityName}`;
369
+ if (registry.has(routeCapability)) {
370
+ registry.import(routeCapability).ref();
371
+ }
372
+ }
373
+ const variableDeclarator = b.variableDeclarator(b.identifier("app"), cast.toExpr(chainExpr));
374
+ const variableDeclaration = b.variableDeclaration("const", [variableDeclarator]);
375
+ return {
376
+ statements: [variableDeclaration],
377
+ externalImports,
378
+ };
379
+ }
380
+ export function elysia(config) {
381
+ const schemaConfig = S.decodeSync(HttpElysiaConfigSchema)(config ?? {});
382
+ // Resolve FileNaming functions (Schema can't validate these)
383
+ const resolvedConfig = {
384
+ outputDir: schemaConfig.outputDir,
385
+ basePath: schemaConfig.basePath,
386
+ routesFile: normalizeFileNaming(config?.routesFile, DEFAULT_ROUTES_FILE),
387
+ appFile: normalizeFileNaming(config?.appFile, DEFAULT_APP_FILE),
388
+ };
389
+ return {
390
+ name: PLUGIN_NAME,
391
+ provides: [],
392
+ fileDefaults: [
393
+ // Entity routes use routesFile config
394
+ {
395
+ pattern: "http-routes:elysia:",
396
+ outputDir: resolvedConfig.outputDir,
397
+ fileNaming: resolvedConfig.routesFile,
398
+ },
399
+ // App aggregator uses appFile config (more specific pattern wins)
400
+ {
401
+ pattern: "http-routes:elysia:app",
402
+ outputDir: resolvedConfig.outputDir,
403
+ fileNaming: resolvedConfig.appFile,
404
+ },
405
+ ],
406
+ declare: Effect.gen(function* () {
407
+ const ir = yield* IR;
408
+ const inflection = yield* Inflection;
409
+ const declarations = [];
410
+ // Declare routes for all table entities that might have queries
411
+ // The actual routes generated depend on what queries exist at render time
412
+ for (const entity of ir.entities.values()) {
413
+ if (!isTableEntity(entity))
483
414
  continue;
484
- // Find the entity in the IR for inline TypeBox body schema generation
485
- const entity = tableEntities.find((e) => e.name === entityName);
486
- const pathSegment = entityToPathSegment(entityName);
487
- const filePath = `${outputDir}/${inflect.uncapitalize(entityName)}.ts`;
488
- const routesVarName = `${inflect.uncapitalize(entityName)}Routes`;
489
- const file = ctx.file(filePath);
490
- if (header) {
491
- file.header(header);
492
- }
493
- // Import Elysia
494
- file.import({ kind: "package", names: ["Elysia"], from: "elysia" });
495
- // Import queries as namespace
496
- const queriesImportPath = `../${entityMethods.importPath.replace(/\.ts$/, ".js")}`;
497
- file.import({
498
- kind: "relative",
499
- namespace: "Queries",
500
- from: queriesImportPath,
501
- });
502
- // Build the Elysia route chain
503
- // new Elysia({ prefix: '/api/users' }).get(...).post(...)
504
- const prefixPath = `${basePath}/${pathSegment}`;
505
- const elysiaConfig = conjure.obj().prop("prefix", b.stringLiteral(prefixPath)).build();
506
- let chainExpr = b.newExpression(b.identifier("Elysia"), [elysiaConfig]);
507
- let fileNeedsElysiaT = false;
508
- const bodySchemaImports = [];
509
- let schemaLibraryImport = null;
510
- // Create a schema builder function that requests from the service registry
511
- const requestSchema = (params) => {
512
- if (params.length === 0)
513
- return undefined;
514
- try {
515
- const request = { variant: "params", params };
516
- return ctx.request(SCHEMA_BUILDER_KIND, request);
517
- }
518
- catch {
519
- // No schema-builder registered, will fall back to TypeBox
520
- return undefined;
521
- }
522
- };
523
- // Check if a schema provider is available for body validation
524
- // We probe by checking if the schema-builder service exists
525
- let hasSchemaProvider = false;
526
- try {
527
- // Try to request an entity schema - if it succeeds, we have a provider
528
- ctx.request(SCHEMA_BUILDER_KIND, { variant: "entity", entity: entityName, shape: "insert" });
529
- hasSchemaProvider = true;
530
- }
531
- catch {
532
- // No schema provider registered
533
- }
534
- for (const method of entityMethods.methods) {
535
- const { httpMethod, path, handler, options, needsElysiaT, bodySchema, schemaBuilderImport } = buildRouteCall(method, entityName, requestSchema, hasSchemaProvider, entity, typeBoxCtx);
536
- if (needsElysiaT)
537
- fileNeedsElysiaT = true;
538
- if (bodySchema)
539
- bodySchemaImports.push(bodySchema);
540
- if (schemaBuilderImport)
541
- schemaLibraryImport = schemaBuilderImport;
542
- const callArgs = [b.stringLiteral(path), handler];
543
- if (options) {
544
- callArgs.push(options);
545
- }
546
- chainExpr = b.callExpression(b.memberExpression(cast.toExpr(chainExpr), b.identifier(httpMethod)), callArgs.map(cast.toExpr));
547
- }
548
- if (fileNeedsElysiaT) {
549
- file.import({ kind: "package", names: ["t"], from: "elysia" });
550
- }
551
- // Import schema library (e.g., Zod) if using schema-builder for params
552
- if (schemaLibraryImport) {
553
- if (schemaLibraryImport.names) {
554
- file.import({
555
- kind: "package",
556
- names: [...schemaLibraryImport.names],
557
- from: schemaLibraryImport.from,
558
- });
559
- }
560
- else if (schemaLibraryImport.namespace) {
561
- file.import({
562
- kind: "package",
563
- namespace: schemaLibraryImport.namespace,
564
- from: schemaLibraryImport.from,
565
- });
566
- }
567
- }
568
- // Import body schemas from schema plugins (Zod, Valibot, etc.)
569
- // These are resolved via the symbol registry's "schemas" capability
570
- for (const schemaImport of bodySchemaImports) {
571
- file.import({
572
- kind: "symbol",
573
- ref: {
574
- capability: "schemas",
575
- entity: schemaImport.entity,
576
- shape: schemaImport.shape,
577
- },
415
+ if (entity.tags.omit === true)
416
+ continue;
417
+ // If entity has any CRUD permissions, it could have queries
418
+ const hasAnyPermissions = entity.permissions.canSelect ||
419
+ entity.permissions.canInsert ||
420
+ entity.permissions.canUpdate ||
421
+ entity.permissions.canDelete;
422
+ if (hasAnyPermissions) {
423
+ declarations.push({
424
+ name: inflection.variableName(entity.name, "ElysiaRoutes"),
425
+ capability: `http-routes:elysia:${entity.name}`,
426
+ baseEntityName: entity.name,
578
427
  });
579
428
  }
580
- const exportStmt = conjure.export.const(routesVarName, chainExpr);
581
- file.ast(conjure.program(exportStmt)).emit();
582
- generatedRoutes.push({
583
- fileName: `${inflect.uncapitalize(entityName)}.js`,
584
- exportName: routesVarName,
585
- });
586
429
  }
587
- // Generate aggregator index.ts
588
- if (generatedRoutes.length > 0) {
589
- const indexPath = `${outputDir}/index.ts`;
590
- const indexFile = ctx.file(indexPath);
591
- if (header) {
592
- indexFile.header(header);
430
+ // Also declare the aggregator
431
+ declarations.push({
432
+ name: "elysiaApp",
433
+ capability: "http-routes:elysia:app",
434
+ });
435
+ return declarations;
436
+ }),
437
+ render: Effect.gen(function* () {
438
+ const ir = yield* IR;
439
+ const registry = yield* SymbolRegistry;
440
+ const inflection = yield* Inflection;
441
+ const rendered = [];
442
+ // Query the registry for all entity query capabilities
443
+ // Use generic prefix - registry resolves to implementation provider
444
+ const entityQueries = new Map();
445
+ const queryCapabilities = registry.query("queries:");
446
+ for (const decl of queryCapabilities) {
447
+ // Only look at aggregate capabilities (queries:impl:EntityName, not queries:impl:EntityName:method)
448
+ const parts = decl.capability.split(":");
449
+ if (parts.length !== 3)
450
+ continue; // Skip method-specific capabilities
451
+ const entityName = parts[2];
452
+ const metadata = registry.getMetadata(decl.capability);
453
+ if (metadata && typeof metadata === "object" && "methods" in metadata) {
454
+ entityQueries.set(entityName, metadata);
593
455
  }
594
- indexFile.import({ kind: "package", names: ["Elysia"], from: "elysia" });
595
- for (const route of generatedRoutes) {
596
- indexFile.import({
597
- kind: "relative",
598
- names: [route.exportName],
599
- from: `./${route.fileName}`,
600
- });
601
- }
602
- // Build: new Elysia().use(userRoutes).use(postRoutes)...
603
- let chainExpr = b.newExpression(b.identifier("Elysia"), []);
604
- for (const route of generatedRoutes) {
605
- chainExpr = b.callExpression(b.memberExpression(cast.toExpr(chainExpr), b.identifier("use")), [b.identifier(route.exportName)]);
606
- }
607
- const exportStmt = conjure.export.const(aggregatorName, chainExpr);
608
- indexFile.ast(conjure.program(exportStmt)).emit();
609
456
  }
610
- },
611
- });
457
+ for (const [entityName, queries] of entityQueries) {
458
+ const entity = ir.entities.get(entityName);
459
+ if (!entity || !isTableEntity(entity))
460
+ continue;
461
+ const capability = `http-routes:elysia:${entityName}`;
462
+ // Scope cross-references to this specific capability
463
+ const { statements, externalImports } = registry.forSymbol(capability, () => generateElysiaRoutes(entityName, queries, resolvedConfig, registry, inflection));
464
+ rendered.push({
465
+ name: inflection.variableName(entityName, "ElysiaRoutes"),
466
+ capability,
467
+ node: statements[0],
468
+ exports: "named",
469
+ externalImports,
470
+ });
471
+ }
472
+ if (entityQueries.size > 0) {
473
+ const appCapability = "http-routes:elysia:app";
474
+ // Scope cross-references to the app capability
475
+ const { statements, externalImports } = registry.forSymbol(appCapability, () => generateAggregator(entityQueries, resolvedConfig, registry, inflection));
476
+ rendered.push({
477
+ name: "elysiaApp",
478
+ capability: appCapability,
479
+ node: statements[0],
480
+ exports: "named",
481
+ externalImports,
482
+ });
483
+ }
484
+ return rendered;
485
+ }),
486
+ };
612
487
  }
613
488
  //# sourceMappingURL=http-elysia.js.map