@danielfgray/pg-sourcerer 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,937 +1,537 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQL Queries
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { getTableEntities, getEnumEntities, getFunctionEntities, getCompositeEntities, } from "../ir/semantic-ir.js";
|
|
7
|
-
import { conjure } from "../lib/conjure.js";
|
|
8
|
-
import { hex } from "../lib/hex.js";
|
|
9
|
-
import { resolveFieldType, tsTypeToAst } from "../lib/field-utils.js";
|
|
10
|
-
import { inflect } from "../services/inflection.js";
|
|
11
|
-
const { ts, b, param, asyncFn } = conjure;
|
|
12
|
-
/** Default export name: camelCase of methodName + entityName (e.g., "findUserById") */
|
|
13
|
-
const defaultExportName = (entityName, methodName) => {
|
|
14
|
-
// methodName is like "FindById", "Insert", "GetByUsername"
|
|
15
|
-
// We want: findUserById, insertUser, getUserByUsername
|
|
16
|
-
const camelMethod = methodName.charAt(0).toLowerCase() + methodName.slice(1);
|
|
17
|
-
// Insert entity name after the verb (find, insert, get, delete)
|
|
18
|
-
// Pattern: verb + Entity + rest (e.g., find + User + ById)
|
|
19
|
-
const verbMatch = camelMethod.match(/^(find|insert|delete|get)(.*)$/);
|
|
20
|
-
if (verbMatch) {
|
|
21
|
-
const [, verb, rest] = verbMatch;
|
|
22
|
-
return `${verb}${entityName}${rest}`;
|
|
23
|
-
}
|
|
24
|
-
// Fallback: just prepend entity
|
|
25
|
-
return `${camelMethod}${entityName}`;
|
|
26
|
-
};
|
|
27
|
-
const SqlQueriesPluginConfigSchema = S.Struct({
|
|
28
|
-
outputDir: S.optionalWith(S.String, { default: () => "sql-queries" }),
|
|
29
|
-
/**
|
|
30
|
-
* Header content to prepend to each generated file.
|
|
31
|
-
* Must include the SQL client import (e.g., `import { sql } from "../db"`).
|
|
32
|
-
*/
|
|
33
|
-
header: S.String,
|
|
34
|
-
/** SQL query style. Defaults to "tag" (tagged template literals) */
|
|
35
|
-
sqlStyle: S.optionalWith(S.Union(S.Literal("tag"), S.Literal("string")), {
|
|
36
|
-
default: () => "tag",
|
|
37
|
-
}),
|
|
38
|
-
/**
|
|
39
|
-
* Use explicit column lists instead of SELECT *.
|
|
40
|
-
* When true, generates "SELECT col1, col2" which excludes omitted fields at runtime.
|
|
41
|
-
* Defaults to true.
|
|
42
|
-
*/
|
|
43
|
-
explicitColumns: S.optionalWith(S.Boolean, { default: () => true }),
|
|
44
|
-
/** Generate wrappers for PostgreSQL functions. Defaults to true. */
|
|
45
|
-
generateFunctions: S.optionalWith(S.Boolean, { default: () => true }),
|
|
46
|
-
/** Output file for scalar-returning functions. Defaults to "functions.ts". */
|
|
47
|
-
functionsFile: S.optionalWith(S.String, { default: () => "functions.ts" }),
|
|
48
|
-
/** Export name function - use S.Any for schema, properly typed after resolution */
|
|
49
|
-
exportName: S.optional(S.Any),
|
|
50
|
-
/**
|
|
51
|
-
* Export style for generated query functions.
|
|
52
|
-
* - "flat": Individual exports (e.g., `export async function findById() {...}`)
|
|
53
|
-
* - "namespace": Single object export (e.g., `export const User = { findById: ... }`)
|
|
54
|
-
*/
|
|
55
|
-
exportStyle: S.optionalWith(S.Literal("flat", "namespace"), { default: () => "flat" }),
|
|
56
|
-
});
|
|
57
|
-
/** Find a field in the row shape by column name */
|
|
58
|
-
const findRowField = (entity, columnName) => entity.shapes.row.fields.find(f => f.columnName === columnName);
|
|
59
|
-
/** Build comma-separated column list from row shape fields */
|
|
60
|
-
const buildColumnList = (entity) => entity.shapes.row.fields.map(f => f.columnName).join(", ");
|
|
61
|
-
/** Build SELECT clause - explicit columns or * based on config */
|
|
62
|
-
const buildSelectClause = (entity, explicitColumns) => explicitColumns ? `select ${buildColumnList(entity)}` : "select *";
|
|
63
|
-
/** Get the TypeScript type AST for a field */
|
|
64
|
-
const getFieldTypeAst = (field, ctx) => {
|
|
65
|
-
if (!field)
|
|
66
|
-
return ts.string();
|
|
67
|
-
const resolved = resolveFieldType(field, ctx.enums, ctx.ir.extensions);
|
|
68
|
-
return resolved.enumDef ? ts.ref(resolved.enumDef.name) : tsTypeToAst(resolved.tsType);
|
|
69
|
-
};
|
|
70
|
-
// ============================================================================
|
|
71
|
-
// FK Semantic Naming Helpers
|
|
72
|
-
// ============================================================================
|
|
73
|
-
/**
|
|
74
|
-
* Find a belongsTo relation that uses the given column as its local FK column.
|
|
75
|
-
* For single-column indexes only.
|
|
76
|
-
*/
|
|
77
|
-
const findRelationForColumn = (entity, columnName) => entity.relations.find(r => r.kind === "belongsTo" && r.columns.length === 1 && r.columns[0]?.local === columnName);
|
|
78
|
-
/**
|
|
79
|
-
* Derive semantic name for an FK-based lookup.
|
|
80
|
-
* Priority: @fieldName tag → column minus _id suffix → target entity name
|
|
81
|
-
*/
|
|
82
|
-
const deriveSemanticName = (relation, columnName) => {
|
|
83
|
-
// 1. Check for @fieldName smart tag
|
|
84
|
-
if (relation.tags.fieldName && typeof relation.tags.fieldName === "string") {
|
|
85
|
-
return relation.tags.fieldName;
|
|
86
|
-
}
|
|
87
|
-
// 2. Strip common FK suffixes from column name
|
|
88
|
-
const suffixes = ["_id", "_fk", "Id", "Fk"];
|
|
89
|
-
for (const suffix of suffixes) {
|
|
90
|
-
if (columnName.endsWith(suffix)) {
|
|
91
|
-
const stripped = columnName.slice(0, -suffix.length);
|
|
92
|
-
if (stripped.length > 0)
|
|
93
|
-
return stripped;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// 3. Fall back to target entity name (lowercased first char)
|
|
97
|
-
const target = relation.targetEntity;
|
|
98
|
-
return target.charAt(0).toLowerCase() + target.slice(1);
|
|
99
|
-
};
|
|
100
|
-
/**
|
|
101
|
-
* Capitalize first letter for use in function names
|
|
102
|
-
*/
|
|
103
|
-
/**
|
|
104
|
-
* Convert to PascalCase for use in function names.
|
|
105
|
-
* Handles snake_case (created_at → CreatedAt) and regular strings.
|
|
2
|
+
* SQL Queries Plugin - Generate raw SQL query functions using template strings
|
|
3
|
+
*
|
|
4
|
+
* Generates SQL query functions with tagged template literals.
|
|
5
|
+
* Uses parameterized queries ($1, $2, etc.) for safety.
|
|
106
6
|
*/
|
|
107
|
-
|
|
7
|
+
import { Effect, Schema as S } from "effect";
|
|
8
|
+
import { normalizeFileNaming } from "../runtime/file-assignment.js";
|
|
9
|
+
import { IR } from "../services/ir.js";
|
|
10
|
+
import { Inflection } from "../services/inflection.js";
|
|
11
|
+
import { getTableEntities, getCursorPaginationCandidates, } from "../ir/semantic-ir.js";
|
|
12
|
+
import { conjure } from "../conjure/index.js";
|
|
13
|
+
const { fn, ts, param, str, b, exp } = conjure;
|
|
108
14
|
// ============================================================================
|
|
109
|
-
//
|
|
15
|
+
// Name Building Helpers
|
|
110
16
|
// ============================================================================
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
|
|
121
|
-
if (!entity.primaryKey || !entity.permissions.canSelect)
|
|
122
|
-
return undefined;
|
|
123
|
-
const pkColName = entity.primaryKey.columns[0];
|
|
124
|
-
const pkField = findRowField(entity, pkColName);
|
|
125
|
-
if (!pkField)
|
|
126
|
-
return undefined;
|
|
127
|
-
const rowType = entity.shapes.row.name;
|
|
128
|
-
const fieldName = pkField.name; // JS property name (e.g., "id")
|
|
129
|
-
const selectClause = buildSelectClause(entity, explicitColumns);
|
|
130
|
-
const parts = {
|
|
131
|
-
templateParts: [
|
|
132
|
-
`${selectClause} from ${entity.schemaName}.${entity.pgName} where ${pkColName} = `,
|
|
133
|
-
"",
|
|
134
|
-
],
|
|
135
|
-
params: [b.identifier(fieldName)],
|
|
136
|
-
};
|
|
137
|
-
// Build query and extract first row
|
|
138
|
-
const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
|
|
139
|
-
const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
|
|
140
|
-
const name = exportName(entityName, "FindById");
|
|
141
|
-
const fn = asyncFn(name, [param.pick([fieldName], rowType)], [varDecl, b.returnStatement(b.identifier("result"))]);
|
|
142
|
-
const meta = {
|
|
143
|
-
name,
|
|
144
|
-
kind: "read",
|
|
145
|
-
params: [
|
|
146
|
-
{
|
|
147
|
-
name: fieldName,
|
|
148
|
-
type: getFieldTypeString(pkField, ctx),
|
|
149
|
-
required: true,
|
|
150
|
-
columnName: pkColName,
|
|
151
|
-
source: "pk",
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
returns: { type: rowType, nullable: true, isArray: false },
|
|
155
|
-
callSignature: { style: "named" },
|
|
156
|
-
};
|
|
157
|
-
return { name, fn, meta };
|
|
158
|
-
};
|
|
159
|
-
/** Generate findMany method with pagination if entity has canSelect permission */
|
|
160
|
-
const generateFindMany = (ctx) => {
|
|
161
|
-
const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
|
|
162
|
-
if (!entity.permissions.canSelect)
|
|
163
|
-
return undefined;
|
|
164
|
-
const rowType = entity.shapes.row.name;
|
|
165
|
-
const selectClause = buildSelectClause(entity, explicitColumns);
|
|
166
|
-
const parts = {
|
|
167
|
-
templateParts: [
|
|
168
|
-
`${selectClause} from ${entity.schemaName}.${entity.pgName} limit `,
|
|
169
|
-
` offset `,
|
|
170
|
-
"",
|
|
171
|
-
],
|
|
172
|
-
params: [b.identifier("limit"), b.identifier("offset")],
|
|
173
|
-
};
|
|
174
|
-
const name = exportName(entityName, "FindManys");
|
|
175
|
-
const fn = asyncFn(name, [
|
|
176
|
-
param.destructured([
|
|
177
|
-
{ name: "limit", type: ts.number(), optional: true, defaultValue: b.numericLiteral(50) },
|
|
178
|
-
{ name: "offset", type: ts.number(), optional: true, defaultValue: b.numericLiteral(0) },
|
|
179
|
-
]),
|
|
180
|
-
], hex.returnQuery(sqlStyle, parts, ts.array(ts.ref(rowType))));
|
|
181
|
-
const meta = {
|
|
182
|
-
name,
|
|
183
|
-
kind: "list",
|
|
184
|
-
params: [
|
|
185
|
-
{ name: "limit", type: "number", required: false, source: "pagination" },
|
|
186
|
-
{ name: "offset", type: "number", required: false, source: "pagination" },
|
|
187
|
-
],
|
|
188
|
-
returns: { type: rowType, nullable: false, isArray: true },
|
|
189
|
-
callSignature: { style: "named" },
|
|
190
|
-
};
|
|
191
|
-
return { name, fn, meta };
|
|
192
|
-
};
|
|
193
|
-
/** Generate delete method if entity has a primary key and canDelete permission */
|
|
194
|
-
const generateDelete = (ctx) => {
|
|
195
|
-
const { entity, sqlStyle, entityName, exportName } = ctx;
|
|
196
|
-
if (!entity.primaryKey || !entity.permissions.canDelete)
|
|
197
|
-
return undefined;
|
|
198
|
-
const pkColName = entity.primaryKey.columns[0];
|
|
199
|
-
const pkField = findRowField(entity, pkColName);
|
|
200
|
-
if (!pkField)
|
|
201
|
-
return undefined;
|
|
202
|
-
const rowType = entity.shapes.row.name;
|
|
203
|
-
const fieldName = pkField.name;
|
|
204
|
-
const parts = {
|
|
205
|
-
templateParts: [`delete from ${entity.schemaName}.${entity.pgName} where ${pkColName} = `, ""],
|
|
206
|
-
params: [b.identifier(fieldName)],
|
|
207
|
-
};
|
|
208
|
-
// Delete returns void, no type parameter needed
|
|
209
|
-
const queryExpr = hex.query(sqlStyle, parts);
|
|
210
|
-
const name = exportName(entityName, "Delete");
|
|
211
|
-
const fn = asyncFn(name, [param.pick([fieldName], rowType)], [b.expressionStatement(queryExpr)]);
|
|
212
|
-
const meta = {
|
|
213
|
-
name,
|
|
214
|
-
kind: "delete",
|
|
215
|
-
params: [
|
|
216
|
-
{
|
|
217
|
-
name: fieldName,
|
|
218
|
-
type: getFieldTypeString(pkField, ctx),
|
|
219
|
-
required: true,
|
|
220
|
-
columnName: pkColName,
|
|
221
|
-
source: "pk",
|
|
222
|
-
},
|
|
223
|
-
],
|
|
224
|
-
returns: { type: "void", nullable: false, isArray: false },
|
|
225
|
-
callSignature: { style: "named" },
|
|
226
|
-
};
|
|
227
|
-
return { name, fn, meta };
|
|
228
|
-
};
|
|
229
|
-
/** Generate insert method if entity has canInsert permission */
|
|
230
|
-
const generateInsert = (ctx) => {
|
|
231
|
-
const { entity, sqlStyle, entityName, exportName } = ctx;
|
|
232
|
-
if (!entity.permissions.canInsert)
|
|
233
|
-
return undefined;
|
|
234
|
-
// Use insert shape if available, otherwise fall back to row
|
|
235
|
-
const insertShape = entity.shapes.insert ?? entity.shapes.row;
|
|
236
|
-
const rowType = entity.shapes.row.name;
|
|
237
|
-
const insertType = insertShape.name;
|
|
238
|
-
// Build column list and values from insertable fields
|
|
239
|
-
const insertableFields = insertShape.fields.filter(f => f.permissions.canInsert);
|
|
240
|
-
if (insertableFields.length === 0)
|
|
241
|
-
return undefined;
|
|
242
|
-
const columnNames = insertableFields.map(f => f.columnName);
|
|
243
|
-
// Build: insert into schema.table (col1, col2) values ($field1, $field2) returning *
|
|
244
|
-
const columnList = columnNames.join(", ");
|
|
245
|
-
const valuePlaceholders = insertableFields.map((_, i) => (i === 0 ? "" : ", "));
|
|
246
|
-
// For optional fields (nullable or has default), use DEFAULT when undefined
|
|
247
|
-
// Required fields use the value directly
|
|
248
|
-
const paramExprs = insertableFields.map(f => {
|
|
249
|
-
const isOptional = f.optional || f.nullable;
|
|
250
|
-
return isOptional ? hex.defaultIfUndefined(f.name) : b.identifier(f.name);
|
|
251
|
-
});
|
|
252
|
-
// Template parts: "insert into ... values (" + "" + ", " + ", " + ... + ") returning *"
|
|
253
|
-
const parts = {
|
|
254
|
-
templateParts: [
|
|
255
|
-
`insert into ${entity.schemaName}.${entity.pgName} (${columnList}) values (`,
|
|
256
|
-
...valuePlaceholders.slice(1),
|
|
257
|
-
`) returning *`,
|
|
258
|
-
],
|
|
259
|
-
params: paramExprs,
|
|
260
|
-
};
|
|
261
|
-
const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
|
|
262
|
-
const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
|
|
263
|
-
// Destructured parameter - use Pick from insert type
|
|
264
|
-
const fieldNames = insertableFields.map(f => f.name);
|
|
265
|
-
const dataParam = param.pick(fieldNames, insertType);
|
|
266
|
-
const name = exportName(entityName, "Insert");
|
|
267
|
-
const fn = asyncFn(name, [dataParam], [varDecl, b.returnStatement(b.identifier("result"))]);
|
|
268
|
-
const meta = {
|
|
269
|
-
name,
|
|
270
|
-
kind: "create",
|
|
271
|
-
params: [
|
|
272
|
-
{
|
|
273
|
-
name: "data",
|
|
274
|
-
type: insertType,
|
|
275
|
-
required: true,
|
|
276
|
-
source: "body",
|
|
277
|
-
},
|
|
278
|
-
],
|
|
279
|
-
returns: { type: rowType, nullable: false, isArray: false },
|
|
280
|
-
callSignature: { style: "named", bodyStyle: "spread" },
|
|
281
|
-
};
|
|
282
|
-
return { name, fn, meta };
|
|
283
|
-
};
|
|
284
|
-
/** Generate all CRUD methods for an entity */
|
|
285
|
-
const generateCrudMethods = (ctx) => [generateFindById(ctx), generateFindMany(ctx), generateInsert(ctx), generateDelete(ctx)].filter((m) => m != null);
|
|
17
|
+
function buildQueryName(inflection, entityName, operation) {
|
|
18
|
+
return inflection.variableName(entityName, operation);
|
|
19
|
+
}
|
|
20
|
+
function buildFindByName(inflection, entityName, columnName) {
|
|
21
|
+
return inflection.variableName(entityName, `FindBy${inflection.pascalCase(columnName)}`);
|
|
22
|
+
}
|
|
23
|
+
function buildListByName(inflection, entityName, columnName) {
|
|
24
|
+
return inflection.variableName(entityName, `ListBy${inflection.pascalCase(columnName)}`);
|
|
25
|
+
}
|
|
286
26
|
// ============================================================================
|
|
287
|
-
//
|
|
27
|
+
// Configuration
|
|
288
28
|
// ============================================================================
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const generateLookupMethodName = (index, relation, columnName) => {
|
|
300
|
-
const isUnique = index.isUnique || index.isPrimary;
|
|
301
|
-
const prefix = isUnique ? "GetBy" : "GetsBy";
|
|
302
|
-
// Use semantic name if FK relation exists, otherwise fall back to column name
|
|
303
|
-
const byName = relation ? deriveSemanticName(relation, columnName) : index.columns[0];
|
|
304
|
-
return `${prefix}${toPascalCase(byName)}`;
|
|
305
|
-
};
|
|
306
|
-
/**
|
|
307
|
-
* Generate a lookup method for a single-column index.
|
|
308
|
-
* Uses semantic parameter naming when the column corresponds to an FK relation.
|
|
309
|
-
*/
|
|
310
|
-
const generateLookupMethod = (index, ctx) => {
|
|
311
|
-
const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
|
|
312
|
-
const rowType = entity.shapes.row.name;
|
|
313
|
-
const columnName = index.columnNames[0];
|
|
314
|
-
const field = findRowField(entity, columnName);
|
|
315
|
-
const fieldName = field?.name ?? index.columns[0];
|
|
316
|
-
const isUnique = index.isUnique || index.isPrimary;
|
|
317
|
-
// Check if this index column corresponds to an FK relation
|
|
318
|
-
const relation = findRelationForColumn(entity, columnName);
|
|
319
|
-
// Use semantic param name if FK relation exists, otherwise use field name
|
|
320
|
-
const paramName = relation ? deriveSemanticName(relation, columnName) : fieldName;
|
|
321
|
-
// For semantic naming, use indexed access type (Post["userId"])
|
|
322
|
-
// For regular naming, use Pick<Post, "fieldName">
|
|
323
|
-
const useSemanticNaming = relation !== undefined && paramName !== fieldName;
|
|
324
|
-
const selectClause = buildSelectClause(entity, explicitColumns);
|
|
325
|
-
const parts = {
|
|
326
|
-
templateParts: [
|
|
327
|
-
`${selectClause} from ${entity.schemaName}.${entity.pgName} where ${columnName} = `,
|
|
328
|
-
"",
|
|
329
|
-
],
|
|
330
|
-
params: [b.identifier(paramName)],
|
|
331
|
-
};
|
|
332
|
-
const methodName = generateLookupMethodName(index, relation, columnName);
|
|
333
|
-
const name = exportName(entityName, methodName);
|
|
334
|
-
// Build the parameter - use destructured style for both cases
|
|
335
|
-
// Lookup params must be non-nullable (you're searching FOR a value, not handling null)
|
|
336
|
-
// Semantic naming: { user }: { user: NonNullable<Post["user_id"]> }
|
|
337
|
-
// Regular naming: { fieldName }: { fieldName: NonNullable<Post["fieldName"]> }
|
|
338
|
-
const indexedType = ts.indexedAccess(ts.ref(rowType), ts.literal(fieldName));
|
|
339
|
-
const paramType = ts.ref("NonNullable", [indexedType]);
|
|
340
|
-
const paramNode = param.destructured([{ name: paramName, type: paramType }]);
|
|
341
|
-
// Build metadata for the lookup method
|
|
342
|
-
const meta = {
|
|
343
|
-
name,
|
|
344
|
-
kind: "lookup",
|
|
345
|
-
params: [
|
|
346
|
-
{
|
|
347
|
-
name: paramName,
|
|
348
|
-
type: getFieldTypeString(field, ctx),
|
|
349
|
-
required: true,
|
|
350
|
-
columnName,
|
|
351
|
-
source: relation ? "fk" : "lookup",
|
|
352
|
-
},
|
|
353
|
-
],
|
|
354
|
-
returns: {
|
|
355
|
-
type: rowType,
|
|
356
|
-
nullable: isUnique,
|
|
357
|
-
isArray: !isUnique,
|
|
358
|
-
},
|
|
359
|
-
lookupField: fieldName,
|
|
360
|
-
isUniqueLookup: isUnique,
|
|
361
|
-
callSignature: { style: "named" },
|
|
362
|
-
};
|
|
363
|
-
if (isUnique) {
|
|
364
|
-
// Extract first row for unique lookups
|
|
365
|
-
const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
|
|
366
|
-
const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
|
|
367
|
-
const fn = asyncFn(name, [paramNode], [varDecl, b.returnStatement(b.identifier("result"))]);
|
|
368
|
-
return { name, fn, meta };
|
|
369
|
-
}
|
|
370
|
-
// Non-unique: return all matching rows
|
|
371
|
-
const fn = asyncFn(name, [paramNode], hex.returnQuery(sqlStyle, parts, ts.array(ts.ref(rowType))));
|
|
372
|
-
return { name, fn, meta };
|
|
373
|
-
};
|
|
374
|
-
/** Generate lookup methods for all eligible indexes, deduplicating by name */
|
|
375
|
-
const generateLookupMethods = (ctx) => {
|
|
376
|
-
const seen = new Set();
|
|
377
|
-
return ctx.entity.indexes
|
|
378
|
-
.filter(index => shouldGenerateLookup(index) && !index.isPrimary)
|
|
379
|
-
.filter(index => {
|
|
380
|
-
const columnName = index.columnNames[0];
|
|
381
|
-
const relation = findRelationForColumn(ctx.entity, columnName);
|
|
382
|
-
const methodName = generateLookupMethodName(index, relation, columnName);
|
|
383
|
-
const name = ctx.exportName(ctx.entityName, methodName);
|
|
384
|
-
if (seen.has(name))
|
|
385
|
-
return false;
|
|
386
|
-
seen.add(name);
|
|
387
|
-
return true;
|
|
388
|
-
})
|
|
389
|
-
.map(index => generateLookupMethod(index, ctx));
|
|
390
|
-
};
|
|
29
|
+
const SqlQueriesConfigSchema = S.Struct({
|
|
30
|
+
/** Generate query functions (default: true) */
|
|
31
|
+
generateQueries: S.optionalWith(S.Boolean, { default: () => true }),
|
|
32
|
+
/** SQL query style - always "tag" for template literals */
|
|
33
|
+
sqlStyle: S.optionalWith(S.Literal("tag"), { default: () => "tag" }),
|
|
34
|
+
/** Use explicit column lists instead of SELECT * (default: true) */
|
|
35
|
+
explicitColumns: S.optionalWith(S.Boolean, { default: () => true }),
|
|
36
|
+
/** Default limit for list queries (default: 50) */
|
|
37
|
+
defaultLimit: S.optionalWith(S.Number, { default: () => 50 }),
|
|
38
|
+
});
|
|
391
39
|
// ============================================================================
|
|
392
|
-
//
|
|
40
|
+
// Query Generation Helpers
|
|
393
41
|
// ============================================================================
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
json: "unknown",
|
|
427
|
-
jsonb: "unknown",
|
|
428
|
-
// Binary
|
|
429
|
-
bytea: "Buffer",
|
|
430
|
-
// Other
|
|
431
|
-
void: "void",
|
|
432
|
-
};
|
|
433
|
-
return typeMap[typeName] ?? "unknown";
|
|
434
|
-
};
|
|
435
|
-
/**
|
|
436
|
-
* Check if a function argument has a row type (composite type matching a table).
|
|
437
|
-
* Functions with row-type arguments are computed fields, not standalone functions.
|
|
438
|
-
*/
|
|
439
|
-
const hasRowTypeArg = (arg, ir) => {
|
|
440
|
-
const tables = getTableEntities(ir);
|
|
441
|
-
return tables.some(t => {
|
|
442
|
-
const qualifiedName = `${t.schemaName}.${t.pgName}`;
|
|
443
|
-
return arg.typeName === qualifiedName || arg.typeName === t.pgName;
|
|
444
|
-
});
|
|
445
|
-
};
|
|
446
|
-
/**
|
|
447
|
-
* Check if a function can be wrapped (not a trigger, computed field, etc.)
|
|
448
|
-
*/
|
|
449
|
-
const isGeneratableFunction = (fn, ir) => {
|
|
450
|
-
if (!fn.canExecute)
|
|
451
|
-
return false;
|
|
452
|
-
if (fn.returnTypeName === "trigger")
|
|
453
|
-
return false;
|
|
454
|
-
if (fn.isFromExtension)
|
|
455
|
-
return false;
|
|
456
|
-
if (fn.tags.omit === true)
|
|
457
|
-
return false;
|
|
458
|
-
// Filter out computed field functions (have row-type args)
|
|
459
|
-
if (fn.args.some(arg => hasRowTypeArg(arg, ir)))
|
|
460
|
-
return false;
|
|
461
|
-
return true;
|
|
462
|
-
};
|
|
463
|
-
/**
|
|
464
|
-
* Categorize functions by volatility.
|
|
465
|
-
* Volatile functions go in mutations namespace, stable/immutable in queries.
|
|
466
|
-
*/
|
|
467
|
-
const categorizeFunction = (fn) => fn.volatility === "volatile" ? "mutations" : "queries";
|
|
468
|
-
/**
|
|
469
|
-
* Get all generatable functions from the IR, categorized by volatility.
|
|
470
|
-
*/
|
|
471
|
-
const getGeneratableFunctions = (ir) => {
|
|
472
|
-
const all = getFunctionEntities(ir).filter(fn => isGeneratableFunction(fn, ir));
|
|
42
|
+
function buildColumnList(fields) {
|
|
43
|
+
return fields.map(f => f.columnName).join(", ");
|
|
44
|
+
}
|
|
45
|
+
function buildSelectClause(entity, explicitColumns) {
|
|
46
|
+
return explicitColumns
|
|
47
|
+
? `select ${buildColumnList(entity.shapes.row.fields)}`
|
|
48
|
+
: "select *";
|
|
49
|
+
}
|
|
50
|
+
function buildTableName(entity, defaultSchemas) {
|
|
51
|
+
return defaultSchemas.includes(entity.schemaName)
|
|
52
|
+
? entity.pgName
|
|
53
|
+
: `${entity.schemaName}.${entity.pgName}`;
|
|
54
|
+
}
|
|
55
|
+
function getPgType(field) {
|
|
56
|
+
const pgType = field.pgAttribute.getType();
|
|
57
|
+
return pgType?.typname ?? "unknown";
|
|
58
|
+
}
|
|
59
|
+
function pgTypeToTsType(pgType) {
|
|
60
|
+
const lower = pgType.toLowerCase();
|
|
61
|
+
if (["uuid", "text", "varchar", "char", "citext", "name"].includes(lower))
|
|
62
|
+
return "string";
|
|
63
|
+
if (["int2", "int4", "int8", "integer", "smallint", "bigint", "numeric", "decimal", "real", "float4", "float8"].includes(lower))
|
|
64
|
+
return "number";
|
|
65
|
+
if (["bool", "boolean"].includes(lower))
|
|
66
|
+
return "boolean";
|
|
67
|
+
if (["timestamp", "timestamptz", "date"].includes(lower))
|
|
68
|
+
return "Date";
|
|
69
|
+
if (["json", "jsonb"].includes(lower))
|
|
70
|
+
return "unknown";
|
|
71
|
+
return "string";
|
|
72
|
+
}
|
|
73
|
+
function buildPkParam(field) {
|
|
473
74
|
return {
|
|
474
|
-
|
|
475
|
-
|
|
75
|
+
name: field.name,
|
|
76
|
+
type: pgTypeToTsType(getPgType(field)),
|
|
77
|
+
required: true,
|
|
78
|
+
columnName: field.columnName,
|
|
79
|
+
source: "pk",
|
|
476
80
|
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
* Resolve a function's return type to TypeScript type information.
|
|
480
|
-
*/
|
|
481
|
-
const resolveReturnType = (fn, ir) => {
|
|
482
|
-
const returnTypeName = fn.returnTypeName;
|
|
483
|
-
const isArray = fn.returnsSet;
|
|
484
|
-
// 1. Check if it's a table return type
|
|
485
|
-
const tableEntities = getTableEntities(ir);
|
|
486
|
-
const tableMatch = tableEntities.find(entity => {
|
|
487
|
-
const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
|
|
488
|
-
return returnTypeName === qualifiedName || returnTypeName === entity.pgName;
|
|
489
|
-
});
|
|
490
|
-
if (tableMatch) {
|
|
491
|
-
return {
|
|
492
|
-
tsType: tableMatch.name,
|
|
493
|
-
isArray,
|
|
494
|
-
isScalar: false,
|
|
495
|
-
needsImport: tableMatch.name,
|
|
496
|
-
returnEntity: tableMatch,
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
// 2. Check if it's a composite type return
|
|
500
|
-
const compositeEntities = getCompositeEntities(ir);
|
|
501
|
-
const compositeMatch = compositeEntities.find(entity => {
|
|
502
|
-
const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
|
|
503
|
-
return returnTypeName === qualifiedName || returnTypeName === entity.pgName;
|
|
504
|
-
});
|
|
505
|
-
if (compositeMatch) {
|
|
506
|
-
return {
|
|
507
|
-
tsType: compositeMatch.name,
|
|
508
|
-
isArray,
|
|
509
|
-
isScalar: false,
|
|
510
|
-
needsImport: compositeMatch.name,
|
|
511
|
-
returnEntity: compositeMatch,
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
// 3. It's a scalar type - map via type name
|
|
515
|
-
const baseTypeName = returnTypeName.includes(".")
|
|
516
|
-
? returnTypeName.split(".").pop()
|
|
517
|
-
: returnTypeName;
|
|
518
|
-
const tsType = pgTypeNameToTs(baseTypeName);
|
|
81
|
+
}
|
|
82
|
+
function buildLookupParam(field) {
|
|
519
83
|
return {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
84
|
+
name: field.name,
|
|
85
|
+
type: pgTypeToTsType(getPgType(field)),
|
|
86
|
+
required: true,
|
|
87
|
+
columnName: field.columnName,
|
|
88
|
+
source: "lookup",
|
|
523
89
|
};
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
*/
|
|
528
|
-
const resolveArg = (arg, ir) => {
|
|
529
|
-
const typeName = arg.typeName;
|
|
530
|
-
// Check if it's an array type (ends with [])
|
|
531
|
-
const isArrayType = typeName.endsWith("[]");
|
|
532
|
-
const baseTypeName = isArrayType ? typeName.slice(0, -2) : typeName;
|
|
533
|
-
// Check enums
|
|
534
|
-
const enums = getEnumEntities(ir);
|
|
535
|
-
const enumMatch = enums.find(e => {
|
|
536
|
-
const qualifiedName = `${e.schemaName}.${e.pgName}`;
|
|
537
|
-
return baseTypeName === qualifiedName || baseTypeName === e.pgName;
|
|
538
|
-
});
|
|
539
|
-
if (enumMatch) {
|
|
540
|
-
const tsType = isArrayType ? `${enumMatch.name}[]` : enumMatch.name;
|
|
541
|
-
return {
|
|
542
|
-
name: arg.name || "arg",
|
|
543
|
-
tsType,
|
|
544
|
-
isOptional: arg.hasDefault,
|
|
545
|
-
needsImport: enumMatch.name,
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
// Check composites
|
|
549
|
-
const composites = getCompositeEntities(ir);
|
|
550
|
-
const compositeMatch = composites.find(e => {
|
|
551
|
-
const qualifiedName = `${e.schemaName}.${e.pgName}`;
|
|
552
|
-
return baseTypeName === qualifiedName || baseTypeName === e.pgName;
|
|
553
|
-
});
|
|
554
|
-
if (compositeMatch) {
|
|
555
|
-
const tsType = isArrayType ? `${compositeMatch.name}[]` : compositeMatch.name;
|
|
556
|
-
return {
|
|
557
|
-
name: arg.name || "arg",
|
|
558
|
-
tsType,
|
|
559
|
-
isOptional: arg.hasDefault,
|
|
560
|
-
needsImport: compositeMatch.name,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
// Scalar type - map via type name
|
|
564
|
-
const scalarBase = baseTypeName.includes(".") ? baseTypeName.split(".").pop() : baseTypeName;
|
|
565
|
-
const scalarTs = pgTypeNameToTs(scalarBase);
|
|
566
|
-
const tsType = isArrayType ? `${scalarTs}[]` : scalarTs;
|
|
90
|
+
}
|
|
91
|
+
function buildBodyParam(entityName, shape) {
|
|
92
|
+
const wrapper = shape === "insert" ? "Insertable" : "Updateable";
|
|
567
93
|
return {
|
|
568
|
-
name:
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
* Resolve all arguments for a function.
|
|
575
|
-
*/
|
|
576
|
-
const resolveArgs = (fn, ir) => fn.args.map(arg => resolveArg(arg, ir));
|
|
577
|
-
/**
|
|
578
|
-
* Get the fully qualified function name for SQL.
|
|
579
|
-
*/
|
|
580
|
-
const getFunctionQualifiedName = (fn) => `${fn.schemaName}.${fn.pgName}`;
|
|
581
|
-
/**
|
|
582
|
-
* Generate a function wrapper for a PostgreSQL function.
|
|
583
|
-
*
|
|
584
|
-
* Patterns:
|
|
585
|
-
* - SETOF/table return: select * from schema.fn(args)
|
|
586
|
-
* - Single row return: select * from schema.fn(args) (same SQL, single result)
|
|
587
|
-
* - Scalar return: select schema.fn(args)
|
|
588
|
-
*/
|
|
589
|
-
const generateFunctionWrapper = (fn, ir, sqlStyle) => {
|
|
590
|
-
const resolvedReturn = resolveReturnType(fn, ir);
|
|
591
|
-
const resolvedArgs = resolveArgs(fn, ir);
|
|
592
|
-
const qualifiedName = getFunctionQualifiedName(fn);
|
|
593
|
-
// Use fn.name which is already inflected by the IR builder
|
|
594
|
-
const name = fn.name;
|
|
595
|
-
// Helper to convert resolved type string to AST
|
|
596
|
-
const typeStrToAst = (typeStr) => {
|
|
597
|
-
if (typeStr.endsWith("[]")) {
|
|
598
|
-
const elemType = typeStr.slice(0, -2);
|
|
599
|
-
return ts.array(typeStrToAst(elemType));
|
|
600
|
-
}
|
|
601
|
-
switch (typeStr) {
|
|
602
|
-
case "string":
|
|
603
|
-
return ts.string();
|
|
604
|
-
case "number":
|
|
605
|
-
return ts.number();
|
|
606
|
-
case "boolean":
|
|
607
|
-
return ts.boolean();
|
|
608
|
-
case "void":
|
|
609
|
-
return ts.void();
|
|
610
|
-
case "unknown":
|
|
611
|
-
return ts.unknown();
|
|
612
|
-
case "Date":
|
|
613
|
-
return ts.ref("Date");
|
|
614
|
-
case "Buffer":
|
|
615
|
-
return ts.ref("Buffer");
|
|
616
|
-
default:
|
|
617
|
-
return ts.ref(typeStr);
|
|
618
|
-
}
|
|
94
|
+
name: "data",
|
|
95
|
+
type: `${wrapper}<${entityName}>`,
|
|
96
|
+
wrapper,
|
|
97
|
+
entityType: entityName,
|
|
98
|
+
required: true,
|
|
99
|
+
source: "body",
|
|
619
100
|
};
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
:
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
let resultType;
|
|
633
|
-
if (resolvedReturn.isScalar) {
|
|
634
|
-
// Scalar: select schema.fn(args)
|
|
635
|
-
const argPlaceholders = resolvedArgs.map((_, i) => `$${i + 1}`).join(", ");
|
|
636
|
-
sql = `select ${qualifiedName}(${argPlaceholders})`;
|
|
637
|
-
// Return type is a record with the function name as key
|
|
638
|
-
resultType = ts.array(ts.ref("Record", [ts.string(), typeStrToAst(resolvedReturn.tsType)]));
|
|
639
|
-
}
|
|
640
|
-
else {
|
|
641
|
-
// Table/composite: select * from schema.fn(args)
|
|
642
|
-
const argPlaceholders = resolvedArgs.map((_, i) => `$${i + 1}`).join(", ");
|
|
643
|
-
sql = `select * from ${qualifiedName}(${argPlaceholders})`;
|
|
644
|
-
resultType = ts.array(ts.ref(resolvedReturn.tsType));
|
|
645
|
-
}
|
|
646
|
-
const paramExprs = resolvedArgs.map(arg => b.identifier(arg.name));
|
|
647
|
-
// Build template parts by splitting on $N placeholders
|
|
648
|
-
let templateParts = sql.split(/\$\d+/);
|
|
649
|
-
// For zero-arg functions, template is just the SQL string
|
|
650
|
-
if (resolvedArgs.length === 0) {
|
|
651
|
-
templateParts = [sql];
|
|
652
|
-
}
|
|
653
|
-
const parts = {
|
|
654
|
-
templateParts,
|
|
655
|
-
params: paramExprs,
|
|
656
|
-
};
|
|
657
|
-
// Build the function body
|
|
658
|
-
let body;
|
|
659
|
-
if (resolvedReturn.isScalar) {
|
|
660
|
-
// Scalar: extract the result from the first row's first column
|
|
661
|
-
const queryExpr = hex.query(sqlStyle, parts, resultType);
|
|
662
|
-
const varDecl = hex.firstRowDecl(sqlStyle, "row", queryExpr);
|
|
663
|
-
// Use optional chaining: row?.[fn.pgName]
|
|
664
|
-
const optionalReturn = b.optionalMemberExpression(b.identifier("row"), b.identifier(fn.pgName), false, true);
|
|
665
|
-
body = [varDecl, b.returnStatement(optionalReturn)];
|
|
666
|
-
}
|
|
667
|
-
else if (resolvedReturn.isArray) {
|
|
668
|
-
// SETOF: return all rows
|
|
669
|
-
body = hex.returnQuery(sqlStyle, parts, resultType);
|
|
670
|
-
}
|
|
671
|
-
else {
|
|
672
|
-
// Single row: extract first row
|
|
673
|
-
const queryExpr = hex.query(sqlStyle, parts, resultType);
|
|
674
|
-
const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
|
|
675
|
-
body = [varDecl, b.returnStatement(b.identifier("result"))];
|
|
676
|
-
}
|
|
677
|
-
const fnDecl = asyncFn(name, params, body);
|
|
678
|
-
// Build metadata for the function wrapper
|
|
679
|
-
const meta = {
|
|
680
|
-
name,
|
|
681
|
-
kind: "function",
|
|
682
|
-
params: resolvedArgs.map(arg => ({
|
|
683
|
-
name: arg.name,
|
|
684
|
-
type: arg.tsType,
|
|
685
|
-
required: !arg.isOptional,
|
|
686
|
-
})),
|
|
687
|
-
returns: {
|
|
688
|
-
type: resolvedReturn.tsType,
|
|
689
|
-
nullable: resolvedReturn.isScalar || !resolvedReturn.isArray, // Scalars and single rows can be null
|
|
690
|
-
isArray: resolvedReturn.isArray,
|
|
691
|
-
},
|
|
692
|
-
callSignature: { style: "named" },
|
|
101
|
+
}
|
|
102
|
+
function buildPaginationParams(defaultLimit) {
|
|
103
|
+
return [
|
|
104
|
+
{ name: "limit", type: "number", required: false, defaultValue: defaultLimit, source: "pagination" },
|
|
105
|
+
{ name: "offset", type: "number", required: false, defaultValue: 0, source: "pagination" },
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
function buildReturnType(entityName, isArray, nullable) {
|
|
109
|
+
return {
|
|
110
|
+
type: entityName,
|
|
111
|
+
nullable,
|
|
112
|
+
isArray,
|
|
693
113
|
};
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
imports.add(resolvedReturn.needsImport);
|
|
705
|
-
}
|
|
706
|
-
for (const arg of resolveArgs(fn, ir)) {
|
|
707
|
-
if (arg.needsImport) {
|
|
708
|
-
imports.add(arg.needsImport);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
114
|
+
}
|
|
115
|
+
function isBodyParam(p) {
|
|
116
|
+
return "wrapper" in p;
|
|
117
|
+
}
|
|
118
|
+
function isPaginationParam(p) {
|
|
119
|
+
return "defaultValue" in p;
|
|
120
|
+
}
|
|
121
|
+
function buildParamType(p) {
|
|
122
|
+
if (isBodyParam(p)) {
|
|
123
|
+
return ts.ref(p.wrapper, [ts.ref(p.entityType)]);
|
|
711
124
|
}
|
|
712
|
-
return
|
|
713
|
-
}
|
|
125
|
+
return ts.ref(p.type);
|
|
126
|
+
}
|
|
127
|
+
function buildDestructuredParam(params) {
|
|
128
|
+
return param.destructured(params.map(p => ({
|
|
129
|
+
name: p.name,
|
|
130
|
+
type: buildParamType(p),
|
|
131
|
+
optional: "required" in p ? p.required === false : false,
|
|
132
|
+
defaultValue: isPaginationParam(p) ? conjure.num(p.defaultValue) : undefined,
|
|
133
|
+
})));
|
|
134
|
+
}
|
|
714
135
|
// ============================================================================
|
|
715
|
-
//
|
|
136
|
+
// Plugin Definition
|
|
716
137
|
// ============================================================================
|
|
717
138
|
/**
|
|
718
|
-
*
|
|
719
|
-
* Each method becomes: export function methodName(...) { ... }
|
|
720
|
-
*/
|
|
721
|
-
const toFlatExports = (methods) => methods.map(m => conjure.export.fn(m.fn));
|
|
722
|
-
/**
|
|
723
|
-
* Convert a FunctionDeclaration to a FunctionExpression for object property use.
|
|
724
|
-
*/
|
|
725
|
-
const fnDeclToExpr = (fn) => {
|
|
726
|
-
const expr = b.functionExpression(null, fn.params, fn.body);
|
|
727
|
-
expr.async = fn.async;
|
|
728
|
-
expr.generator = fn.generator;
|
|
729
|
-
return expr;
|
|
730
|
-
};
|
|
731
|
-
/**
|
|
732
|
-
* Convert MethodDef array to a single namespace object export.
|
|
733
|
-
* All methods become: export const EntityName = { methodName: async function(...) { ... }, ... }
|
|
734
|
-
*/
|
|
735
|
-
const toNamespaceExport = (entityName, methods) => {
|
|
736
|
-
const properties = methods.map(m => b.objectProperty(b.identifier(m.name), fnDeclToExpr(m.fn)));
|
|
737
|
-
const obj = b.objectExpression(properties);
|
|
738
|
-
return conjure.export.const(entityName, obj);
|
|
739
|
-
};
|
|
740
|
-
/**
|
|
741
|
-
* Convert MethodDef array to statements based on export style.
|
|
742
|
-
*/
|
|
743
|
-
const toStatements = (methods, exportStyle, entityName) => {
|
|
744
|
-
if (methods.length === 0)
|
|
745
|
-
return [];
|
|
746
|
-
return exportStyle === "namespace"
|
|
747
|
-
? [toNamespaceExport(entityName, methods)]
|
|
748
|
-
: toFlatExports(methods);
|
|
749
|
-
};
|
|
750
|
-
/**
|
|
751
|
-
* Create a SQL queries provider that generates raw SQL query functions.
|
|
139
|
+
* SQL Queries plugin - generates raw SQL query functions with tagged templates.
|
|
752
140
|
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
* import { sqlQueries } from "pg-sourcerer"
|
|
756
|
-
*
|
|
757
|
-
* export default defineConfig({
|
|
758
|
-
* plugins: [
|
|
759
|
-
* types(),
|
|
760
|
-
* sqlQueries({ header: 'import { sql } from "../db"' }),
|
|
761
|
-
* ],
|
|
762
|
-
* })
|
|
763
|
-
* ```
|
|
141
|
+
* Capabilities provided:
|
|
142
|
+
* - `queries:sql:EntityName:operation` - CRUD query functions
|
|
764
143
|
*/
|
|
765
144
|
export function sqlQueries(config) {
|
|
766
|
-
const
|
|
767
|
-
|
|
145
|
+
const schemaConfig = S.decodeSync(SqlQueriesConfigSchema)(config ?? {});
|
|
146
|
+
const queriesFile = normalizeFileNaming(config?.queriesFile, "queries.ts");
|
|
768
147
|
const resolvedConfig = {
|
|
769
|
-
...
|
|
770
|
-
|
|
148
|
+
...schemaConfig,
|
|
149
|
+
queriesFile,
|
|
771
150
|
};
|
|
772
|
-
|
|
151
|
+
const queriesFilePath = typeof queriesFile === "string" ? queriesFile : "queries.ts";
|
|
152
|
+
return {
|
|
773
153
|
name: "sql-queries",
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
for (const
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
154
|
+
provides: ["queries"],
|
|
155
|
+
consumes: [],
|
|
156
|
+
fileDefaults: [
|
|
157
|
+
{
|
|
158
|
+
pattern: "queries:sql:",
|
|
159
|
+
fileNaming: resolvedConfig.queriesFile,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
declare: Effect.gen(function* () {
|
|
163
|
+
const ir = yield* IR;
|
|
164
|
+
const inflection = yield* Inflection;
|
|
165
|
+
const declarations = [];
|
|
166
|
+
const tableEntities = getTableEntities(ir).filter(e => e.tags.omit !== true);
|
|
167
|
+
if (resolvedConfig.generateQueries) {
|
|
168
|
+
for (const entity of tableEntities) {
|
|
169
|
+
const entityName = entity.name;
|
|
170
|
+
let hasAnyMethods = false;
|
|
171
|
+
if (entity.permissions.canSelect && entity.primaryKey && entity.primaryKey.columns.length > 0) {
|
|
172
|
+
hasAnyMethods = true;
|
|
173
|
+
declarations.push({
|
|
174
|
+
name: buildQueryName(inflection, entityName, "FindById"),
|
|
175
|
+
capability: `queries:sql:${entityName}:findById`,
|
|
176
|
+
});
|
|
794
177
|
}
|
|
795
|
-
|
|
796
|
-
|
|
178
|
+
if (entity.kind === "table" && entity.permissions.canInsert && entity.shapes.insert) {
|
|
179
|
+
hasAnyMethods = true;
|
|
180
|
+
declarations.push({
|
|
181
|
+
name: buildQueryName(inflection, entityName, "Create"),
|
|
182
|
+
capability: `queries:sql:${entityName}:create`,
|
|
183
|
+
});
|
|
797
184
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
ir,
|
|
808
|
-
sqlStyle,
|
|
809
|
-
entityName,
|
|
810
|
-
exportName,
|
|
811
|
-
explicitColumns,
|
|
812
|
-
};
|
|
813
|
-
// Generate CRUD and lookup methods
|
|
814
|
-
const crudMethods = [...generateCrudMethods(genCtx), ...generateLookupMethods(genCtx)];
|
|
815
|
-
// Get functions that return this entity
|
|
816
|
-
const entityFunctions = functionsByEntity.get(entity.name) ?? [];
|
|
817
|
-
if (crudMethods.length === 0 && entityFunctions.length === 0)
|
|
818
|
-
return;
|
|
819
|
-
const filePath = `${outputDir}/${entityName}.ts`;
|
|
820
|
-
// Convert methods to statements based on export style
|
|
821
|
-
const statements = toStatements(crudMethods, exportStyle, entityName);
|
|
822
|
-
// Add function wrappers (these are always flat exports for now)
|
|
823
|
-
for (const fn of entityFunctions) {
|
|
824
|
-
const wrapper = generateFunctionWrapper(fn, ir, sqlStyle);
|
|
825
|
-
statements.push(conjure.export.fn(wrapper.fn));
|
|
826
|
-
}
|
|
827
|
-
const file = ctx.file(filePath);
|
|
828
|
-
// Add user-provided header (must include SQL client import)
|
|
829
|
-
file.header(header);
|
|
830
|
-
file.import({
|
|
831
|
-
kind: "symbol",
|
|
832
|
-
ref: { capability: "types", entity: entity.name, shape: "row" },
|
|
833
|
-
});
|
|
834
|
-
// Import insert type if insert function is generated
|
|
835
|
-
if (entity.permissions.canInsert) {
|
|
836
|
-
const insertShape = entity.shapes.insert ?? entity.shapes.row;
|
|
837
|
-
// Only import if it's a different type than row
|
|
838
|
-
if (insertShape !== entity.shapes.row) {
|
|
839
|
-
file.import({
|
|
840
|
-
kind: "symbol",
|
|
841
|
-
ref: { capability: "types", entity: entity.name, shape: "insert" },
|
|
185
|
+
if (entity.kind === "table" &&
|
|
186
|
+
entity.permissions.canUpdate &&
|
|
187
|
+
entity.shapes.update &&
|
|
188
|
+
entity.primaryKey &&
|
|
189
|
+
entity.primaryKey.columns.length > 0) {
|
|
190
|
+
hasAnyMethods = true;
|
|
191
|
+
declarations.push({
|
|
192
|
+
name: buildQueryName(inflection, entityName, "Update"),
|
|
193
|
+
capability: `queries:sql:${entityName}:update`,
|
|
842
194
|
});
|
|
843
195
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
kind: "symbol",
|
|
853
|
-
ref: { capability: "types", entity: typeName },
|
|
196
|
+
if (entity.kind === "table" &&
|
|
197
|
+
entity.permissions.canDelete &&
|
|
198
|
+
entity.primaryKey &&
|
|
199
|
+
entity.primaryKey.columns.length > 0) {
|
|
200
|
+
hasAnyMethods = true;
|
|
201
|
+
declarations.push({
|
|
202
|
+
name: buildQueryName(inflection, entityName, "Delete"),
|
|
203
|
+
capability: `queries:sql:${entityName}:delete`,
|
|
854
204
|
});
|
|
855
205
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const compositeFunctions = functionsByEntity.get(composite.name) ?? [];
|
|
892
|
-
if (compositeFunctions.length === 0)
|
|
893
|
-
continue;
|
|
894
|
-
const filePath = `${outputDir}/${composite.name}.ts`;
|
|
895
|
-
const methods = compositeFunctions.map(fn => generateFunctionWrapper(fn, ir, sqlStyle));
|
|
896
|
-
// Function wrappers are always flat exports
|
|
897
|
-
const statements = methods.map(m => conjure.export.fn(m.fn));
|
|
898
|
-
const file = ctx.file(filePath);
|
|
899
|
-
// Add user-provided header (must include SQL client import)
|
|
900
|
-
file.header(header);
|
|
901
|
-
// Import the composite type and any types needed by function args
|
|
902
|
-
const fnTypeImports = collectFunctionTypeImports(compositeFunctions, ir);
|
|
903
|
-
fnTypeImports.add(composite.name); // Always import the composite type
|
|
904
|
-
for (const typeName of fnTypeImports) {
|
|
905
|
-
file.import({
|
|
906
|
-
kind: "symbol",
|
|
907
|
-
ref: { capability: "types", entity: typeName },
|
|
206
|
+
if (entity.permissions.canSelect) {
|
|
207
|
+
const pkColumns = new Set(entity.primaryKey?.columns ?? []);
|
|
208
|
+
const processedColumns = new Set();
|
|
209
|
+
for (const index of entity.indexes) {
|
|
210
|
+
if (index.isPartial || index.hasExpressions || index.columns.length !== 1)
|
|
211
|
+
continue;
|
|
212
|
+
if (index.method === "gin" || index.method === "gist")
|
|
213
|
+
continue;
|
|
214
|
+
const columnName = index.columns[0];
|
|
215
|
+
if (pkColumns.has(columnName))
|
|
216
|
+
continue;
|
|
217
|
+
if (processedColumns.has(columnName))
|
|
218
|
+
continue;
|
|
219
|
+
processedColumns.add(columnName);
|
|
220
|
+
const pascalColumn = inflection.pascalCase(columnName);
|
|
221
|
+
hasAnyMethods = true;
|
|
222
|
+
declarations.push({
|
|
223
|
+
name: buildFindByName(inflection, entityName, columnName),
|
|
224
|
+
capability: `queries:sql:${entityName}:findBy${pascalColumn}`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const cursorCandidates = getCursorPaginationCandidates(entity);
|
|
229
|
+
for (const candidate of cursorCandidates) {
|
|
230
|
+
const pascalColumn = inflection.pascalCase(candidate.cursorColumnName);
|
|
231
|
+
hasAnyMethods = true;
|
|
232
|
+
declarations.push({
|
|
233
|
+
name: buildListByName(inflection, entityName, candidate.cursorColumnName),
|
|
234
|
+
capability: `queries:sql:${entityName}:listBy${pascalColumn}`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (hasAnyMethods) {
|
|
238
|
+
declarations.push({
|
|
239
|
+
name: `${entityName}Queries`,
|
|
240
|
+
capability: `queries:sql:${entityName}`,
|
|
908
241
|
});
|
|
909
242
|
}
|
|
910
|
-
file.ast(conjure.program(...statements)).emit();
|
|
911
243
|
}
|
|
912
244
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
245
|
+
return declarations;
|
|
246
|
+
}),
|
|
247
|
+
render: Effect.gen(function* () {
|
|
248
|
+
const ir = yield* IR;
|
|
249
|
+
const inflection = yield* Inflection;
|
|
250
|
+
const symbols = [];
|
|
251
|
+
const tableEntities = getTableEntities(ir).filter(e => e.tags.omit !== true);
|
|
252
|
+
const defaultSchemas = ir.schemas;
|
|
253
|
+
// User module imports for sql client
|
|
254
|
+
const queryUserImports = resolvedConfig.sqlImport
|
|
255
|
+
? [resolvedConfig.sqlImport]
|
|
256
|
+
: undefined;
|
|
257
|
+
if (resolvedConfig.generateQueries) {
|
|
258
|
+
for (const entity of tableEntities) {
|
|
259
|
+
const entityName = entity.name;
|
|
260
|
+
const tableName = buildTableName(entity, defaultSchemas);
|
|
261
|
+
const selectClause = buildSelectClause(entity, resolvedConfig.explicitColumns);
|
|
262
|
+
const entityMethods = [];
|
|
263
|
+
const fromClause = `from ${tableName}`;
|
|
264
|
+
const buildTemplateLiteral = (parts) => {
|
|
265
|
+
return conjure.taggedTemplate("sql", parts, []);
|
|
266
|
+
};
|
|
267
|
+
const buildTemplateLiteralWithParams = (parts, params) => {
|
|
268
|
+
return conjure.taggedTemplate("sql", parts, [...params]);
|
|
269
|
+
};
|
|
270
|
+
if (entity.permissions.canSelect && entity.primaryKey && entity.primaryKey.columns.length > 0) {
|
|
271
|
+
const pkColumn = entity.primaryKey.columns[0];
|
|
272
|
+
const pkField = entity.shapes.row.fields.find(f => f.columnName === pkColumn);
|
|
273
|
+
const pkParam = buildPkParam(pkField);
|
|
274
|
+
const method = {
|
|
275
|
+
name: buildQueryName(inflection, entityName, "FindById"),
|
|
276
|
+
kind: "read",
|
|
277
|
+
params: [pkParam],
|
|
278
|
+
returns: buildReturnType(entityName, false, true),
|
|
279
|
+
callSignature: { style: "named" },
|
|
280
|
+
};
|
|
281
|
+
entityMethods.push(method);
|
|
282
|
+
const templateLiteral = buildTemplateLiteral([
|
|
283
|
+
`${selectClause} ${fromClause} where ${pkColumn} = `,
|
|
284
|
+
"",
|
|
285
|
+
]);
|
|
286
|
+
const destructuredParam = buildDestructuredParam([pkParam]);
|
|
287
|
+
const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
|
|
288
|
+
symbols.push({
|
|
289
|
+
name: method.name,
|
|
290
|
+
capability: `queries:sql:${entityName}:findById`,
|
|
291
|
+
node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
|
|
292
|
+
exports: "named",
|
|
293
|
+
userImports: queryUserImports,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (entity.kind === "table" && entity.permissions.canInsert && entity.shapes.insert) {
|
|
297
|
+
const bodyParam = buildBodyParam(entityName, "insert");
|
|
298
|
+
const method = {
|
|
299
|
+
name: buildQueryName(inflection, entityName, "Create"),
|
|
300
|
+
kind: "create",
|
|
301
|
+
params: [bodyParam],
|
|
302
|
+
returns: buildReturnType(entityName, false, false),
|
|
303
|
+
callSignature: { style: "named", bodyStyle: "property" },
|
|
304
|
+
};
|
|
305
|
+
entityMethods.push(method);
|
|
306
|
+
const insertableFields = entity.shapes.insert.fields.filter(f => f.permissions.canInsert);
|
|
307
|
+
const columnNames = insertableFields.map(f => f.columnName);
|
|
308
|
+
const columnList = columnNames.join(", ");
|
|
309
|
+
const templateParts = [`insert into ${tableName} (${columnList}) values (`];
|
|
310
|
+
for (let i = 0; i < insertableFields.length; i++) {
|
|
311
|
+
if (i === 0) {
|
|
312
|
+
templateParts.push("");
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
templateParts.push(", ");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
templateParts.push(") returning *");
|
|
319
|
+
const paramExprs = insertableFields.map(f => b.memberExpression(b.identifier("data"), b.identifier(f.name)));
|
|
320
|
+
const templateLiteral = buildTemplateLiteralWithParams(templateParts, paramExprs);
|
|
321
|
+
const destructuredParam = buildDestructuredParam([bodyParam]);
|
|
322
|
+
const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
|
|
323
|
+
symbols.push({
|
|
324
|
+
name: method.name,
|
|
325
|
+
capability: `queries:sql:${entityName}:create`,
|
|
326
|
+
node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
|
|
327
|
+
exports: "named",
|
|
328
|
+
externalImports: [
|
|
329
|
+
{
|
|
330
|
+
from: queriesFilePath,
|
|
331
|
+
types: [entityName],
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
userImports: queryUserImports,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (entity.kind === "table" &&
|
|
338
|
+
entity.permissions.canUpdate &&
|
|
339
|
+
entity.shapes.update &&
|
|
340
|
+
entity.primaryKey &&
|
|
341
|
+
entity.primaryKey.columns.length > 0) {
|
|
342
|
+
const pkColumn = entity.primaryKey.columns[0];
|
|
343
|
+
const pkField = entity.shapes.row.fields.find(f => f.columnName === pkColumn);
|
|
344
|
+
const pkParam = buildPkParam(pkField);
|
|
345
|
+
const bodyParam = buildBodyParam(entityName, "update");
|
|
346
|
+
const method = {
|
|
347
|
+
name: buildQueryName(inflection, entityName, "Update"),
|
|
348
|
+
kind: "update",
|
|
349
|
+
params: [pkParam, bodyParam],
|
|
350
|
+
returns: buildReturnType(entityName, false, true),
|
|
351
|
+
callSignature: { style: "named", bodyStyle: "property" },
|
|
352
|
+
};
|
|
353
|
+
entityMethods.push(method);
|
|
354
|
+
const updatableFields = entity.shapes.update.fields.filter(f => f.permissions.canUpdate);
|
|
355
|
+
const templateParts = [`update ${tableName} set `];
|
|
356
|
+
for (let i = 0; i < updatableFields.length; i++) {
|
|
357
|
+
if (i === 0) {
|
|
358
|
+
templateParts.push(`${updatableFields[i].columnName} = `);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
templateParts.push(`, ${updatableFields[i].columnName} = `);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
templateParts.push(` where ${pkColumn} = `);
|
|
365
|
+
templateParts.push(" returning *");
|
|
366
|
+
const paramExprs = [
|
|
367
|
+
...updatableFields.map(f => b.memberExpression(b.identifier("data"), b.identifier(f.name))),
|
|
368
|
+
b.identifier(pkField.name),
|
|
369
|
+
];
|
|
370
|
+
const templateLiteral = buildTemplateLiteralWithParams(templateParts, paramExprs);
|
|
371
|
+
const destructuredParam = buildDestructuredParam([pkParam, bodyParam]);
|
|
372
|
+
const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
|
|
373
|
+
symbols.push({
|
|
374
|
+
name: method.name,
|
|
375
|
+
capability: `queries:sql:${entityName}:update`,
|
|
376
|
+
node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
|
|
377
|
+
exports: "named",
|
|
378
|
+
externalImports: [
|
|
379
|
+
{
|
|
380
|
+
from: queriesFilePath,
|
|
381
|
+
types: [entityName],
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
userImports: queryUserImports,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (entity.kind === "table" &&
|
|
388
|
+
entity.permissions.canDelete &&
|
|
389
|
+
entity.primaryKey &&
|
|
390
|
+
entity.primaryKey.columns.length > 0) {
|
|
391
|
+
const pkColumn = entity.primaryKey.columns[0];
|
|
392
|
+
const pkField = entity.shapes.row.fields.find(f => f.columnName === pkColumn);
|
|
393
|
+
const pkParam = buildPkParam(pkField);
|
|
394
|
+
const method = {
|
|
395
|
+
name: buildQueryName(inflection, entityName, "Delete"),
|
|
396
|
+
kind: "delete",
|
|
397
|
+
params: [pkParam],
|
|
398
|
+
returns: buildReturnType(entityName, false, false),
|
|
399
|
+
callSignature: { style: "named" },
|
|
400
|
+
};
|
|
401
|
+
entityMethods.push(method);
|
|
402
|
+
const templateLiteral = buildTemplateLiteral([
|
|
403
|
+
`delete from ${tableName} where ${pkColumn} = `,
|
|
404
|
+
"",
|
|
405
|
+
]);
|
|
406
|
+
const destructuredParam = buildDestructuredParam([pkParam]);
|
|
407
|
+
const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
|
|
408
|
+
symbols.push({
|
|
409
|
+
name: method.name,
|
|
410
|
+
capability: `queries:sql:${entityName}:delete`,
|
|
411
|
+
node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
|
|
412
|
+
exports: "named",
|
|
413
|
+
userImports: queryUserImports,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (entity.permissions.canSelect) {
|
|
417
|
+
const pkColumns = new Set(entity.primaryKey?.columns ?? []);
|
|
418
|
+
const processedColumns = new Set();
|
|
419
|
+
for (const index of entity.indexes) {
|
|
420
|
+
if (index.isPartial || index.hasExpressions || index.columns.length !== 1)
|
|
421
|
+
continue;
|
|
422
|
+
if (index.method === "gin" || index.method === "gist")
|
|
423
|
+
continue;
|
|
424
|
+
const columnName = index.columns[0];
|
|
425
|
+
if (pkColumns.has(columnName))
|
|
426
|
+
continue;
|
|
427
|
+
if (processedColumns.has(columnName))
|
|
428
|
+
continue;
|
|
429
|
+
processedColumns.add(columnName);
|
|
430
|
+
const field = entity.shapes.row.fields.find(f => f.columnName === columnName);
|
|
431
|
+
if (!field)
|
|
432
|
+
continue;
|
|
433
|
+
const pascalColumn = inflection.pascalCase(columnName);
|
|
434
|
+
const isUnique = index.isUnique;
|
|
435
|
+
const lookupParam = buildLookupParam(field);
|
|
436
|
+
const method = {
|
|
437
|
+
name: buildFindByName(inflection, entityName, columnName),
|
|
438
|
+
kind: "lookup",
|
|
439
|
+
params: [lookupParam],
|
|
440
|
+
returns: buildReturnType(entityName, !isUnique, isUnique),
|
|
441
|
+
lookupField: field.name,
|
|
442
|
+
isUniqueLookup: isUnique,
|
|
443
|
+
callSignature: { style: "named" },
|
|
444
|
+
};
|
|
445
|
+
entityMethods.push(method);
|
|
446
|
+
const templateLiteral = buildTemplateLiteral([
|
|
447
|
+
`${selectClause} ${fromClause} where ${columnName} = `,
|
|
448
|
+
"",
|
|
449
|
+
]);
|
|
450
|
+
const destructuredParam = buildDestructuredParam([lookupParam]);
|
|
451
|
+
const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
|
|
452
|
+
symbols.push({
|
|
453
|
+
name: method.name,
|
|
454
|
+
capability: `queries:sql:${entityName}:findBy${pascalColumn}`,
|
|
455
|
+
node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
|
|
456
|
+
exports: "named",
|
|
457
|
+
userImports: queryUserImports,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const cursorCandidates = getCursorPaginationCandidates(entity);
|
|
462
|
+
for (const candidate of cursorCandidates) {
|
|
463
|
+
const pascalColumn = inflection.pascalCase(candidate.cursorColumnName);
|
|
464
|
+
const pkField = entity.shapes.row.fields.find(f => f.name === candidate.pkColumn);
|
|
465
|
+
if (!pkField)
|
|
466
|
+
continue;
|
|
467
|
+
const pkParam = {
|
|
468
|
+
name: candidate.pkColumn,
|
|
469
|
+
type: pgTypeToTsType(getPgType(pkField)),
|
|
470
|
+
required: false,
|
|
471
|
+
};
|
|
472
|
+
const limitParam = {
|
|
473
|
+
name: "limit",
|
|
474
|
+
type: "number",
|
|
475
|
+
required: false,
|
|
476
|
+
defaultValue: resolvedConfig.defaultLimit,
|
|
477
|
+
source: "pagination",
|
|
478
|
+
};
|
|
479
|
+
const cursorParam = {
|
|
480
|
+
name: "cursor",
|
|
481
|
+
type: `{ ${candidate.cursorColumn}: Date; ${candidate.pkColumn}: ${pkParam.type} }`,
|
|
482
|
+
required: false,
|
|
483
|
+
};
|
|
484
|
+
const operator = candidate.desc ? "<" : ">";
|
|
485
|
+
const orderDirection = candidate.desc ? "DESC" : "ASC";
|
|
486
|
+
const method = {
|
|
487
|
+
name: buildListByName(inflection, entityName, candidate.cursorColumnName),
|
|
488
|
+
kind: "list",
|
|
489
|
+
params: [cursorParam, limitParam],
|
|
490
|
+
returns: buildReturnType(entityName, true, false),
|
|
491
|
+
callSignature: { style: "named" },
|
|
492
|
+
};
|
|
493
|
+
entityMethods.push(method);
|
|
494
|
+
const templateParts = [
|
|
495
|
+
`${selectClause} ${fromClause} where ($`,
|
|
496
|
+
`::timestamptz IS NULL OR (${candidate.cursorColumnName}, ${candidate.pkColumnName}) ${operator} ($`,
|
|
497
|
+
`, `,
|
|
498
|
+
`)) order by ${candidate.cursorColumnName} ${orderDirection}, ${candidate.pkColumnName} ${orderDirection} limit `,
|
|
499
|
+
];
|
|
500
|
+
const paramExprs = [
|
|
501
|
+
b.memberExpression(b.identifier("cursor"), b.identifier(candidate.cursorColumn)),
|
|
502
|
+
b.memberExpression(b.identifier("cursor"), b.identifier(candidate.pkColumn)),
|
|
503
|
+
b.identifier("limit"),
|
|
504
|
+
];
|
|
505
|
+
const templateLiteral = buildTemplateLiteralWithParams(templateParts, paramExprs);
|
|
506
|
+
const destructuredParam = buildDestructuredParam([cursorParam, limitParam]);
|
|
507
|
+
const fnExpr = fn().rawParam(destructuredParam).arrow().body(conjure.stmt.return(templateLiteral)).build();
|
|
508
|
+
symbols.push({
|
|
509
|
+
name: method.name,
|
|
510
|
+
capability: `queries:sql:${entityName}:listBy${pascalColumn}`,
|
|
511
|
+
node: exp.const(method.name, { capability: "", entity: entityName }, fnExpr).node,
|
|
512
|
+
exports: "named",
|
|
513
|
+
userImports: queryUserImports,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const pkField = entity.primaryKey?.columns[0]
|
|
517
|
+
? entity.shapes.row.fields.find(f => f.columnName === entity.primaryKey.columns[0])
|
|
518
|
+
: undefined;
|
|
519
|
+
const entityExtension = {
|
|
520
|
+
methods: entityMethods,
|
|
521
|
+
pkType: pkField ? pgTypeToTsType(getPgType(pkField)) : undefined,
|
|
522
|
+
hasCompositePk: (entity.primaryKey?.columns.length ?? 0) > 1,
|
|
523
|
+
};
|
|
524
|
+
symbols.push({
|
|
525
|
+
name: `${entityName}Queries`,
|
|
526
|
+
capability: `queries:sql:${entityName}`,
|
|
527
|
+
node: b.emptyStatement(),
|
|
528
|
+
metadata: entityExtension,
|
|
529
|
+
exports: false,
|
|
928
530
|
});
|
|
929
531
|
}
|
|
930
|
-
file.ast(conjure.program(...statements)).emit();
|
|
931
|
-
// TODO: Register standalone functions to symbol registry when HTTP plugins need them
|
|
932
|
-
// For now, standalone functions are not exposed via routes
|
|
933
532
|
}
|
|
934
|
-
|
|
935
|
-
|
|
533
|
+
return symbols;
|
|
534
|
+
}),
|
|
535
|
+
};
|
|
936
536
|
}
|
|
937
537
|
//# sourceMappingURL=sql-queries.js.map
|