@danielfgray/pg-sourcerer 0.2.0 → 0.2.2
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/dist/cli.js +25 -19
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +14 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -1
- package/dist/generate.d.ts +31 -21
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +53 -43
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +20 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -13
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +0 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +90 -35
- package/dist/init.js.map +1 -1
- package/dist/ir/extensions/queries.d.ts +264 -0
- package/dist/ir/extensions/queries.d.ts.map +1 -0
- package/dist/ir/extensions/queries.js +153 -0
- package/dist/ir/extensions/queries.js.map +1 -0
- package/dist/ir/extensions/schema-builder.d.ts +61 -0
- package/dist/ir/extensions/schema-builder.d.ts.map +1 -0
- package/dist/ir/extensions/schema-builder.js +5 -0
- package/dist/ir/extensions/schema-builder.js.map +1 -0
- package/dist/lib/conjure.d.ts +101 -0
- package/dist/lib/conjure.d.ts.map +1 -1
- package/dist/lib/conjure.js +204 -26
- package/dist/lib/conjure.js.map +1 -1
- package/dist/lib/hex.d.ts +10 -8
- package/dist/lib/hex.d.ts.map +1 -1
- package/dist/lib/hex.js +18 -15
- package/dist/lib/hex.js.map +1 -1
- package/dist/plugins/arktype.d.ts +27 -14
- package/dist/plugins/arktype.d.ts.map +1 -1
- package/dist/plugins/arktype.js +166 -130
- package/dist/plugins/arktype.js.map +1 -1
- package/dist/plugins/effect.d.ts +53 -0
- package/dist/plugins/effect.d.ts.map +1 -0
- package/dist/plugins/effect.js +1074 -0
- package/dist/plugins/effect.js.map +1 -0
- package/dist/plugins/http-elysia.d.ts +32 -0
- package/dist/plugins/http-elysia.d.ts.map +1 -0
- package/dist/plugins/http-elysia.js +613 -0
- package/dist/plugins/http-elysia.js.map +1 -0
- package/dist/plugins/http-express.d.ts +36 -0
- package/dist/plugins/http-express.d.ts.map +1 -0
- package/dist/plugins/http-express.js +388 -0
- package/dist/plugins/http-express.js.map +1 -0
- package/dist/plugins/http-hono.d.ts +36 -0
- package/dist/plugins/http-hono.d.ts.map +1 -0
- package/dist/plugins/http-hono.js +453 -0
- package/dist/plugins/http-hono.js.map +1 -0
- package/dist/plugins/http-orpc.d.ts +55 -0
- package/dist/plugins/http-orpc.d.ts.map +1 -0
- package/dist/plugins/http-orpc.js +370 -0
- package/dist/plugins/http-orpc.js.map +1 -0
- package/dist/plugins/http-trpc.d.ts +59 -0
- package/dist/plugins/http-trpc.d.ts.map +1 -0
- package/dist/plugins/http-trpc.js +392 -0
- package/dist/plugins/http-trpc.js.map +1 -0
- package/dist/plugins/kysely/queries.d.ts +92 -0
- package/dist/plugins/kysely/queries.d.ts.map +1 -0
- package/dist/plugins/kysely/queries.js +1169 -0
- package/dist/plugins/kysely/queries.js.map +1 -0
- package/dist/plugins/kysely/shared.d.ts +59 -0
- package/dist/plugins/kysely/shared.d.ts.map +1 -0
- package/dist/plugins/kysely/shared.js +247 -0
- package/dist/plugins/kysely/shared.js.map +1 -0
- package/dist/plugins/kysely/types.d.ts +22 -0
- package/dist/plugins/kysely/types.d.ts.map +1 -0
- package/dist/plugins/kysely/types.js +428 -0
- package/dist/plugins/kysely/types.js.map +1 -0
- package/dist/plugins/kysely.d.ts +72 -0
- package/dist/plugins/kysely.d.ts.map +1 -0
- package/dist/plugins/kysely.js +906 -0
- package/dist/plugins/kysely.js.map +1 -0
- package/dist/plugins/sql-queries.d.ts +55 -7
- package/dist/plugins/sql-queries.d.ts.map +1 -1
- package/dist/plugins/sql-queries.js +747 -121
- package/dist/plugins/sql-queries.js.map +1 -1
- package/dist/plugins/types.d.ts +20 -14
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/plugins/types.js +90 -112
- package/dist/plugins/types.js.map +1 -1
- package/dist/plugins/valibot.d.ts +45 -0
- package/dist/plugins/valibot.d.ts.map +1 -0
- package/dist/plugins/valibot.js +422 -0
- package/dist/plugins/valibot.js.map +1 -0
- package/dist/plugins/zod.d.ts +27 -14
- package/dist/plugins/zod.d.ts.map +1 -1
- package/dist/plugins/zod.js +231 -166
- package/dist/plugins/zod.js.map +1 -1
- package/dist/services/artifact-store.d.ts +11 -1
- package/dist/services/artifact-store.d.ts.map +1 -1
- package/dist/services/artifact-store.js +9 -0
- package/dist/services/artifact-store.js.map +1 -1
- package/dist/services/config-loader.d.ts +4 -0
- package/dist/services/config-loader.d.ts.map +1 -1
- package/dist/services/config-loader.js +1 -1
- package/dist/services/config-loader.js.map +1 -1
- package/dist/services/config.d.ts +57 -0
- package/dist/services/config.d.ts.map +1 -0
- package/dist/services/config.js +66 -0
- package/dist/services/config.js.map +1 -0
- package/dist/services/core-providers.d.ts +15 -0
- package/dist/services/core-providers.d.ts.map +1 -0
- package/dist/services/core-providers.js +23 -0
- package/dist/services/core-providers.js.map +1 -0
- package/dist/services/emissions.d.ts +14 -0
- package/dist/services/emissions.d.ts.map +1 -1
- package/dist/services/emissions.js +86 -47
- package/dist/services/emissions.js.map +1 -1
- package/dist/services/execution.d.ts +35 -0
- package/dist/services/execution.d.ts.map +1 -0
- package/dist/services/execution.js +86 -0
- package/dist/services/execution.js.map +1 -0
- package/dist/services/file-builder.d.ts +4 -0
- package/dist/services/file-builder.d.ts.map +1 -1
- package/dist/services/file-builder.js.map +1 -1
- package/dist/services/inflection.d.ts +2 -2
- package/dist/services/inflection.d.ts.map +1 -1
- package/dist/services/inflection.js +4 -4
- package/dist/services/inflection.js.map +1 -1
- package/dist/services/ir-builder.d.ts.map +1 -1
- package/dist/services/ir-builder.js +10 -3
- package/dist/services/ir-builder.js.map +1 -1
- package/dist/services/pg-types.d.ts +31 -0
- package/dist/services/pg-types.d.ts.map +1 -1
- package/dist/services/pg-types.js +24 -0
- package/dist/services/pg-types.js.map +1 -1
- package/dist/services/plugin-runner.d.ts +27 -37
- package/dist/services/plugin-runner.d.ts.map +1 -1
- package/dist/services/plugin-runner.js +73 -171
- package/dist/services/plugin-runner.js.map +1 -1
- package/dist/services/plugin.d.ts +350 -215
- package/dist/services/plugin.d.ts.map +1 -1
- package/dist/services/plugin.js +182 -130
- package/dist/services/plugin.js.map +1 -1
- package/dist/services/resolution.d.ts +38 -0
- package/dist/services/resolution.d.ts.map +1 -0
- package/dist/services/resolution.js +242 -0
- package/dist/services/resolution.js.map +1 -0
- package/dist/services/service-registry.d.ts +74 -0
- package/dist/services/service-registry.d.ts.map +1 -0
- package/dist/services/service-registry.js +61 -0
- package/dist/services/service-registry.js.map +1 -0
- package/dist/services/symbols.d.ts +59 -0
- package/dist/services/symbols.d.ts.map +1 -1
- package/dist/services/symbols.js +16 -0
- package/dist/services/symbols.js.map +1 -1
- package/dist/testing.d.ts +4 -25
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -23
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/plugins/effect-model.d.ts +0 -17
- package/dist/plugins/effect-model.d.ts.map +0 -1
- package/dist/plugins/effect-model.js +0 -409
- package/dist/plugins/effect-model.js.map +0 -1
- package/dist/plugins/kysely-queries.d.ts +0 -66
- package/dist/plugins/kysely-queries.d.ts.map +0 -1
- package/dist/plugins/kysely-queries.js +0 -960
- package/dist/plugins/kysely-queries.js.map +0 -1
- package/dist/plugins/kysely-types.d.ts +0 -35
- package/dist/plugins/kysely-types.d.ts.map +0 -1
- package/dist/plugins/kysely-types.js +0 -601
- package/dist/plugins/kysely-types.js.map +0 -1
|
@@ -1,24 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQL Queries
|
|
2
|
+
* SQL Queries Provider - Generate raw SQL query functions using template strings
|
|
3
3
|
*/
|
|
4
4
|
import { Schema as S } from "effect";
|
|
5
5
|
import { definePlugin } from "../services/plugin.js";
|
|
6
|
-
import { getTableEntities, getEnumEntities } from "../ir/semantic-ir.js";
|
|
6
|
+
import { getTableEntities, getEnumEntities, getFunctionEntities, getCompositeEntities, } from "../ir/semantic-ir.js";
|
|
7
7
|
import { conjure } from "../lib/conjure.js";
|
|
8
8
|
import { hex } from "../lib/hex.js";
|
|
9
9
|
import { resolveFieldType, tsTypeToAst } from "../lib/field-utils.js";
|
|
10
10
|
import { inflect } from "../services/inflection.js";
|
|
11
|
-
const { ts, b, param } = conjure;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
|
|
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({
|
|
16
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,
|
|
17
34
|
/** SQL query style. Defaults to "tag" (tagged template literals) */
|
|
18
|
-
sqlStyle: S.optionalWith(S.Union(S.Literal("tag"), S.Literal("string")), {
|
|
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" }),
|
|
19
56
|
});
|
|
20
57
|
/** Find a field in the row shape by column name */
|
|
21
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 *";
|
|
22
63
|
/** Get the TypeScript type AST for a field */
|
|
23
64
|
const getFieldTypeAst = (field, ctx) => {
|
|
24
65
|
if (!field)
|
|
@@ -67,9 +108,16 @@ const toPascalCase = (s) => inflect.pascalCase(s);
|
|
|
67
108
|
// ============================================================================
|
|
68
109
|
// CRUD Function Generators
|
|
69
110
|
// ============================================================================
|
|
70
|
-
/**
|
|
111
|
+
/** Get TypeScript type string for a field */
|
|
112
|
+
const getFieldTypeString = (field, ctx) => {
|
|
113
|
+
if (!field)
|
|
114
|
+
return "string";
|
|
115
|
+
const resolved = resolveFieldType(field, ctx.enums, ctx.ir.extensions);
|
|
116
|
+
return resolved.enumDef ? resolved.enumDef.name : resolved.tsType;
|
|
117
|
+
};
|
|
118
|
+
/** Generate findById method if entity has a primary key and canSelect permission */
|
|
71
119
|
const generateFindById = (ctx) => {
|
|
72
|
-
const { entity, sqlStyle } = ctx;
|
|
120
|
+
const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
|
|
73
121
|
if (!entity.primaryKey || !entity.permissions.canSelect)
|
|
74
122
|
return undefined;
|
|
75
123
|
const pkColName = entity.primaryKey.columns[0];
|
|
@@ -78,38 +126,73 @@ const generateFindById = (ctx) => {
|
|
|
78
126
|
return undefined;
|
|
79
127
|
const rowType = entity.shapes.row.name;
|
|
80
128
|
const fieldName = pkField.name; // JS property name (e.g., "id")
|
|
129
|
+
const selectClause = buildSelectClause(entity, explicitColumns);
|
|
81
130
|
const parts = {
|
|
82
|
-
templateParts: [
|
|
131
|
+
templateParts: [
|
|
132
|
+
`${selectClause} from ${entity.schemaName}.${entity.pgName} where ${pkColName} = `,
|
|
133
|
+
"",
|
|
134
|
+
],
|
|
83
135
|
params: [b.identifier(fieldName)],
|
|
84
136
|
};
|
|
85
137
|
// Build query and extract first row
|
|
86
138
|
const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
|
|
87
139
|
const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 };
|
|
92
158
|
};
|
|
93
|
-
/** Generate findMany
|
|
159
|
+
/** Generate findMany method with pagination if entity has canSelect permission */
|
|
94
160
|
const generateFindMany = (ctx) => {
|
|
95
|
-
const { entity, sqlStyle } = ctx;
|
|
161
|
+
const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
|
|
96
162
|
if (!entity.permissions.canSelect)
|
|
97
163
|
return undefined;
|
|
98
164
|
const rowType = entity.shapes.row.name;
|
|
165
|
+
const selectClause = buildSelectClause(entity, explicitColumns);
|
|
99
166
|
const parts = {
|
|
100
|
-
templateParts: [
|
|
167
|
+
templateParts: [
|
|
168
|
+
`${selectClause} from ${entity.schemaName}.${entity.pgName} limit `,
|
|
169
|
+
` offset `,
|
|
170
|
+
"",
|
|
171
|
+
],
|
|
101
172
|
params: [b.identifier("limit"), b.identifier("offset")],
|
|
102
173
|
};
|
|
103
|
-
|
|
174
|
+
const name = exportName(entityName, "FindManys");
|
|
175
|
+
const fn = asyncFn(name, [
|
|
104
176
|
param.destructured([
|
|
105
177
|
{ name: "limit", type: ts.number(), optional: true, defaultValue: b.numericLiteral(50) },
|
|
106
178
|
{ name: "offset", type: ts.number(), optional: true, defaultValue: b.numericLiteral(0) },
|
|
107
179
|
]),
|
|
108
|
-
], hex.returnQuery(sqlStyle, parts, ts.array(ts.ref(rowType))))
|
|
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 };
|
|
109
192
|
};
|
|
110
|
-
/** Generate delete
|
|
193
|
+
/** Generate delete method if entity has a primary key and canDelete permission */
|
|
111
194
|
const generateDelete = (ctx) => {
|
|
112
|
-
const { entity, sqlStyle } = ctx;
|
|
195
|
+
const { entity, sqlStyle, entityName, exportName } = ctx;
|
|
113
196
|
if (!entity.primaryKey || !entity.permissions.canDelete)
|
|
114
197
|
return undefined;
|
|
115
198
|
const pkColName = entity.primaryKey.columns[0];
|
|
@@ -124,13 +207,28 @@ const generateDelete = (ctx) => {
|
|
|
124
207
|
};
|
|
125
208
|
// Delete returns void, no type parameter needed
|
|
126
209
|
const queryExpr = hex.query(sqlStyle, parts);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 };
|
|
130
228
|
};
|
|
131
|
-
/** Generate insert
|
|
229
|
+
/** Generate insert method if entity has canInsert permission */
|
|
132
230
|
const generateInsert = (ctx) => {
|
|
133
|
-
const { entity, sqlStyle } = ctx;
|
|
231
|
+
const { entity, sqlStyle, entityName, exportName } = ctx;
|
|
134
232
|
if (!entity.permissions.canInsert)
|
|
135
233
|
return undefined;
|
|
136
234
|
// Use insert shape if available, otherwise fall back to row
|
|
@@ -142,10 +240,15 @@ const generateInsert = (ctx) => {
|
|
|
142
240
|
if (insertableFields.length === 0)
|
|
143
241
|
return undefined;
|
|
144
242
|
const columnNames = insertableFields.map(f => f.columnName);
|
|
145
|
-
|
|
146
|
-
// Build: insert into schema.table (col1, col2) values ($data.field1, $data.field2) returning *
|
|
243
|
+
// Build: insert into schema.table (col1, col2) values ($field1, $field2) returning *
|
|
147
244
|
const columnList = columnNames.join(", ");
|
|
148
|
-
const valuePlaceholders =
|
|
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
|
+
});
|
|
149
252
|
// Template parts: "insert into ... values (" + "" + ", " + ", " + ... + ") returning *"
|
|
150
253
|
const parts = {
|
|
151
254
|
templateParts: [
|
|
@@ -153,21 +256,33 @@ const generateInsert = (ctx) => {
|
|
|
153
256
|
...valuePlaceholders.slice(1),
|
|
154
257
|
`) returning *`,
|
|
155
258
|
],
|
|
156
|
-
params:
|
|
259
|
+
params: paramExprs,
|
|
157
260
|
};
|
|
158
261
|
const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
|
|
159
262
|
const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
|
|
160
|
-
//
|
|
161
|
-
const
|
|
162
|
-
|
|
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 };
|
|
163
283
|
};
|
|
164
|
-
/** Generate all CRUD
|
|
165
|
-
const
|
|
166
|
-
generateFindById(ctx),
|
|
167
|
-
generateFindMany(ctx),
|
|
168
|
-
generateInsert(ctx),
|
|
169
|
-
generateDelete(ctx),
|
|
170
|
-
].filter((s) => s != null);
|
|
284
|
+
/** Generate all CRUD methods for an entity */
|
|
285
|
+
const generateCrudMethods = (ctx) => [generateFindById(ctx), generateFindMany(ctx), generateInsert(ctx), generateDelete(ctx)].filter((m) => m != null);
|
|
171
286
|
// ============================================================================
|
|
172
287
|
// Index-based Lookup Functions
|
|
173
288
|
// ============================================================================
|
|
@@ -178,27 +293,22 @@ const shouldGenerateLookup = (index) => !index.isPartial &&
|
|
|
178
293
|
index.method !== "gin" &&
|
|
179
294
|
index.method !== "gist";
|
|
180
295
|
/**
|
|
181
|
-
* Generate
|
|
182
|
-
*
|
|
296
|
+
* Generate the method name portion for an index-based lookup.
|
|
297
|
+
* Returns PascalCase like "GetByUsername" or "GetsByUser" for use with exportName.
|
|
183
298
|
*/
|
|
184
|
-
const
|
|
299
|
+
const generateLookupMethodName = (index, relation, columnName) => {
|
|
185
300
|
const isUnique = index.isUnique || index.isPrimary;
|
|
186
|
-
const
|
|
187
|
-
? entity.name.replace(/s$/, "") // singular for unique
|
|
188
|
-
: entity.name.replace(/s$/, "") + "s"; // plural for non-unique
|
|
301
|
+
const prefix = isUnique ? "GetBy" : "GetsBy";
|
|
189
302
|
// Use semantic name if FK relation exists, otherwise fall back to column name
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
? deriveSemanticName(relation, columnName)
|
|
193
|
-
: index.columns[0];
|
|
194
|
-
return `get${entityName}By${toPascalCase(byName)}`;
|
|
303
|
+
const byName = relation ? deriveSemanticName(relation, columnName) : index.columns[0];
|
|
304
|
+
return `${prefix}${toPascalCase(byName)}`;
|
|
195
305
|
};
|
|
196
306
|
/**
|
|
197
|
-
* Generate a lookup
|
|
307
|
+
* Generate a lookup method for a single-column index.
|
|
198
308
|
* Uses semantic parameter naming when the column corresponds to an FK relation.
|
|
199
309
|
*/
|
|
200
|
-
const
|
|
201
|
-
const { entity, sqlStyle } = ctx;
|
|
310
|
+
const generateLookupMethod = (index, ctx) => {
|
|
311
|
+
const { entity, sqlStyle, entityName, exportName, explicitColumns } = ctx;
|
|
202
312
|
const rowType = entity.shapes.row.name;
|
|
203
313
|
const columnName = index.columnNames[0];
|
|
204
314
|
const field = findRowField(entity, columnName);
|
|
@@ -207,105 +317,621 @@ const generateLookupFunction = (index, ctx) => {
|
|
|
207
317
|
// Check if this index column corresponds to an FK relation
|
|
208
318
|
const relation = findRelationForColumn(entity, columnName);
|
|
209
319
|
// Use semantic param name if FK relation exists, otherwise use field name
|
|
210
|
-
const paramName = relation
|
|
211
|
-
? deriveSemanticName(relation, columnName)
|
|
212
|
-
: fieldName;
|
|
320
|
+
const paramName = relation ? deriveSemanticName(relation, columnName) : fieldName;
|
|
213
321
|
// For semantic naming, use indexed access type (Post["userId"])
|
|
214
322
|
// For regular naming, use Pick<Post, "fieldName">
|
|
215
323
|
const useSemanticNaming = relation !== undefined && paramName !== fieldName;
|
|
324
|
+
const selectClause = buildSelectClause(entity, explicitColumns);
|
|
216
325
|
const parts = {
|
|
217
|
-
templateParts: [
|
|
326
|
+
templateParts: [
|
|
327
|
+
`${selectClause} from ${entity.schemaName}.${entity.pgName} where ${columnName} = `,
|
|
328
|
+
"",
|
|
329
|
+
],
|
|
218
330
|
params: [b.identifier(paramName)],
|
|
219
331
|
};
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
};
|
|
225
363
|
if (isUnique) {
|
|
226
364
|
// Extract first row for unique lookups
|
|
227
365
|
const queryExpr = hex.query(sqlStyle, parts, ts.array(ts.ref(rowType)));
|
|
228
366
|
const varDecl = hex.firstRowDecl(sqlStyle, "result", queryExpr);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
b.returnStatement(b.identifier("result")),
|
|
232
|
-
]));
|
|
367
|
+
const fn = asyncFn(name, [paramNode], [varDecl, b.returnStatement(b.identifier("result"))]);
|
|
368
|
+
return { name, fn, meta };
|
|
233
369
|
}
|
|
234
370
|
// Non-unique: return all matching rows
|
|
235
|
-
|
|
371
|
+
const fn = asyncFn(name, [paramNode], hex.returnQuery(sqlStyle, parts, ts.array(ts.ref(rowType))));
|
|
372
|
+
return { name, fn, meta };
|
|
236
373
|
};
|
|
237
|
-
/** Generate lookup
|
|
238
|
-
const
|
|
374
|
+
/** Generate lookup methods for all eligible indexes, deduplicating by name */
|
|
375
|
+
const generateLookupMethods = (ctx) => {
|
|
239
376
|
const seen = new Set();
|
|
240
377
|
return ctx.entity.indexes
|
|
241
378
|
.filter(index => shouldGenerateLookup(index) && !index.isPrimary)
|
|
242
379
|
.filter(index => {
|
|
243
380
|
const columnName = index.columnNames[0];
|
|
244
381
|
const relation = findRelationForColumn(ctx.entity, columnName);
|
|
245
|
-
const
|
|
382
|
+
const methodName = generateLookupMethodName(index, relation, columnName);
|
|
383
|
+
const name = ctx.exportName(ctx.entityName, methodName);
|
|
246
384
|
if (seen.has(name))
|
|
247
385
|
return false;
|
|
248
386
|
seen.add(name);
|
|
249
387
|
return true;
|
|
250
388
|
})
|
|
251
|
-
.map(index =>
|
|
389
|
+
.map(index => generateLookupMethod(index, ctx));
|
|
252
390
|
};
|
|
253
391
|
// ============================================================================
|
|
254
|
-
//
|
|
392
|
+
// Function Wrapper Generation
|
|
255
393
|
// ============================================================================
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
394
|
+
/**
|
|
395
|
+
* Map PostgreSQL type names to TypeScript types.
|
|
396
|
+
* Simplified version - covers common scalar types.
|
|
397
|
+
*/
|
|
398
|
+
const pgTypeNameToTs = (typeName) => {
|
|
399
|
+
const typeMap = {
|
|
400
|
+
// Numeric
|
|
401
|
+
int2: "number",
|
|
402
|
+
int4: "number",
|
|
403
|
+
int8: "string", // bigint as string
|
|
404
|
+
float4: "number",
|
|
405
|
+
float8: "number",
|
|
406
|
+
numeric: "string",
|
|
407
|
+
decimal: "string",
|
|
408
|
+
// Text
|
|
409
|
+
text: "string",
|
|
410
|
+
varchar: "string",
|
|
411
|
+
char: "string",
|
|
412
|
+
citext: "string",
|
|
413
|
+
name: "string",
|
|
414
|
+
// Boolean
|
|
415
|
+
bool: "boolean",
|
|
416
|
+
// Date/time
|
|
417
|
+
date: "Date",
|
|
418
|
+
timestamp: "Date",
|
|
419
|
+
timestamptz: "Date",
|
|
420
|
+
time: "string",
|
|
421
|
+
timetz: "string",
|
|
422
|
+
interval: "string",
|
|
423
|
+
// UUID
|
|
424
|
+
uuid: "string",
|
|
425
|
+
// JSON
|
|
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));
|
|
473
|
+
return {
|
|
474
|
+
queries: all.filter(fn => categorizeFunction(fn) === "queries"),
|
|
475
|
+
mutations: all.filter(fn => categorizeFunction(fn) === "mutations"),
|
|
476
|
+
};
|
|
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);
|
|
519
|
+
return {
|
|
520
|
+
tsType,
|
|
521
|
+
isArray,
|
|
522
|
+
isScalar: true,
|
|
523
|
+
};
|
|
524
|
+
};
|
|
525
|
+
/**
|
|
526
|
+
* Resolve a function argument to TypeScript type information.
|
|
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;
|
|
567
|
+
return {
|
|
568
|
+
name: arg.name || "arg",
|
|
569
|
+
tsType,
|
|
570
|
+
isOptional: arg.hasDefault,
|
|
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
|
+
}
|
|
619
|
+
};
|
|
620
|
+
// Build parameter: destructured object for named style (zero-arg functions have no params)
|
|
621
|
+
const params = resolvedArgs.length === 0
|
|
622
|
+
? []
|
|
623
|
+
: [
|
|
624
|
+
param.destructured(resolvedArgs.map(arg => ({
|
|
625
|
+
name: arg.name,
|
|
626
|
+
type: typeStrToAst(arg.tsType),
|
|
627
|
+
optional: arg.isOptional,
|
|
628
|
+
}))),
|
|
629
|
+
];
|
|
630
|
+
// Build SQL based on return type
|
|
631
|
+
let sql;
|
|
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" },
|
|
693
|
+
};
|
|
694
|
+
return { name, fn: fnDecl, meta };
|
|
695
|
+
};
|
|
696
|
+
/**
|
|
697
|
+
* Collect type imports needed for function wrappers.
|
|
698
|
+
*/
|
|
699
|
+
const collectFunctionTypeImports = (functions, ir) => {
|
|
700
|
+
const imports = new Set();
|
|
701
|
+
for (const fn of functions) {
|
|
702
|
+
const resolvedReturn = resolveReturnType(fn, ir);
|
|
703
|
+
if (resolvedReturn.needsImport) {
|
|
704
|
+
imports.add(resolvedReturn.needsImport);
|
|
705
|
+
}
|
|
706
|
+
for (const arg of resolveArgs(fn, ir)) {
|
|
707
|
+
if (arg.needsImport) {
|
|
708
|
+
imports.add(arg.needsImport);
|
|
288
709
|
}
|
|
289
|
-
|
|
290
|
-
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return imports;
|
|
713
|
+
};
|
|
714
|
+
// ============================================================================
|
|
715
|
+
// Export Style Helpers
|
|
716
|
+
// ============================================================================
|
|
717
|
+
/**
|
|
718
|
+
* Convert MethodDef array to flat export statements.
|
|
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.
|
|
752
|
+
*
|
|
753
|
+
* @example
|
|
754
|
+
* ```typescript
|
|
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
|
+
* ```
|
|
764
|
+
*/
|
|
765
|
+
export function sqlQueries(config) {
|
|
766
|
+
const parsed = S.decodeUnknownSync(SqlQueriesPluginConfigSchema)(config);
|
|
767
|
+
// Resolve config with properly typed exportName
|
|
768
|
+
const resolvedConfig = {
|
|
769
|
+
...parsed,
|
|
770
|
+
exportName: config.exportName ?? defaultExportName,
|
|
771
|
+
};
|
|
772
|
+
return definePlugin({
|
|
773
|
+
name: "sql-queries",
|
|
774
|
+
kind: "queries",
|
|
775
|
+
singleton: true,
|
|
776
|
+
canProvide: () => true,
|
|
777
|
+
provide: (_params, _deps, ctx) => {
|
|
778
|
+
const { ir, inflection } = ctx;
|
|
779
|
+
const enums = getEnumEntities(ir);
|
|
780
|
+
const { sqlStyle, generateFunctions, exportName, exportStyle, outputDir, header, functionsFile, explicitColumns, } = resolvedConfig;
|
|
781
|
+
// Pre-compute function groupings by return entity name
|
|
782
|
+
// Functions returning entities go in that entity's file; scalars go in functions.ts
|
|
783
|
+
const functionsByEntity = new Map();
|
|
784
|
+
const scalarFunctions = [];
|
|
785
|
+
if (generateFunctions) {
|
|
786
|
+
const { queries, mutations } = getGeneratableFunctions(ir);
|
|
787
|
+
const allFunctions = [...queries, ...mutations];
|
|
788
|
+
for (const fn of allFunctions) {
|
|
789
|
+
const resolved = resolveReturnType(fn, ir);
|
|
790
|
+
if (resolved.returnEntity) {
|
|
791
|
+
const entityName = resolved.returnEntity.name;
|
|
792
|
+
const existing = functionsByEntity.get(entityName) ?? [];
|
|
793
|
+
functionsByEntity.set(entityName, [...existing, fn]);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
scalarFunctions.push(fn);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
291
799
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
800
|
+
getTableEntities(ir)
|
|
801
|
+
.filter(entity => entity.tags.omit !== true)
|
|
802
|
+
.forEach(entity => {
|
|
803
|
+
const entityName = inflection.entityName(entity.pgClass, entity.tags);
|
|
804
|
+
const genCtx = {
|
|
805
|
+
entity,
|
|
806
|
+
enums,
|
|
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" },
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// Import types needed by function args (for functions grouped into this file)
|
|
846
|
+
if (entityFunctions.length > 0) {
|
|
847
|
+
const fnTypeImports = collectFunctionTypeImports(entityFunctions, ir);
|
|
848
|
+
// Remove the entity's own type (already in scope)
|
|
849
|
+
fnTypeImports.delete(entity.name);
|
|
850
|
+
for (const typeName of fnTypeImports) {
|
|
851
|
+
file.import({
|
|
852
|
+
kind: "symbol",
|
|
853
|
+
ref: { capability: "types", entity: typeName },
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
file.ast(conjure.program(...statements)).emit();
|
|
858
|
+
// Collect metadata for QueryArtifact
|
|
859
|
+
const pkField = entity.primaryKey?.columns[0]
|
|
860
|
+
? findRowField(entity, entity.primaryKey.columns[0])
|
|
861
|
+
: undefined;
|
|
862
|
+
const pkType = pkField ? getFieldTypeString(pkField, genCtx) : undefined;
|
|
863
|
+
// Combine CRUD method metadata with entity-function metadata
|
|
864
|
+
const allMethodMetas = [
|
|
865
|
+
...crudMethods.map(m => m.meta),
|
|
866
|
+
...entityFunctions.map(fn => generateFunctionWrapper(fn, ir, sqlStyle).meta),
|
|
867
|
+
];
|
|
868
|
+
// Register entity methods to symbol registry for HTTP providers
|
|
869
|
+
ctx.symbols.registerEntityMethods({
|
|
870
|
+
entity: entityName,
|
|
871
|
+
importPath: filePath,
|
|
872
|
+
pkType,
|
|
873
|
+
hasCompositePk: (entity.primaryKey?.columns.length ?? 0) > 1,
|
|
874
|
+
methods: allMethodMetas.map(m => ({
|
|
875
|
+
name: m.name,
|
|
876
|
+
file: filePath,
|
|
877
|
+
entity: entityName,
|
|
878
|
+
kind: m.kind,
|
|
879
|
+
params: m.params,
|
|
880
|
+
returns: m.returns,
|
|
881
|
+
lookupField: m.lookupField,
|
|
882
|
+
isUniqueLookup: m.isUniqueLookup,
|
|
883
|
+
callSignature: m.callSignature,
|
|
884
|
+
})),
|
|
885
|
+
}, "sql-queries");
|
|
295
886
|
});
|
|
296
|
-
//
|
|
297
|
-
if (
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
887
|
+
// Generate files for composite types that have functions returning them
|
|
888
|
+
if (generateFunctions) {
|
|
889
|
+
const composites = getCompositeEntities(ir);
|
|
890
|
+
for (const composite of composites) {
|
|
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 },
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
file.ast(conjure.program(...statements)).emit();
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
// Generate functions.ts for scalar-returning functions only
|
|
914
|
+
if (generateFunctions && scalarFunctions.length > 0) {
|
|
915
|
+
const filePath = `${outputDir}/${functionsFile}`;
|
|
916
|
+
const methods = scalarFunctions.map(fn => generateFunctionWrapper(fn, ir, sqlStyle));
|
|
917
|
+
// Function wrappers are always flat exports
|
|
918
|
+
const statements = methods.map(m => conjure.export.fn(m.fn));
|
|
919
|
+
const file = ctx.file(filePath);
|
|
920
|
+
// Add user-provided header (must include SQL client import)
|
|
921
|
+
file.header(header);
|
|
922
|
+
// Import types needed by function args
|
|
923
|
+
const fnTypeImports = collectFunctionTypeImports(scalarFunctions, ir);
|
|
924
|
+
for (const typeName of fnTypeImports) {
|
|
301
925
|
file.import({
|
|
302
926
|
kind: "symbol",
|
|
303
|
-
ref: { capability: "types", entity:
|
|
927
|
+
ref: { capability: "types", entity: typeName },
|
|
304
928
|
});
|
|
305
929
|
}
|
|
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
|
|
306
933
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
});
|
|
934
|
+
},
|
|
935
|
+
});
|
|
936
|
+
}
|
|
311
937
|
//# sourceMappingURL=sql-queries.js.map
|