@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.
- package/bin/pgsourcerer +2 -0
- package/dist/__tests__/fixtures/index.d.ts +15 -0
- package/dist/__tests__/fixtures/index.d.ts.map +1 -0
- package/dist/__tests__/fixtures/index.js +19 -0
- package/dist/__tests__/fixtures/index.js.map +1 -0
- package/dist/__tests__/fixtures/introspection.json +40522 -0
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +7 -46
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +38 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -2
- package/dist/config.js.map +1 -1
- package/dist/{lib/conjure.d.ts → conjure/index.d.ts} +62 -3
- package/dist/conjure/index.d.ts.map +1 -0
- package/dist/{lib/conjure.js → conjure/index.js} +124 -3
- package/dist/conjure/index.js.map +1 -0
- package/dist/conjure/signature.d.ts +85 -0
- package/dist/conjure/signature.d.ts.map +1 -0
- package/dist/conjure/signature.js +130 -0
- package/dist/conjure/signature.js.map +1 -0
- package/dist/conjure/types.d.ts +97 -0
- package/dist/conjure/types.d.ts.map +1 -0
- package/dist/conjure/types.js +206 -0
- package/dist/conjure/types.js.map +1 -0
- package/dist/errors.d.ts +114 -139
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +82 -36
- package/dist/errors.js.map +1 -1
- package/dist/generate.d.ts +45 -46
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +86 -59
- package/dist/generate.js.map +1 -1
- package/dist/hex/builder.d.ts +12 -0
- package/dist/hex/builder.d.ts.map +1 -0
- package/dist/hex/builder.js +64 -0
- package/dist/hex/builder.js.map +1 -0
- package/dist/hex/ddl.d.ts +53 -0
- package/dist/hex/ddl.d.ts.map +1 -0
- package/dist/hex/ddl.js +306 -0
- package/dist/hex/ddl.js.map +1 -0
- package/dist/hex/index.d.ts +105 -0
- package/dist/hex/index.d.ts.map +1 -0
- package/dist/hex/index.js +81 -0
- package/dist/hex/index.js.map +1 -0
- package/dist/hex/primitives.d.ts +23 -0
- package/dist/hex/primitives.d.ts.map +1 -0
- package/dist/hex/primitives.js +38 -0
- package/dist/hex/primitives.js.map +1 -0
- package/dist/hex/query.d.ts +116 -0
- package/dist/hex/query.d.ts.map +1 -0
- package/dist/hex/query.js +219 -0
- package/dist/hex/query.js.map +1 -0
- package/dist/hex/types.d.ts +287 -0
- package/dist/hex/types.d.ts.map +1 -0
- package/dist/hex/types.js +431 -0
- package/dist/hex/types.js.map +1 -0
- package/dist/index.d.ts +17 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -44
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +76 -140
- package/dist/init.js.map +1 -1
- package/dist/ir/extensions/queries.d.ts +6 -6
- package/dist/ir/extensions/queries.d.ts.map +1 -1
- package/dist/ir/extensions/queries.js +6 -4
- package/dist/ir/extensions/queries.js.map +1 -1
- package/dist/ir/extensions/schema-builder.d.ts.map +1 -1
- package/dist/ir/extensions/schema-builder.js.map +1 -1
- package/dist/ir/index.d.ts.map +1 -1
- package/dist/ir/index.js.map +1 -1
- package/dist/ir/relation-graph.d.ts.map +1 -1
- package/dist/ir/relation-graph.js +8 -8
- package/dist/ir/relation-graph.js.map +1 -1
- package/dist/ir/semantic-ir.d.ts +38 -0
- package/dist/ir/semantic-ir.d.ts.map +1 -1
- package/dist/ir/semantic-ir.js +50 -2
- package/dist/ir/semantic-ir.js.map +1 -1
- package/dist/ir/smart-tags.d.ts.map +1 -1
- package/dist/ir/smart-tags.js.map +1 -1
- package/dist/lib/field-utils.d.ts.map +1 -1
- package/dist/lib/field-utils.js +7 -7
- package/dist/lib/field-utils.js.map +1 -1
- package/dist/lib/join-graph.d.ts +95 -0
- package/dist/lib/join-graph.d.ts.map +1 -0
- package/dist/lib/join-graph.js +305 -0
- package/dist/lib/join-graph.js.map +1 -0
- package/dist/lib/picker.d.ts +60 -0
- package/dist/lib/picker.d.ts.map +1 -0
- package/dist/lib/picker.js +325 -0
- package/dist/lib/picker.js.map +1 -0
- package/dist/plugins/arktype.d.ts +20 -24
- package/dist/plugins/arktype.d.ts.map +1 -1
- package/dist/plugins/arktype.js +462 -386
- package/dist/plugins/arktype.js.map +1 -1
- package/dist/plugins/effect/http.d.ts +7 -0
- package/dist/plugins/effect/http.d.ts.map +1 -0
- package/dist/plugins/effect/http.js +460 -0
- package/dist/plugins/effect/http.js.map +1 -0
- package/dist/plugins/effect/index.d.ts +22 -0
- package/dist/plugins/effect/index.d.ts.map +1 -0
- package/dist/plugins/effect/index.js +65 -0
- package/dist/plugins/effect/index.js.map +1 -0
- package/dist/plugins/effect/models.d.ts +6 -0
- package/dist/plugins/effect/models.d.ts.map +1 -0
- package/dist/plugins/effect/models.js +116 -0
- package/dist/plugins/effect/models.js.map +1 -0
- package/dist/plugins/effect/repos.d.ts +21 -0
- package/dist/plugins/effect/repos.d.ts.map +1 -0
- package/dist/plugins/effect/repos.js +131 -0
- package/dist/plugins/effect/repos.js.map +1 -0
- package/dist/plugins/effect/schemas.d.ts +7 -0
- package/dist/plugins/effect/schemas.d.ts.map +1 -0
- package/dist/plugins/effect/schemas.js +75 -0
- package/dist/plugins/effect/schemas.js.map +1 -0
- package/dist/plugins/effect/shared.d.ts +116 -0
- package/dist/plugins/effect/shared.d.ts.map +1 -0
- package/dist/plugins/effect/shared.js +164 -0
- package/dist/plugins/effect/shared.js.map +1 -0
- package/dist/plugins/http-elysia.d.ts +20 -27
- package/dist/plugins/http-elysia.d.ts.map +1 -1
- package/dist/plugins/http-elysia.js +350 -475
- package/dist/plugins/http-elysia.js.map +1 -1
- package/dist/plugins/http-express.d.ts +20 -31
- package/dist/plugins/http-express.d.ts.map +1 -1
- package/dist/plugins/http-express.js +281 -268
- package/dist/plugins/http-express.js.map +1 -1
- package/dist/plugins/http-hono.d.ts +17 -33
- package/dist/plugins/http-hono.d.ts.map +1 -1
- package/dist/plugins/http-hono.js +317 -341
- package/dist/plugins/http-hono.js.map +1 -1
- package/dist/plugins/http-orpc.d.ts +34 -33
- package/dist/plugins/http-orpc.d.ts.map +1 -1
- package/dist/plugins/http-orpc.js +345 -257
- package/dist/plugins/http-orpc.js.map +1 -1
- package/dist/plugins/http-trpc.d.ts +33 -35
- package/dist/plugins/http-trpc.d.ts.map +1 -1
- package/dist/plugins/http-trpc.js +337 -241
- package/dist/plugins/http-trpc.js.map +1 -1
- package/dist/plugins/kysely.d.ts +54 -59
- package/dist/plugins/kysely.d.ts.map +1 -1
- package/dist/plugins/kysely.js +826 -687
- package/dist/plugins/kysely.js.map +1 -1
- package/dist/plugins/sql-queries.d.ts +38 -44
- package/dist/plugins/sql-queries.d.ts.map +1 -1
- package/dist/plugins/sql-queries.js +497 -897
- package/dist/plugins/sql-queries.js.map +1 -1
- package/dist/plugins/types.d.ts +12 -20
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/plugins/types.js +84 -227
- package/dist/plugins/types.js.map +1 -1
- package/dist/plugins/valibot.d.ts +7 -44
- package/dist/plugins/valibot.d.ts.map +1 -1
- package/dist/plugins/valibot.js +376 -382
- package/dist/plugins/valibot.js.map +1 -1
- package/dist/plugins/zod.d.ts +20 -24
- package/dist/plugins/zod.d.ts.map +1 -1
- package/dist/plugins/zod.js +370 -367
- package/dist/plugins/zod.js.map +1 -1
- package/dist/runtime/emit.d.ts +64 -0
- package/dist/runtime/emit.d.ts.map +1 -0
- package/dist/runtime/emit.js +445 -0
- package/dist/runtime/emit.js.map +1 -0
- package/dist/runtime/errors.d.ts +36 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +29 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/file-assignment.d.ts +161 -0
- package/dist/runtime/file-assignment.d.ts.map +1 -0
- package/dist/runtime/file-assignment.js +195 -0
- package/dist/runtime/file-assignment.js.map +1 -0
- package/dist/runtime/orchestrator.d.ts +62 -0
- package/dist/runtime/orchestrator.d.ts.map +1 -0
- package/dist/runtime/orchestrator.js +99 -0
- package/dist/runtime/orchestrator.js.map +1 -0
- package/dist/runtime/registry.d.ts +268 -0
- package/dist/runtime/registry.d.ts.map +1 -0
- package/dist/runtime/registry.js +436 -0
- package/dist/runtime/registry.js.map +1 -0
- package/dist/runtime/types.d.ts +182 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/runtime/validation.d.ts +41 -0
- package/dist/runtime/validation.d.ts.map +1 -0
- package/dist/runtime/validation.js +70 -0
- package/dist/runtime/validation.js.map +1 -0
- package/dist/services/config-loader.d.ts.map +1 -1
- package/dist/services/config-loader.js +15 -6
- package/dist/services/config-loader.js.map +1 -1
- package/dist/services/config.d.ts +55 -25
- package/dist/services/config.d.ts.map +1 -1
- package/dist/services/config.js +60 -34
- package/dist/services/config.js.map +1 -1
- package/dist/services/file-writer.d.ts +3 -3
- package/dist/services/file-writer.d.ts.map +1 -1
- package/dist/services/file-writer.js +6 -8
- package/dist/services/file-writer.js.map +1 -1
- package/dist/services/inflection.d.ts +126 -27
- package/dist/services/inflection.d.ts.map +1 -1
- package/dist/services/inflection.js +300 -72
- package/dist/services/inflection.js.map +1 -1
- package/dist/services/introspection.d.ts.map +1 -1
- package/dist/services/introspection.js +6 -6
- package/dist/services/introspection.js.map +1 -1
- package/dist/services/ir-builder.d.ts.map +1 -1
- package/dist/services/ir-builder.js +73 -77
- package/dist/services/ir-builder.js.map +1 -1
- package/dist/services/ir.d.ts.map +1 -1
- package/dist/services/ir.js.map +1 -1
- package/dist/services/pg-types.d.ts.map +1 -1
- package/dist/services/pg-types.js +3 -3
- package/dist/services/pg-types.js.map +1 -1
- package/dist/services/smart-tags-parser.d.ts.map +1 -1
- package/dist/services/smart-tags-parser.js +4 -4
- package/dist/services/smart-tags-parser.js.map +1 -1
- package/dist/services/type-hints.d.ts.map +1 -1
- package/dist/services/type-hints.js +1 -1
- package/dist/services/type-hints.js.map +1 -1
- package/dist/services/user-module-parser.d.ts +46 -0
- package/dist/services/user-module-parser.d.ts.map +1 -0
- package/dist/services/user-module-parser.js +181 -0
- package/dist/services/user-module-parser.js.map +1 -0
- package/dist/shared/converters.d.ts +60 -0
- package/dist/shared/converters.d.ts.map +1 -0
- package/dist/shared/converters.js +168 -0
- package/dist/shared/converters.js.map +1 -0
- package/dist/shared/query-types.d.ts +95 -0
- package/dist/shared/query-types.d.ts.map +1 -0
- package/dist/shared/query-types.js +9 -0
- package/dist/shared/query-types.js.map +1 -0
- package/dist/testing.d.ts +125 -37
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +134 -42
- package/dist/testing.js.map +1 -1
- package/dist/user-module.d.ts +86 -0
- package/dist/user-module.d.ts.map +1 -0
- package/dist/user-module.js +55 -0
- package/dist/user-module.js.map +1 -0
- package/package.json +10 -6
- package/dist/lib/conjure.d.ts.map +0 -1
- package/dist/lib/conjure.js.map +0 -1
- package/dist/lib/hex.d.ts +0 -119
- package/dist/lib/hex.d.ts.map +0 -1
- package/dist/lib/hex.js +0 -188
- package/dist/lib/hex.js.map +0 -1
- package/dist/plugins/effect.d.ts +0 -53
- package/dist/plugins/effect.d.ts.map +0 -1
- package/dist/plugins/effect.js +0 -1074
- package/dist/plugins/effect.js.map +0 -1
- package/dist/plugins/kysely/queries.d.ts +0 -92
- package/dist/plugins/kysely/queries.d.ts.map +0 -1
- package/dist/plugins/kysely/queries.js +0 -1169
- package/dist/plugins/kysely/queries.js.map +0 -1
- package/dist/plugins/kysely/shared.d.ts +0 -59
- package/dist/plugins/kysely/shared.d.ts.map +0 -1
- package/dist/plugins/kysely/shared.js +0 -247
- package/dist/plugins/kysely/shared.js.map +0 -1
- package/dist/plugins/kysely/types.d.ts +0 -22
- package/dist/plugins/kysely/types.d.ts.map +0 -1
- package/dist/plugins/kysely/types.js +0 -428
- package/dist/plugins/kysely/types.js.map +0 -1
- package/dist/services/artifact-store.d.ts +0 -65
- package/dist/services/artifact-store.d.ts.map +0 -1
- package/dist/services/artifact-store.js +0 -57
- package/dist/services/artifact-store.js.map +0 -1
- package/dist/services/core-providers.d.ts +0 -15
- package/dist/services/core-providers.d.ts.map +0 -1
- package/dist/services/core-providers.js +0 -23
- package/dist/services/core-providers.js.map +0 -1
- package/dist/services/emissions.d.ts +0 -103
- package/dist/services/emissions.d.ts.map +0 -1
- package/dist/services/emissions.js +0 -241
- package/dist/services/emissions.js.map +0 -1
- package/dist/services/execution.d.ts +0 -35
- package/dist/services/execution.d.ts.map +0 -1
- package/dist/services/execution.js +0 -86
- package/dist/services/execution.js.map +0 -1
- package/dist/services/file-builder.d.ts +0 -85
- package/dist/services/file-builder.d.ts.map +0 -1
- package/dist/services/file-builder.js +0 -112
- package/dist/services/file-builder.js.map +0 -1
- package/dist/services/plugin-meta.d.ts +0 -33
- package/dist/services/plugin-meta.d.ts.map +0 -1
- package/dist/services/plugin-meta.js +0 -24
- package/dist/services/plugin-meta.js.map +0 -1
- package/dist/services/plugin-runner.d.ts +0 -42
- package/dist/services/plugin-runner.d.ts.map +0 -1
- package/dist/services/plugin-runner.js +0 -84
- package/dist/services/plugin-runner.js.map +0 -1
- package/dist/services/plugin.d.ts +0 -421
- package/dist/services/plugin.d.ts.map +0 -1
- package/dist/services/plugin.js +0 -197
- package/dist/services/plugin.js.map +0 -1
- package/dist/services/resolution.d.ts +0 -38
- package/dist/services/resolution.d.ts.map +0 -1
- package/dist/services/resolution.js +0 -242
- package/dist/services/resolution.js.map +0 -1
- package/dist/services/service-registry.d.ts +0 -74
- package/dist/services/service-registry.d.ts.map +0 -1
- package/dist/services/service-registry.js +0 -61
- package/dist/services/service-registry.js.map +0 -1
- package/dist/services/symbols.d.ts +0 -144
- package/dist/services/symbols.d.ts.map +0 -1
- package/dist/services/symbols.js +0 -144
- package/dist/services/symbols.js.map +0 -1
|
@@ -1,48 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HTTP
|
|
2
|
+
* Elysia HTTP Plugin - Generates Elysia route handlers from query symbols
|
|
3
3
|
*
|
|
4
|
-
* Consumes
|
|
5
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
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 "
|
|
80
|
+
return "patch";
|
|
56
81
|
case "delete":
|
|
57
82
|
return "delete";
|
|
58
83
|
case "function":
|
|
59
84
|
return "post";
|
|
60
85
|
}
|
|
61
86
|
};
|
|
62
|
-
|
|
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 =
|
|
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 `/${
|
|
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
|
|
239
|
-
|
|
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
|
|
262
|
-
//
|
|
263
|
-
objBuilder = objBuilder.
|
|
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
|
|
274
|
-
|
|
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
|
-
//
|
|
283
|
-
const queryCall = b.callExpression(b.
|
|
284
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
197
|
+
function getBodySchemaName(method, entityName) {
|
|
300
198
|
if (method.kind === "create") {
|
|
301
|
-
return
|
|
199
|
+
return `${entityName}Insert`;
|
|
302
200
|
}
|
|
303
201
|
if (method.kind === "update") {
|
|
304
|
-
return
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|