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