@danielfgray/pg-sourcerer 0.2.1 → 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 +3 -4
- 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 +5 -9
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +27 -29
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +19 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -13
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +39 -9
- 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 +66 -0
- package/dist/lib/conjure.d.ts.map +1 -1
- package/dist/lib/conjure.js +127 -29
- package/dist/lib/conjure.js.map +1 -1
- package/dist/lib/hex.d.ts +10 -3
- package/dist/lib/hex.d.ts.map +1 -1
- package/dist/lib/hex.js +18 -8
- 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 -11
- package/dist/plugins/sql-queries.d.ts.map +1 -1
- package/dist/plugins/sql-queries.js +467 -218
- 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/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 +349 -217
- 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 -951
- 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,951 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Kysely Queries Plugin - Generate type-safe Kysely query functions
|
|
3
|
-
*
|
|
4
|
-
* Generates permission-aware CRUD query functions using Kysely's query builder.
|
|
5
|
-
* Uses object namespace style with explicit `db: Kysely<DB>` first parameter.
|
|
6
|
-
*/
|
|
7
|
-
import { Schema as S } from "effect";
|
|
8
|
-
import { definePlugin } from "../services/plugin.js";
|
|
9
|
-
import { getTableEntities, getEnumEntities, getFunctionEntities, getCompositeEntities } from "../ir/semantic-ir.js";
|
|
10
|
-
import { conjure, cast } from "../lib/conjure.js";
|
|
11
|
-
import { resolveFieldType, tsTypeToAst } from "../lib/field-utils.js";
|
|
12
|
-
import { inflect } from "../services/inflection.js";
|
|
13
|
-
const { ts, b } = conjure;
|
|
14
|
-
const { toExpr } = cast;
|
|
15
|
-
/** Default export name: entityName + methodName (e.g., "UserFindById") */
|
|
16
|
-
const defaultExportName = (entityName, methodName) => `${entityName}${methodName}`;
|
|
17
|
-
/** Default function export name: camelCase of pg function name */
|
|
18
|
-
const defaultFunctionExportName = (pgFunctionName) => inflect.camelCase(pgFunctionName);
|
|
19
|
-
/**
|
|
20
|
-
* Schema for serializable config options (JSON/YAML compatible).
|
|
21
|
-
* Function options are typed separately in KyselyQueriesConfigInput.
|
|
22
|
-
*/
|
|
23
|
-
const KyselyQueriesPluginConfigSchema = S.Struct({
|
|
24
|
-
outputDir: S.optionalWith(S.String, { default: () => "kysely-queries" }),
|
|
25
|
-
/**
|
|
26
|
-
* Path to import DB type from (relative to outputDir).
|
|
27
|
-
* Defaults to "../db.js" which works with kysely-types plugin output.
|
|
28
|
-
* For node16/nodenext module resolution, use ".js" extension even for .ts files.
|
|
29
|
-
*/
|
|
30
|
-
dbTypesPath: S.optionalWith(S.String, { default: () => "../db.js" }),
|
|
31
|
-
/**
|
|
32
|
-
* Whether to call .execute() / .executeTakeFirst() on queries.
|
|
33
|
-
* When true (default), methods return Promise<Row> or Promise<Row[]>.
|
|
34
|
-
* When false, methods return the query builder for further customization.
|
|
35
|
-
*/
|
|
36
|
-
executeQueries: S.optionalWith(S.Boolean, { default: () => true }),
|
|
37
|
-
/**
|
|
38
|
-
* Whether to generate listMany() method for unfiltered table scans.
|
|
39
|
-
* Disabled by default since unfiltered scans don't use indexes.
|
|
40
|
-
* When enabled, generates: listMany(db, limit = 50, offset = 0)
|
|
41
|
-
*/
|
|
42
|
-
generateListMany: S.optionalWith(S.Boolean, { default: () => false }),
|
|
43
|
-
/**
|
|
44
|
-
* Whether to generate function wrappers for stored functions.
|
|
45
|
-
* When true (default), generates queries/mutations namespaces in functions.ts.
|
|
46
|
-
*/
|
|
47
|
-
generateFunctions: S.optionalWith(S.Boolean, { default: () => true }),
|
|
48
|
-
/**
|
|
49
|
-
* Output file name for function wrappers (relative to outputDir).
|
|
50
|
-
*/
|
|
51
|
-
functionsFile: S.optionalWith(S.String, { default: () => "functions.ts" }),
|
|
52
|
-
/**
|
|
53
|
-
* Export name function (validated as Any, properly typed in KyselyQueriesConfigInput)
|
|
54
|
-
*/
|
|
55
|
-
exportName: S.optional(S.Any),
|
|
56
|
-
/**
|
|
57
|
-
* Function export name function (validated as Any, properly typed in KyselyQueriesConfigInput)
|
|
58
|
-
*/
|
|
59
|
-
functionExportName: S.optional(S.Any),
|
|
60
|
-
});
|
|
61
|
-
/**
|
|
62
|
-
* Get the Kysely table interface name from the entity.
|
|
63
|
-
* Uses entity.name which is already PascalCase from inflection (e.g., Users).
|
|
64
|
-
*/
|
|
65
|
-
const getTableTypeName = (entity) => entity.name;
|
|
66
|
-
/**
|
|
67
|
-
* Get the table reference for Kysely queries.
|
|
68
|
-
* Uses schema-qualified name only if the schema is NOT in defaultSchemas.
|
|
69
|
-
* This matches the keys in the DB interface from kysely-types plugin.
|
|
70
|
-
*/
|
|
71
|
-
const getTableRef = (entity, defaultSchemas) => defaultSchemas.includes(entity.schemaName)
|
|
72
|
-
? entity.pgName
|
|
73
|
-
: `${entity.schemaName}.${entity.pgName}`;
|
|
74
|
-
/** Find a field in the row shape by column name */
|
|
75
|
-
const findRowField = (entity, columnName) => entity.shapes.row.fields.find(f => f.columnName === columnName);
|
|
76
|
-
/** Get the TypeScript type AST for a field */
|
|
77
|
-
const getFieldTypeAst = (field, ctx) => {
|
|
78
|
-
if (!field)
|
|
79
|
-
return ts.string();
|
|
80
|
-
const resolved = resolveFieldType(field, ctx.enums, ctx.ir.extensions);
|
|
81
|
-
return resolved.enumDef ? ts.ref(resolved.enumDef.name) : tsTypeToAst(resolved.tsType);
|
|
82
|
-
};
|
|
83
|
-
// ============================================================================
|
|
84
|
-
// FK Semantic Naming Helpers
|
|
85
|
-
// ============================================================================
|
|
86
|
-
/**
|
|
87
|
-
* Find a belongsTo relation that uses the given column as its local FK column.
|
|
88
|
-
* For single-column indexes only.
|
|
89
|
-
*/
|
|
90
|
-
const findRelationForColumn = (entity, columnName) => entity.relations.find(r => r.kind === "belongsTo" && r.columns.length === 1 && r.columns[0]?.local === columnName);
|
|
91
|
-
/**
|
|
92
|
-
* Derive semantic name for an FK-based lookup.
|
|
93
|
-
* Priority: @fieldName tag → column minus _id suffix → target entity name
|
|
94
|
-
*/
|
|
95
|
-
const deriveSemanticName = (relation, columnName) => {
|
|
96
|
-
// 1. Check for @fieldName smart tag
|
|
97
|
-
if (relation.tags.fieldName && typeof relation.tags.fieldName === "string") {
|
|
98
|
-
return relation.tags.fieldName;
|
|
99
|
-
}
|
|
100
|
-
// 2. Strip common FK suffixes from column name
|
|
101
|
-
const suffixes = ["_id", "_fk", "Id", "Fk"];
|
|
102
|
-
for (const suffix of suffixes) {
|
|
103
|
-
if (columnName.endsWith(suffix)) {
|
|
104
|
-
const stripped = columnName.slice(0, -suffix.length);
|
|
105
|
-
if (stripped.length > 0)
|
|
106
|
-
return stripped;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// 3. Fall back to target entity name (lowercased first char)
|
|
110
|
-
const target = relation.targetEntity;
|
|
111
|
-
return target.charAt(0).toLowerCase() + target.slice(1);
|
|
112
|
-
};
|
|
113
|
-
/**
|
|
114
|
-
* Convert to PascalCase for use in method names.
|
|
115
|
-
* Handles snake_case (created_at → CreatedAt) and regular strings.
|
|
116
|
-
*/
|
|
117
|
-
const toPascalCase = (s) => inflect.pascalCase(s);
|
|
118
|
-
// ============================================================================
|
|
119
|
-
// AST Building Helpers
|
|
120
|
-
// ============================================================================
|
|
121
|
-
/** Create identifier */
|
|
122
|
-
const id = (name) => b.identifier(name);
|
|
123
|
-
/** Create member expression: obj.prop */
|
|
124
|
-
const member = (obj, prop) => b.memberExpression(toExpr(obj), id(prop));
|
|
125
|
-
/** Create method call: obj.method(args) */
|
|
126
|
-
const call = (obj, method, args = []) => b.callExpression(member(obj, method), args.map(toExpr));
|
|
127
|
-
/** Create string literal */
|
|
128
|
-
const str = (value) => b.stringLiteral(value);
|
|
129
|
-
/** Create typed parameter: name: Type */
|
|
130
|
-
const typedParam = (name, type) => {
|
|
131
|
-
const param = id(name);
|
|
132
|
-
param.typeAnnotation = b.tsTypeAnnotation(cast.toTSType(type));
|
|
133
|
-
return param;
|
|
134
|
-
};
|
|
135
|
-
/** Create optional typed parameter: name?: Type */
|
|
136
|
-
const optionalTypedParam = (name, type) => {
|
|
137
|
-
const param = typedParam(name, type);
|
|
138
|
-
param.optional = true;
|
|
139
|
-
return param;
|
|
140
|
-
};
|
|
141
|
-
/**
|
|
142
|
-
* Build Kysely query chain starting with db.selectFrom('table')
|
|
143
|
-
*/
|
|
144
|
-
const selectFrom = (tableRef) => call(id("db"), "selectFrom", [str(tableRef)]);
|
|
145
|
-
/**
|
|
146
|
-
* Build Kysely query chain: db.insertInto('table')
|
|
147
|
-
*/
|
|
148
|
-
const insertInto = (tableRef) => call(id("db"), "insertInto", [str(tableRef)]);
|
|
149
|
-
/**
|
|
150
|
-
* Build Kysely query chain: db.updateTable('table')
|
|
151
|
-
*/
|
|
152
|
-
const updateTable = (tableRef) => call(id("db"), "updateTable", [str(tableRef)]);
|
|
153
|
-
/**
|
|
154
|
-
* Build Kysely query chain: db.deleteFrom('table')
|
|
155
|
-
*/
|
|
156
|
-
const deleteFrom = (tableRef) => call(id("db"), "deleteFrom", [str(tableRef)]);
|
|
157
|
-
/**
|
|
158
|
-
* Chain method call onto existing expression
|
|
159
|
-
*/
|
|
160
|
-
const chain = (expr, method, args = []) => call(expr, method, args);
|
|
161
|
-
/**
|
|
162
|
-
* Build arrow function expression: (params) => body
|
|
163
|
-
*/
|
|
164
|
-
const arrowFn = (params, body) => {
|
|
165
|
-
const fn = b.arrowFunctionExpression(params.map(p => p), toExpr(body));
|
|
166
|
-
return fn;
|
|
167
|
-
};
|
|
168
|
-
/**
|
|
169
|
-
* Build object property: key: value
|
|
170
|
-
*/
|
|
171
|
-
const objProp = (key, value) => {
|
|
172
|
-
const prop = b.objectProperty(id(key), toExpr(value));
|
|
173
|
-
return prop;
|
|
174
|
-
};
|
|
175
|
-
// ============================================================================
|
|
176
|
-
// PostgreSQL Type Name to TypeScript Mapping
|
|
177
|
-
// ============================================================================
|
|
178
|
-
/**
|
|
179
|
-
* Map PostgreSQL type name to TypeScript type string.
|
|
180
|
-
* Used for function argument and return type resolution.
|
|
181
|
-
*/
|
|
182
|
-
const pgTypeNameToTs = (typeName) => {
|
|
183
|
-
// Normalize: strip schema prefix if present
|
|
184
|
-
const baseName = typeName.includes(".") ? typeName.split(".").pop() : typeName;
|
|
185
|
-
switch (baseName) {
|
|
186
|
-
// Boolean
|
|
187
|
-
case "bool":
|
|
188
|
-
case "boolean":
|
|
189
|
-
return "boolean";
|
|
190
|
-
// Integer types → number
|
|
191
|
-
case "int2":
|
|
192
|
-
case "smallint":
|
|
193
|
-
case "int4":
|
|
194
|
-
case "integer":
|
|
195
|
-
case "int":
|
|
196
|
-
case "oid":
|
|
197
|
-
case "float4":
|
|
198
|
-
case "real":
|
|
199
|
-
case "float8":
|
|
200
|
-
case "double precision":
|
|
201
|
-
return "number";
|
|
202
|
-
// Big integers/numeric → string (to avoid precision loss)
|
|
203
|
-
case "int8":
|
|
204
|
-
case "bigint":
|
|
205
|
-
case "numeric":
|
|
206
|
-
case "decimal":
|
|
207
|
-
case "money":
|
|
208
|
-
return "string";
|
|
209
|
-
// Text types → string
|
|
210
|
-
case "text":
|
|
211
|
-
case "varchar":
|
|
212
|
-
case "character varying":
|
|
213
|
-
case "char":
|
|
214
|
-
case "character":
|
|
215
|
-
case "bpchar":
|
|
216
|
-
case "name":
|
|
217
|
-
case "xml":
|
|
218
|
-
case "bit":
|
|
219
|
-
case "varbit":
|
|
220
|
-
case "bit varying":
|
|
221
|
-
case "uuid":
|
|
222
|
-
case "inet":
|
|
223
|
-
case "cidr":
|
|
224
|
-
case "macaddr":
|
|
225
|
-
case "macaddr8":
|
|
226
|
-
case "time":
|
|
227
|
-
case "timetz":
|
|
228
|
-
case "time with time zone":
|
|
229
|
-
case "time without time zone":
|
|
230
|
-
case "interval":
|
|
231
|
-
return "string";
|
|
232
|
-
// Date/Time with date component → Date
|
|
233
|
-
case "date":
|
|
234
|
-
case "timestamp":
|
|
235
|
-
case "timestamptz":
|
|
236
|
-
case "timestamp with time zone":
|
|
237
|
-
case "timestamp without time zone":
|
|
238
|
-
return "Date";
|
|
239
|
-
// JSON → unknown
|
|
240
|
-
case "json":
|
|
241
|
-
case "jsonb":
|
|
242
|
-
case "jsonpath":
|
|
243
|
-
return "unknown";
|
|
244
|
-
// Binary → Buffer
|
|
245
|
-
case "bytea":
|
|
246
|
-
return "Buffer";
|
|
247
|
-
// Void
|
|
248
|
-
case "void":
|
|
249
|
-
return "void";
|
|
250
|
-
// Default to unknown
|
|
251
|
-
default:
|
|
252
|
-
return "unknown";
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
/**
|
|
256
|
-
* Check if a function argument type matches a table/view entity (row type argument).
|
|
257
|
-
* Functions with row-type arguments are computed fields (e.g., posts_short_body(posts))
|
|
258
|
-
* and should be excluded from function wrapper generation.
|
|
259
|
-
*/
|
|
260
|
-
const hasRowTypeArg = (arg, ir) => {
|
|
261
|
-
const tableEntities = getTableEntities(ir);
|
|
262
|
-
// Check if arg.typeName matches a table entity's qualified name
|
|
263
|
-
// Format: "schema.tablename" or just "tablename" for public schema
|
|
264
|
-
return tableEntities.some(entity => {
|
|
265
|
-
const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
|
|
266
|
-
return arg.typeName === qualifiedName || arg.typeName === entity.pgName;
|
|
267
|
-
});
|
|
268
|
-
};
|
|
269
|
-
/**
|
|
270
|
-
* Check if a function should be included in generated wrappers.
|
|
271
|
-
*
|
|
272
|
-
* Includes functions that:
|
|
273
|
-
* - Have canExecute permission
|
|
274
|
-
* - Are not trigger functions
|
|
275
|
-
* - Are not from extensions
|
|
276
|
-
* - Are not @omit tagged
|
|
277
|
-
* - Don't have row-type arguments (computed fields)
|
|
278
|
-
*/
|
|
279
|
-
const isGeneratableFunction = (fn, ir) => {
|
|
280
|
-
if (!fn.canExecute)
|
|
281
|
-
return false;
|
|
282
|
-
if (fn.returnTypeName === "trigger")
|
|
283
|
-
return false;
|
|
284
|
-
if (fn.isFromExtension)
|
|
285
|
-
return false;
|
|
286
|
-
if (fn.tags.omit === true)
|
|
287
|
-
return false;
|
|
288
|
-
// Check for row-type args (computed field pattern)
|
|
289
|
-
if (fn.args.some(arg => hasRowTypeArg(arg, ir)))
|
|
290
|
-
return false;
|
|
291
|
-
return true;
|
|
292
|
-
};
|
|
293
|
-
/**
|
|
294
|
-
* Categorize functions by volatility.
|
|
295
|
-
* Volatile functions go in mutations namespace, stable/immutable in queries.
|
|
296
|
-
*/
|
|
297
|
-
const categorizeFunction = (fn) => fn.volatility === "volatile" ? "mutations" : "queries";
|
|
298
|
-
/**
|
|
299
|
-
* Get all generatable functions from the IR, categorized by volatility.
|
|
300
|
-
*/
|
|
301
|
-
const getGeneratableFunctions = (ir) => {
|
|
302
|
-
const all = getFunctionEntities(ir).filter(fn => isGeneratableFunction(fn, ir));
|
|
303
|
-
return {
|
|
304
|
-
queries: all.filter(fn => categorizeFunction(fn) === "queries"),
|
|
305
|
-
mutations: all.filter(fn => categorizeFunction(fn) === "mutations"),
|
|
306
|
-
};
|
|
307
|
-
};
|
|
308
|
-
/**
|
|
309
|
-
* Resolve a function's return type to TypeScript type information.
|
|
310
|
-
*/
|
|
311
|
-
const resolveReturnType = (fn, ir) => {
|
|
312
|
-
const returnTypeName = fn.returnTypeName;
|
|
313
|
-
const isArray = fn.returnsSet;
|
|
314
|
-
// 1. Check if it's a table return type
|
|
315
|
-
const tableEntities = getTableEntities(ir);
|
|
316
|
-
const tableMatch = tableEntities.find(entity => {
|
|
317
|
-
const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
|
|
318
|
-
return returnTypeName === qualifiedName || returnTypeName === entity.pgName;
|
|
319
|
-
});
|
|
320
|
-
if (tableMatch) {
|
|
321
|
-
return {
|
|
322
|
-
tsType: tableMatch.name,
|
|
323
|
-
isArray,
|
|
324
|
-
isScalar: false,
|
|
325
|
-
needsImport: tableMatch.name,
|
|
326
|
-
returnEntity: tableMatch,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
// 2. Check if it's a composite type return
|
|
330
|
-
const compositeEntities = getCompositeEntities(ir);
|
|
331
|
-
const compositeMatch = compositeEntities.find(entity => {
|
|
332
|
-
const qualifiedName = `${entity.schemaName}.${entity.pgName}`;
|
|
333
|
-
return returnTypeName === qualifiedName || returnTypeName === entity.pgName;
|
|
334
|
-
});
|
|
335
|
-
if (compositeMatch) {
|
|
336
|
-
return {
|
|
337
|
-
tsType: compositeMatch.name,
|
|
338
|
-
isArray,
|
|
339
|
-
isScalar: false,
|
|
340
|
-
needsImport: compositeMatch.name,
|
|
341
|
-
returnEntity: compositeMatch,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
// 3. It's a scalar type - map via type name
|
|
345
|
-
// Handle "schema.typename" format by extracting just the type name
|
|
346
|
-
const baseTypeName = returnTypeName.includes(".")
|
|
347
|
-
? returnTypeName.split(".").pop()
|
|
348
|
-
: returnTypeName;
|
|
349
|
-
const tsType = pgTypeNameToTs(baseTypeName);
|
|
350
|
-
return {
|
|
351
|
-
tsType,
|
|
352
|
-
isArray,
|
|
353
|
-
isScalar: true,
|
|
354
|
-
};
|
|
355
|
-
};
|
|
356
|
-
/**
|
|
357
|
-
* Resolve a function argument to TypeScript type information.
|
|
358
|
-
*/
|
|
359
|
-
const resolveArg = (arg, ir) => {
|
|
360
|
-
const typeName = arg.typeName;
|
|
361
|
-
// Check if it's an array type (ends with [])
|
|
362
|
-
const isArrayType = typeName.endsWith("[]");
|
|
363
|
-
const baseTypeName = isArrayType ? typeName.slice(0, -2) : typeName;
|
|
364
|
-
// Check enums
|
|
365
|
-
const enums = getEnumEntities(ir);
|
|
366
|
-
const enumMatch = enums.find(e => {
|
|
367
|
-
const qualifiedName = `${e.schemaName}.${e.pgName}`;
|
|
368
|
-
return baseTypeName === qualifiedName || baseTypeName === e.pgName;
|
|
369
|
-
});
|
|
370
|
-
if (enumMatch) {
|
|
371
|
-
const tsType = isArrayType ? `${enumMatch.name}[]` : enumMatch.name;
|
|
372
|
-
return {
|
|
373
|
-
name: arg.name || "arg",
|
|
374
|
-
tsType,
|
|
375
|
-
isOptional: arg.hasDefault,
|
|
376
|
-
needsImport: enumMatch.name,
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
// Check composites
|
|
380
|
-
const composites = getCompositeEntities(ir);
|
|
381
|
-
const compositeMatch = composites.find(e => {
|
|
382
|
-
const qualifiedName = `${e.schemaName}.${e.pgName}`;
|
|
383
|
-
return baseTypeName === qualifiedName || baseTypeName === e.pgName;
|
|
384
|
-
});
|
|
385
|
-
if (compositeMatch) {
|
|
386
|
-
const tsType = isArrayType ? `${compositeMatch.name}[]` : compositeMatch.name;
|
|
387
|
-
return {
|
|
388
|
-
name: arg.name || "arg",
|
|
389
|
-
tsType,
|
|
390
|
-
isOptional: arg.hasDefault,
|
|
391
|
-
needsImport: compositeMatch.name,
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
// Scalar type - map via type name
|
|
395
|
-
// Handle "schema.typename" format
|
|
396
|
-
const scalarBase = baseTypeName.includes(".")
|
|
397
|
-
? baseTypeName.split(".").pop()
|
|
398
|
-
: baseTypeName;
|
|
399
|
-
const scalarTs = pgTypeNameToTs(scalarBase);
|
|
400
|
-
const tsType = isArrayType ? `${scalarTs}[]` : scalarTs;
|
|
401
|
-
return {
|
|
402
|
-
name: arg.name || "arg",
|
|
403
|
-
tsType,
|
|
404
|
-
isOptional: arg.hasDefault,
|
|
405
|
-
};
|
|
406
|
-
};
|
|
407
|
-
/**
|
|
408
|
-
* Resolve all arguments for a function.
|
|
409
|
-
*/
|
|
410
|
-
const resolveArgs = (fn, ir) => fn.args.map(arg => resolveArg(arg, ir));
|
|
411
|
-
// ============================================================================
|
|
412
|
-
// Function Wrapper AST Generation
|
|
413
|
-
// ============================================================================
|
|
414
|
-
/**
|
|
415
|
-
* Generate a typed parameter with explicit type annotation from type string.
|
|
416
|
-
*/
|
|
417
|
-
const typedParamFromString = (name, typeStr) => {
|
|
418
|
-
const param = id(name);
|
|
419
|
-
// Map type string to AST
|
|
420
|
-
let typeAst;
|
|
421
|
-
switch (typeStr) {
|
|
422
|
-
case "string":
|
|
423
|
-
typeAst = ts.string();
|
|
424
|
-
break;
|
|
425
|
-
case "number":
|
|
426
|
-
typeAst = ts.number();
|
|
427
|
-
break;
|
|
428
|
-
case "boolean":
|
|
429
|
-
typeAst = ts.boolean();
|
|
430
|
-
break;
|
|
431
|
-
case "Date":
|
|
432
|
-
typeAst = ts.ref("Date");
|
|
433
|
-
break;
|
|
434
|
-
case "Buffer":
|
|
435
|
-
typeAst = ts.ref("Buffer");
|
|
436
|
-
break;
|
|
437
|
-
case "unknown":
|
|
438
|
-
typeAst = ts.unknown();
|
|
439
|
-
break;
|
|
440
|
-
case "void":
|
|
441
|
-
typeAst = ts.void();
|
|
442
|
-
break;
|
|
443
|
-
default:
|
|
444
|
-
// Handle array types like "string[]"
|
|
445
|
-
if (typeStr.endsWith("[]")) {
|
|
446
|
-
const elemType = typeStr.slice(0, -2);
|
|
447
|
-
const elemAst = elemType === "string" ? ts.string()
|
|
448
|
-
: elemType === "number" ? ts.number()
|
|
449
|
-
: elemType === "boolean" ? ts.boolean()
|
|
450
|
-
: ts.ref(elemType);
|
|
451
|
-
typeAst = ts.array(elemAst);
|
|
452
|
-
}
|
|
453
|
-
else {
|
|
454
|
-
// Assume it's a type reference (composite, enum, etc.)
|
|
455
|
-
typeAst = ts.ref(typeStr);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
param.typeAnnotation = b.tsTypeAnnotation(cast.toTSType(typeAst));
|
|
459
|
-
return param;
|
|
460
|
-
};
|
|
461
|
-
/**
|
|
462
|
-
* Generate an optional typed parameter with explicit type annotation.
|
|
463
|
-
*/
|
|
464
|
-
const optionalTypedParamFromString = (name, typeStr) => {
|
|
465
|
-
const param = typedParamFromString(name, typeStr);
|
|
466
|
-
param.optional = true;
|
|
467
|
-
return param;
|
|
468
|
-
};
|
|
469
|
-
/**
|
|
470
|
-
* Get the fully qualified function name for use in eb.fn call.
|
|
471
|
-
*/
|
|
472
|
-
const getFunctionQualifiedName = (fn) => `${fn.schemaName}.${fn.pgName}`;
|
|
473
|
-
/**
|
|
474
|
-
* Generate a function wrapper method as an object property.
|
|
475
|
-
*
|
|
476
|
-
* Patterns:
|
|
477
|
-
* - SETOF/table return: db.selectFrom(eb => eb.fn<Type>(...).as('f')).selectAll().execute()
|
|
478
|
-
* - Single row return: db.selectFrom(eb => eb.fn<Type>(...).as('f')).selectAll().executeTakeFirst()
|
|
479
|
-
* - Scalar return: db.selectNoFrom(eb => eb.fn<Type>(...).as('result')).executeTakeFirst().then(r => r?.result)
|
|
480
|
-
*/
|
|
481
|
-
const generateFunctionWrapper = (fn, ir, executeQueries, functionExportName) => {
|
|
482
|
-
const resolvedReturn = resolveReturnType(fn, ir);
|
|
483
|
-
const resolvedArgs = resolveArgs(fn, ir);
|
|
484
|
-
const qualifiedName = getFunctionQualifiedName(fn);
|
|
485
|
-
// Build eb.val(arg) for each argument
|
|
486
|
-
const fnArgs = resolvedArgs.map(arg => call(id("eb"), "val", [id(arg.name)]));
|
|
487
|
-
// Build eb.fn<Type>('schema.fn_name', [args]).as('alias')
|
|
488
|
-
// The type parameter is the return type
|
|
489
|
-
const returnTypeAst = resolvedReturn.isScalar
|
|
490
|
-
? typedParamFromString("_", resolvedReturn.tsType).typeAnnotation.typeAnnotation
|
|
491
|
-
: ts.ref(resolvedReturn.tsType);
|
|
492
|
-
// Create eb.fn with type parameter: eb.fn<Type>
|
|
493
|
-
const fnMember = b.memberExpression(id("eb"), id("fn"));
|
|
494
|
-
const fnWithType = b.tsInstantiationExpression(fnMember, b.tsTypeParameterInstantiation([cast.toTSType(returnTypeAst)]));
|
|
495
|
-
// Call it: eb.fn<Type>(name, args)
|
|
496
|
-
const fnCallBase = b.callExpression(fnWithType, [str(qualifiedName), b.arrayExpression(fnArgs.map(toExpr))]);
|
|
497
|
-
// .as('f') or .as('result') for scalar
|
|
498
|
-
const alias = resolvedReturn.isScalar ? "result" : "f";
|
|
499
|
-
const fnCallWithAlias = call(fnCallBase, "as", [str(alias)]);
|
|
500
|
-
// Arrow function for selectFrom callback: eb => eb.fn<...>(...).as('f')
|
|
501
|
-
const selectCallback = arrowFn([id("eb")], fnCallWithAlias);
|
|
502
|
-
// Build the query chain
|
|
503
|
-
let query;
|
|
504
|
-
if (resolvedReturn.isScalar) {
|
|
505
|
-
// Scalar: db.selectNoFrom(eb => ...).executeTakeFirst()
|
|
506
|
-
// Returns { result: T } | undefined - caller accesses .result
|
|
507
|
-
query = call(id("db"), "selectNoFrom", [selectCallback]);
|
|
508
|
-
if (executeQueries) {
|
|
509
|
-
query = chain(query, "executeTakeFirst");
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
// Table/composite: db.selectFrom(eb => ...).selectAll()
|
|
514
|
-
query = chain(call(id("db"), "selectFrom", [selectCallback]), "selectAll");
|
|
515
|
-
if (executeQueries) {
|
|
516
|
-
// SETOF → .execute(), single row → .executeTakeFirst()
|
|
517
|
-
query = chain(query, resolvedReturn.isArray ? "execute" : "executeTakeFirst");
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
// Build the parameters: (db: Kysely<DB>, arg1: Type1, arg2?: Type2, ...)
|
|
521
|
-
const params = [
|
|
522
|
-
typedParam("db", ts.ref("Kysely", [ts.ref("DB")])),
|
|
523
|
-
...resolvedArgs.map(arg => arg.isOptional
|
|
524
|
-
? optionalTypedParamFromString(arg.name, arg.tsType)
|
|
525
|
-
: typedParamFromString(arg.name, arg.tsType))
|
|
526
|
-
];
|
|
527
|
-
const wrapperFn = arrowFn(params, query);
|
|
528
|
-
const exportName = functionExportName(fn.pgName);
|
|
529
|
-
const constDecl = b.variableDeclaration("const", [
|
|
530
|
-
b.variableDeclarator(id(exportName), wrapperFn)
|
|
531
|
-
]);
|
|
532
|
-
return b.exportNamedDeclaration(constDecl, []);
|
|
533
|
-
};
|
|
534
|
-
/**
|
|
535
|
-
* Collect all type imports needed for function wrappers.
|
|
536
|
-
*/
|
|
537
|
-
const collectFunctionTypeImports = (functions, ir) => {
|
|
538
|
-
const imports = new Set();
|
|
539
|
-
for (const fn of functions) {
|
|
540
|
-
const resolvedReturn = resolveReturnType(fn, ir);
|
|
541
|
-
if (resolvedReturn.needsImport) {
|
|
542
|
-
imports.add(resolvedReturn.needsImport);
|
|
543
|
-
}
|
|
544
|
-
for (const arg of resolveArgs(fn, ir)) {
|
|
545
|
-
if (arg.needsImport) {
|
|
546
|
-
imports.add(arg.needsImport);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
return imports;
|
|
551
|
-
};
|
|
552
|
-
// ============================================================================
|
|
553
|
-
// CRUD Method Generators
|
|
554
|
-
// ============================================================================
|
|
555
|
-
/**
|
|
556
|
-
* Generate findById method:
|
|
557
|
-
* export const UserFindById = (db, id) => db.selectFrom('table').selectAll().where('id', '=', id).executeTakeFirst()
|
|
558
|
-
*/
|
|
559
|
-
const generateFindById = (ctx) => {
|
|
560
|
-
const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
|
|
561
|
-
if (!entity.primaryKey || !entity.permissions.canSelect)
|
|
562
|
-
return undefined;
|
|
563
|
-
const pkColName = entity.primaryKey.columns[0];
|
|
564
|
-
const pkField = findRowField(entity, pkColName);
|
|
565
|
-
if (!pkField)
|
|
566
|
-
return undefined;
|
|
567
|
-
const tableRef = getTableRef(entity, defaultSchemas);
|
|
568
|
-
const fieldName = pkField.name;
|
|
569
|
-
const fieldType = getFieldTypeAst(pkField, ctx);
|
|
570
|
-
// db.selectFrom('table').selectAll().where('col', '=', id)
|
|
571
|
-
let query = chain(chain(selectFrom(tableRef), "selectAll"), "where", [str(pkColName), str("="), id(fieldName)]);
|
|
572
|
-
if (executeQueries) {
|
|
573
|
-
query = chain(query, "executeTakeFirst");
|
|
574
|
-
}
|
|
575
|
-
const fn = arrowFn([typedParam("db", ts.ref("Kysely", [ts.ref("DB")])), typedParam(fieldName, fieldType)], query);
|
|
576
|
-
return conjure.export.const(exportName(entityName, "FindById"), fn);
|
|
577
|
-
};
|
|
578
|
-
/** Default limit for findMany queries */
|
|
579
|
-
const DEFAULT_LIMIT = 50;
|
|
580
|
-
/** Default offset for findMany queries */
|
|
581
|
-
const DEFAULT_OFFSET = 0;
|
|
582
|
-
/**
|
|
583
|
-
* Create a parameter with a default value: name = defaultValue
|
|
584
|
-
* Type is inferred from the default value, no explicit annotation.
|
|
585
|
-
*/
|
|
586
|
-
const paramWithDefault = (name, defaultValue) => b.assignmentPattern(id(name), toExpr(defaultValue));
|
|
587
|
-
/**
|
|
588
|
-
* Generate listMany method with pagination defaults:
|
|
589
|
-
* export const UserListMany = (db, limit = 50, offset = 0) => db.selectFrom('table').selectAll()
|
|
590
|
-
* .limit(limit).offset(offset).execute()
|
|
591
|
-
*/
|
|
592
|
-
const generateListMany = (ctx) => {
|
|
593
|
-
const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
|
|
594
|
-
if (!entity.permissions.canSelect)
|
|
595
|
-
return undefined;
|
|
596
|
-
const tableRef = getTableRef(entity, defaultSchemas);
|
|
597
|
-
// Build query: db.selectFrom('table').selectAll().limit(limit).offset(offset)
|
|
598
|
-
let query = chain(chain(chain(selectFrom(tableRef), "selectAll"), "limit", [id("limit")]), "offset", [id("offset")]);
|
|
599
|
-
// Add .execute() if executeQueries is true
|
|
600
|
-
if (executeQueries) {
|
|
601
|
-
query = chain(query, "execute");
|
|
602
|
-
}
|
|
603
|
-
const fn = arrowFn([
|
|
604
|
-
typedParam("db", ts.ref("Kysely", [ts.ref("DB")])),
|
|
605
|
-
paramWithDefault("limit", b.numericLiteral(DEFAULT_LIMIT)),
|
|
606
|
-
paramWithDefault("offset", b.numericLiteral(DEFAULT_OFFSET)),
|
|
607
|
-
], query);
|
|
608
|
-
return conjure.export.const(exportName(entityName, "ListMany"), fn);
|
|
609
|
-
};
|
|
610
|
-
/**
|
|
611
|
-
* Generate create method:
|
|
612
|
-
* export const UserCreate = (db, data) => db.insertInto('table').values(data).returningAll().executeTakeFirstOrThrow()
|
|
613
|
-
*/
|
|
614
|
-
const generateCreate = (ctx) => {
|
|
615
|
-
const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
|
|
616
|
-
if (!entity.permissions.canInsert)
|
|
617
|
-
return undefined;
|
|
618
|
-
const tableRef = getTableRef(entity, defaultSchemas);
|
|
619
|
-
const tableTypeName = getTableTypeName(entity);
|
|
620
|
-
// db.insertInto('table').values(data).returningAll()
|
|
621
|
-
let query = chain(chain(insertInto(tableRef), "values", [id("data")]), "returningAll");
|
|
622
|
-
if (executeQueries) {
|
|
623
|
-
query = chain(query, "executeTakeFirstOrThrow");
|
|
624
|
-
}
|
|
625
|
-
// Use Insertable<TableTypeName> for the data parameter
|
|
626
|
-
const fn = arrowFn([
|
|
627
|
-
typedParam("db", ts.ref("Kysely", [ts.ref("DB")])),
|
|
628
|
-
typedParam("data", ts.ref("Insertable", [ts.ref(tableTypeName)])),
|
|
629
|
-
], query);
|
|
630
|
-
return conjure.export.const(exportName(entityName, "Create"), fn);
|
|
631
|
-
};
|
|
632
|
-
/**
|
|
633
|
-
* Generate update method:
|
|
634
|
-
* export const UserUpdate = (db, id, data) => db.updateTable('table').set(data).where('id', '=', id).returningAll().executeTakeFirstOrThrow()
|
|
635
|
-
*/
|
|
636
|
-
const generateUpdate = (ctx) => {
|
|
637
|
-
const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
|
|
638
|
-
if (!entity.primaryKey || !entity.permissions.canUpdate)
|
|
639
|
-
return undefined;
|
|
640
|
-
const pkColName = entity.primaryKey.columns[0];
|
|
641
|
-
const pkField = findRowField(entity, pkColName);
|
|
642
|
-
if (!pkField)
|
|
643
|
-
return undefined;
|
|
644
|
-
const tableRef = getTableRef(entity, defaultSchemas);
|
|
645
|
-
const fieldName = pkField.name;
|
|
646
|
-
const fieldType = getFieldTypeAst(pkField, ctx);
|
|
647
|
-
const tableTypeName = getTableTypeName(entity);
|
|
648
|
-
// db.updateTable('table').set(data).where('id', '=', id).returningAll()
|
|
649
|
-
let query = chain(chain(chain(updateTable(tableRef), "set", [id("data")]), "where", [str(pkColName), str("="), id(fieldName)]), "returningAll");
|
|
650
|
-
if (executeQueries) {
|
|
651
|
-
query = chain(query, "executeTakeFirstOrThrow");
|
|
652
|
-
}
|
|
653
|
-
// Use Updateable<TableTypeName> for the data parameter
|
|
654
|
-
const fn = arrowFn([
|
|
655
|
-
typedParam("db", ts.ref("Kysely", [ts.ref("DB")])),
|
|
656
|
-
typedParam(fieldName, fieldType),
|
|
657
|
-
typedParam("data", ts.ref("Updateable", [ts.ref(tableTypeName)])),
|
|
658
|
-
], query);
|
|
659
|
-
return conjure.export.const(exportName(entityName, "Update"), fn);
|
|
660
|
-
};
|
|
661
|
-
/**
|
|
662
|
-
* Generate delete method:
|
|
663
|
-
* export const UserRemove = (db, id) => db.deleteFrom('table').where('id', '=', id).execute()
|
|
664
|
-
*/
|
|
665
|
-
const generateDelete = (ctx) => {
|
|
666
|
-
const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
|
|
667
|
-
if (!entity.primaryKey || !entity.permissions.canDelete)
|
|
668
|
-
return undefined;
|
|
669
|
-
const pkColName = entity.primaryKey.columns[0];
|
|
670
|
-
const pkField = findRowField(entity, pkColName);
|
|
671
|
-
if (!pkField)
|
|
672
|
-
return undefined;
|
|
673
|
-
const tableRef = getTableRef(entity, defaultSchemas);
|
|
674
|
-
const fieldName = pkField.name;
|
|
675
|
-
const fieldType = getFieldTypeAst(pkField, ctx);
|
|
676
|
-
// db.deleteFrom('table').where('id', '=', id)
|
|
677
|
-
let query = chain(deleteFrom(tableRef), "where", [str(pkColName), str("="), id(fieldName)]);
|
|
678
|
-
if (executeQueries) {
|
|
679
|
-
query = chain(query, "execute");
|
|
680
|
-
}
|
|
681
|
-
const fn = arrowFn([typedParam("db", ts.ref("Kysely", [ts.ref("DB")])), typedParam(fieldName, fieldType)], query);
|
|
682
|
-
return conjure.export.const(exportName(entityName, "Remove"), fn);
|
|
683
|
-
};
|
|
684
|
-
/** Generate all CRUD methods for an entity */
|
|
685
|
-
const generateCrudMethods = (ctx) => [
|
|
686
|
-
generateFindById(ctx),
|
|
687
|
-
ctx.generateListMany ? generateListMany(ctx) : undefined,
|
|
688
|
-
generateCreate(ctx),
|
|
689
|
-
generateUpdate(ctx),
|
|
690
|
-
generateDelete(ctx),
|
|
691
|
-
].filter((p) => p != null);
|
|
692
|
-
// ============================================================================
|
|
693
|
-
// Index-based Lookup Functions
|
|
694
|
-
// ============================================================================
|
|
695
|
-
/** Check if an index should generate a lookup function */
|
|
696
|
-
const shouldGenerateLookup = (index) => !index.isPartial &&
|
|
697
|
-
!index.hasExpressions &&
|
|
698
|
-
index.columns.length === 1 &&
|
|
699
|
-
index.method !== "gin" &&
|
|
700
|
-
index.method !== "gist";
|
|
701
|
-
/**
|
|
702
|
-
* Generate the method name portion for an index-based lookup.
|
|
703
|
-
* Uses semantic naming when the column corresponds to an FK relation.
|
|
704
|
-
*/
|
|
705
|
-
const generateLookupMethodName = (entity, index, relation) => {
|
|
706
|
-
const isUnique = isUniqueLookup(entity, index);
|
|
707
|
-
// Uses "FindOneBy" or "FindManyBy" suffix
|
|
708
|
-
const suffix = isUnique ? "FindOneBy" : "FindManyBy";
|
|
709
|
-
// Use semantic name if FK relation exists, otherwise fall back to column name
|
|
710
|
-
const columnName = index.columnNames[0];
|
|
711
|
-
const byName = relation
|
|
712
|
-
? deriveSemanticName(relation, columnName)
|
|
713
|
-
: index.columns[0];
|
|
714
|
-
return `${suffix}${toPascalCase(byName)}`;
|
|
715
|
-
};
|
|
716
|
-
/**
|
|
717
|
-
* Generate a lookup method for a single-column index.
|
|
718
|
-
* Uses semantic parameter naming when the column corresponds to an FK relation.
|
|
719
|
-
*/
|
|
720
|
-
const generateLookupMethod = (index, ctx) => {
|
|
721
|
-
const { entity, executeQueries, defaultSchemas, entityName, exportName } = ctx;
|
|
722
|
-
const tableRef = getTableRef(entity, defaultSchemas);
|
|
723
|
-
const columnName = index.columnNames[0];
|
|
724
|
-
const field = findRowField(entity, columnName);
|
|
725
|
-
const fieldName = field?.name ?? index.columns[0];
|
|
726
|
-
const isUnique = isUniqueLookup(entity, index);
|
|
727
|
-
// Check if this index column corresponds to an FK relation
|
|
728
|
-
const relation = findRelationForColumn(entity, columnName);
|
|
729
|
-
// Use semantic param name if FK relation exists, otherwise use field name
|
|
730
|
-
const paramName = relation
|
|
731
|
-
? deriveSemanticName(relation, columnName)
|
|
732
|
-
: fieldName;
|
|
733
|
-
// For FK columns, use indexed access on Selectable<TableType> to get the unwrapped type
|
|
734
|
-
// (Kysely's Generated<T> types need Selectable to unwrap for use in where clauses)
|
|
735
|
-
// For regular columns, use the field's type directly
|
|
736
|
-
const useSemanticNaming = relation !== undefined && paramName !== fieldName;
|
|
737
|
-
const tableTypeName = getTableTypeName(entity);
|
|
738
|
-
const paramType = useSemanticNaming
|
|
739
|
-
? ts.indexedAccess(ts.ref("Selectable", [ts.ref(tableTypeName)]), ts.literal(fieldName))
|
|
740
|
-
: getFieldTypeAst(field, ctx);
|
|
741
|
-
// db.selectFrom('table').selectAll().where('col', '=', value)
|
|
742
|
-
let query = chain(chain(selectFrom(tableRef), "selectAll"), "where", [str(columnName), str("="), id(paramName)]);
|
|
743
|
-
if (executeQueries) {
|
|
744
|
-
query = chain(query, isUnique ? "executeTakeFirst" : "execute");
|
|
745
|
-
}
|
|
746
|
-
const fn = arrowFn([typedParam("db", ts.ref("Kysely", [ts.ref("DB")])), typedParam(paramName, paramType)], query);
|
|
747
|
-
const methodName = generateLookupMethodName(entity, index, relation);
|
|
748
|
-
return conjure.export.const(exportName(entityName, methodName), fn);
|
|
749
|
-
};
|
|
750
|
-
/**
|
|
751
|
-
* Check if a column is covered by a unique constraint (not just unique index).
|
|
752
|
-
* This helps determine if a non-unique B-tree index on the column still
|
|
753
|
-
* returns at most one row.
|
|
754
|
-
*/
|
|
755
|
-
const columnHasUniqueConstraint = (entity, columnName) => {
|
|
756
|
-
const constraints = entity.pgClass.getConstraints();
|
|
757
|
-
return constraints.some(c => {
|
|
758
|
-
// 'u' = unique constraint, 'p' = primary key
|
|
759
|
-
if (c.contype !== "u" && c.contype !== "p")
|
|
760
|
-
return false;
|
|
761
|
-
// Single-column constraint on our column?
|
|
762
|
-
const conkey = c.conkey ?? [];
|
|
763
|
-
if (conkey.length !== 1)
|
|
764
|
-
return false;
|
|
765
|
-
// Find the attribute with this attnum
|
|
766
|
-
const attrs = entity.pgClass.getAttributes();
|
|
767
|
-
const attr = attrs.find(a => a.attnum === conkey[0]);
|
|
768
|
-
return attr?.attname === columnName;
|
|
769
|
-
});
|
|
770
|
-
};
|
|
771
|
-
/**
|
|
772
|
-
* Determine if a lookup should be treated as unique (returns one row).
|
|
773
|
-
* True if: index is unique, index is primary, OR column has unique constraint.
|
|
774
|
-
*/
|
|
775
|
-
const isUniqueLookup = (entity, index) => {
|
|
776
|
-
if (index.isUnique || index.isPrimary)
|
|
777
|
-
return true;
|
|
778
|
-
// Check if the single column has a unique constraint
|
|
779
|
-
const columnName = index.columnNames[0];
|
|
780
|
-
return columnName ? columnHasUniqueConstraint(entity, columnName) : false;
|
|
781
|
-
};
|
|
782
|
-
/** Generate lookup methods for all eligible indexes, deduplicating by column */
|
|
783
|
-
const generateLookupMethods = (ctx) => {
|
|
784
|
-
const eligibleIndexes = ctx.entity.indexes
|
|
785
|
-
.filter(index => shouldGenerateLookup(index) && !index.isPrimary && ctx.entity.permissions.canSelect);
|
|
786
|
-
// Group by column name, keeping only one index per column
|
|
787
|
-
// Prefer unique indexes, but also consider columns with unique constraints
|
|
788
|
-
const byColumn = new Map();
|
|
789
|
-
for (const index of eligibleIndexes) {
|
|
790
|
-
const columnName = index.columnNames[0];
|
|
791
|
-
const existing = byColumn.get(columnName);
|
|
792
|
-
if (!existing) {
|
|
793
|
-
byColumn.set(columnName, index);
|
|
794
|
-
}
|
|
795
|
-
else {
|
|
796
|
-
// Prefer explicitly unique index over non-unique
|
|
797
|
-
if (index.isUnique && !existing.isUnique) {
|
|
798
|
-
byColumn.set(columnName, index);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
return Array.from(byColumn.values()).map(index => generateLookupMethod(index, ctx));
|
|
803
|
-
};
|
|
804
|
-
// ============================================================================
|
|
805
|
-
// Plugin Definition
|
|
806
|
-
// ============================================================================
|
|
807
|
-
export const kyselyQueriesPlugin = definePlugin({
|
|
808
|
-
name: "kysely-queries",
|
|
809
|
-
provides: ["queries", "queries:kysely"],
|
|
810
|
-
requires: [], // No dependency on types:kysely for now - uses external kysely-codegen types
|
|
811
|
-
configSchema: KyselyQueriesPluginConfigSchema,
|
|
812
|
-
inflection: {
|
|
813
|
-
outputFile: ctx => `${ctx.entityName}.ts`,
|
|
814
|
-
symbolName: (entityName, artifactKind) => `${entityName}${artifactKind}`,
|
|
815
|
-
},
|
|
816
|
-
run: (ctx, rawConfig) => {
|
|
817
|
-
// Resolve config with function defaults
|
|
818
|
-
const config = {
|
|
819
|
-
...rawConfig,
|
|
820
|
-
exportName: rawConfig.exportName ?? defaultExportName,
|
|
821
|
-
functionExportName: rawConfig.functionExportName ?? defaultFunctionExportName,
|
|
822
|
-
};
|
|
823
|
-
const enums = getEnumEntities(ctx.ir);
|
|
824
|
-
const defaultSchemas = ctx.ir.schemas;
|
|
825
|
-
const { dbTypesPath, executeQueries, generateListMany, exportName, functionExportName } = config;
|
|
826
|
-
// Pre-compute function groupings by return entity name
|
|
827
|
-
// Functions returning entities go in that entity's file; scalars go in functions.ts
|
|
828
|
-
const functionsByEntity = new Map();
|
|
829
|
-
const scalarFunctions = [];
|
|
830
|
-
if (config.generateFunctions) {
|
|
831
|
-
const { queries, mutations } = getGeneratableFunctions(ctx.ir);
|
|
832
|
-
const allFunctions = [...queries, ...mutations];
|
|
833
|
-
for (const fn of allFunctions) {
|
|
834
|
-
const resolved = resolveReturnType(fn, ctx.ir);
|
|
835
|
-
if (resolved.returnEntity) {
|
|
836
|
-
const entityName = resolved.returnEntity.name;
|
|
837
|
-
const existing = functionsByEntity.get(entityName) ?? [];
|
|
838
|
-
functionsByEntity.set(entityName, [...existing, fn]);
|
|
839
|
-
}
|
|
840
|
-
else {
|
|
841
|
-
scalarFunctions.push(fn);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
getTableEntities(ctx.ir)
|
|
846
|
-
.filter(entity => entity.tags.omit !== true)
|
|
847
|
-
.forEach(entity => {
|
|
848
|
-
const entityName = ctx.inflection.entityName(entity.pgClass, entity.tags);
|
|
849
|
-
const genCtx = { entity, enums, ir: ctx.ir, defaultSchemas, dbTypesPath, executeQueries, generateListMany, entityName, exportName };
|
|
850
|
-
const crudStatements = [...generateCrudMethods(genCtx), ...generateLookupMethods(genCtx)];
|
|
851
|
-
// Get functions that return this entity
|
|
852
|
-
const entityFunctions = functionsByEntity.get(entity.name) ?? [];
|
|
853
|
-
if (crudStatements.length === 0 && entityFunctions.length === 0)
|
|
854
|
-
return;
|
|
855
|
-
const fileNameCtx = {
|
|
856
|
-
entityName,
|
|
857
|
-
pgName: entity.pgName,
|
|
858
|
-
schema: entity.schemaName,
|
|
859
|
-
inflection: ctx.inflection,
|
|
860
|
-
entity,
|
|
861
|
-
};
|
|
862
|
-
const filePath = `${config.outputDir}/${ctx.pluginInflection.outputFile(fileNameCtx)}`;
|
|
863
|
-
// All statements for the file: CRUD methods + function wrappers
|
|
864
|
-
const statements = [...crudStatements];
|
|
865
|
-
// Add function wrappers as flat exports
|
|
866
|
-
for (const fn of entityFunctions) {
|
|
867
|
-
statements.push(generateFunctionWrapper(fn, ctx.ir, executeQueries, config.functionExportName));
|
|
868
|
-
}
|
|
869
|
-
const file = ctx
|
|
870
|
-
.file(filePath);
|
|
871
|
-
// Import Kysely type and DB from kysely-codegen output
|
|
872
|
-
file.import({ kind: "package", types: ["Kysely"], from: "kysely" });
|
|
873
|
-
file.import({ kind: "relative", types: ["DB"], from: dbTypesPath });
|
|
874
|
-
// Import Insertable/Updateable helper types and table type if we generate create/update
|
|
875
|
-
const tableTypeName = getTableTypeName(entity);
|
|
876
|
-
// Check if any lookup methods use semantic naming (FK with Selectable indexed access)
|
|
877
|
-
const hasSemanticLookups = entity.indexes.some(index => {
|
|
878
|
-
if (!shouldGenerateLookup(index) || index.isPrimary)
|
|
879
|
-
return false;
|
|
880
|
-
const columnName = index.columnNames[0];
|
|
881
|
-
const relation = findRelationForColumn(entity, columnName);
|
|
882
|
-
if (!relation)
|
|
883
|
-
return false;
|
|
884
|
-
const paramName = deriveSemanticName(relation, columnName);
|
|
885
|
-
const field = findRowField(entity, columnName);
|
|
886
|
-
const fieldName = field?.name ?? index.columns[0];
|
|
887
|
-
return paramName !== fieldName;
|
|
888
|
-
});
|
|
889
|
-
// Import table type if needed for Insertable/Updateable or semantic lookups
|
|
890
|
-
const needsTableType = entity.permissions.canInsert || entity.permissions.canUpdate || hasSemanticLookups;
|
|
891
|
-
if (needsTableType) {
|
|
892
|
-
file.import({ kind: "relative", types: [tableTypeName], from: dbTypesPath });
|
|
893
|
-
}
|
|
894
|
-
// Import Selectable if we have semantic lookups (for unwrapping Generated<T>)
|
|
895
|
-
if (hasSemanticLookups) {
|
|
896
|
-
file.import({ kind: "package", types: ["Selectable"], from: "kysely" });
|
|
897
|
-
}
|
|
898
|
-
if (entity.permissions.canInsert) {
|
|
899
|
-
file.import({ kind: "package", types: ["Insertable"], from: "kysely" });
|
|
900
|
-
}
|
|
901
|
-
if (entity.permissions.canUpdate) {
|
|
902
|
-
file.import({ kind: "package", types: ["Updateable"], from: "kysely" });
|
|
903
|
-
}
|
|
904
|
-
// Import types needed by function args (for functions grouped into this file)
|
|
905
|
-
if (entityFunctions.length > 0) {
|
|
906
|
-
const fnTypeImports = collectFunctionTypeImports(entityFunctions, ctx.ir);
|
|
907
|
-
// Remove the entity's own type (already in scope or self-referential)
|
|
908
|
-
fnTypeImports.delete(entity.name);
|
|
909
|
-
if (fnTypeImports.size > 0) {
|
|
910
|
-
file.import({ kind: "relative", types: [...fnTypeImports], from: dbTypesPath });
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
file.ast(conjure.program(...statements)).emit();
|
|
914
|
-
});
|
|
915
|
-
// Generate files for composite types that have functions returning them
|
|
916
|
-
if (config.generateFunctions) {
|
|
917
|
-
const composites = getCompositeEntities(ctx.ir);
|
|
918
|
-
for (const composite of composites) {
|
|
919
|
-
const compositeFunctions = functionsByEntity.get(composite.name) ?? [];
|
|
920
|
-
if (compositeFunctions.length === 0)
|
|
921
|
-
continue;
|
|
922
|
-
const filePath = `${config.outputDir}/${composite.name}.ts`;
|
|
923
|
-
const statements = compositeFunctions.map(fn => generateFunctionWrapper(fn, ctx.ir, executeQueries, config.functionExportName));
|
|
924
|
-
const file = ctx.file(filePath);
|
|
925
|
-
file.import({ kind: "package", types: ["Kysely"], from: "kysely" });
|
|
926
|
-
file.import({ kind: "relative", types: ["DB"], from: dbTypesPath });
|
|
927
|
-
// Import the composite type and any types needed by function args
|
|
928
|
-
const fnTypeImports = collectFunctionTypeImports(compositeFunctions, ctx.ir);
|
|
929
|
-
fnTypeImports.add(composite.name); // Always import the composite type
|
|
930
|
-
file.import({ kind: "relative", types: [...fnTypeImports], from: dbTypesPath });
|
|
931
|
-
file.ast(conjure.program(...statements)).emit();
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
// Generate functions.ts for scalar-returning functions only
|
|
935
|
-
if (config.generateFunctions && scalarFunctions.length > 0) {
|
|
936
|
-
const filePath = `${config.outputDir}/${config.functionsFile}`;
|
|
937
|
-
const statements = scalarFunctions.map(fn => generateFunctionWrapper(fn, ctx.ir, executeQueries, config.functionExportName));
|
|
938
|
-
const file = ctx.file(filePath);
|
|
939
|
-
// Import Kysely type and DB
|
|
940
|
-
file.import({ kind: "package", types: ["Kysely"], from: "kysely" });
|
|
941
|
-
file.import({ kind: "relative", types: ["DB"], from: dbTypesPath });
|
|
942
|
-
// Import any types needed for function args (scalars don't need return type imports)
|
|
943
|
-
const typeImports = collectFunctionTypeImports(scalarFunctions, ctx.ir);
|
|
944
|
-
if (typeImports.size > 0) {
|
|
945
|
-
file.import({ kind: "relative", types: [...typeImports], from: dbTypesPath });
|
|
946
|
-
}
|
|
947
|
-
file.ast(conjure.program(...statements)).emit();
|
|
948
|
-
}
|
|
949
|
-
},
|
|
950
|
-
});
|
|
951
|
-
//# sourceMappingURL=kysely-queries.js.map
|