postgresdk 0.18.17 → 0.18.19
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 +150 -81
- package/dist/index.js +145 -76
- package/dist/types.d.ts +1 -0
- package/dist/utils.d.ts +8 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -489,8 +489,8 @@ var require_config = __commonJS(() => {
|
|
|
489
489
|
});
|
|
490
490
|
|
|
491
491
|
// src/utils.ts
|
|
492
|
-
import { mkdir, writeFile, readFile } from "fs/promises";
|
|
493
|
-
import { dirname } from "path";
|
|
492
|
+
import { mkdir, writeFile, readFile, readdir, unlink } from "fs/promises";
|
|
493
|
+
import { dirname, join } from "path";
|
|
494
494
|
import { existsSync } from "fs";
|
|
495
495
|
async function writeFilesIfChanged(files) {
|
|
496
496
|
let written = 0;
|
|
@@ -515,6 +515,28 @@ async function ensureDirs(dirs) {
|
|
|
515
515
|
for (const d of dirs)
|
|
516
516
|
await mkdir(d, { recursive: true });
|
|
517
517
|
}
|
|
518
|
+
async function deleteStaleFiles(generatedPaths, dirsToScan) {
|
|
519
|
+
let deleted = 0;
|
|
520
|
+
const filesDeleted = [];
|
|
521
|
+
for (const dir of dirsToScan) {
|
|
522
|
+
if (!existsSync(dir))
|
|
523
|
+
continue;
|
|
524
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
525
|
+
for (const entry of entries) {
|
|
526
|
+
if (!entry.isFile())
|
|
527
|
+
continue;
|
|
528
|
+
if (!/\.(ts|md|yml|sh)$/.test(entry.name))
|
|
529
|
+
continue;
|
|
530
|
+
const fullPath = join(dir, entry.name);
|
|
531
|
+
if (!generatedPaths.has(fullPath)) {
|
|
532
|
+
await unlink(fullPath);
|
|
533
|
+
deleted++;
|
|
534
|
+
filesDeleted.push(fullPath);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return { deleted, filesDeleted };
|
|
539
|
+
}
|
|
518
540
|
var pascal = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
|
519
541
|
var init_utils = () => {};
|
|
520
542
|
|
|
@@ -2559,7 +2581,7 @@ __export(exports_cli_pull, {
|
|
|
2559
2581
|
pullCommand: () => pullCommand
|
|
2560
2582
|
});
|
|
2561
2583
|
import { writeFile as writeFile2, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
2562
|
-
import { join as
|
|
2584
|
+
import { join as join3, dirname as dirname3, resolve as resolve2 } from "path";
|
|
2563
2585
|
import { existsSync as existsSync4 } from "fs";
|
|
2564
2586
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
2565
2587
|
async function pullCommand(args) {
|
|
@@ -2645,7 +2667,7 @@ Options:`);
|
|
|
2645
2667
|
let filesUnchanged = 0;
|
|
2646
2668
|
const changedFiles = [];
|
|
2647
2669
|
for (const [path, content] of Object.entries(sdk.files)) {
|
|
2648
|
-
const fullPath =
|
|
2670
|
+
const fullPath = join3(config.output, path);
|
|
2649
2671
|
await mkdir2(dirname3(fullPath), { recursive: true });
|
|
2650
2672
|
let shouldWrite = true;
|
|
2651
2673
|
if (existsSync4(fullPath)) {
|
|
@@ -2662,7 +2684,7 @@ Options:`);
|
|
|
2662
2684
|
console.log(` ✓ ${path}`);
|
|
2663
2685
|
}
|
|
2664
2686
|
}
|
|
2665
|
-
const metadataPath =
|
|
2687
|
+
const metadataPath = join3(config.output, ".postgresdk.json");
|
|
2666
2688
|
const metadata = {
|
|
2667
2689
|
version: sdk.version,
|
|
2668
2690
|
pulledFrom: config.from
|
|
@@ -2692,7 +2714,7 @@ var init_cli_pull = () => {};
|
|
|
2692
2714
|
|
|
2693
2715
|
// src/index.ts
|
|
2694
2716
|
var import_config = __toESM(require_config(), 1);
|
|
2695
|
-
import { join, relative, dirname as dirname2 } from "node:path";
|
|
2717
|
+
import { join as join2, relative, dirname as dirname2 } from "node:path";
|
|
2696
2718
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
2697
2719
|
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
2698
2720
|
|
|
@@ -3607,6 +3629,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
3607
3629
|
where?: Where<Select${Type}>;
|
|
3608
3630
|
orderBy?: string | string[];
|
|
3609
3631
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3632
|
+
distinctOn?: string | string[];
|
|
3610
3633
|
${paramName}?: {
|
|
3611
3634
|
select?: string[];
|
|
3612
3635
|
exclude?: string[];
|
|
@@ -3639,6 +3662,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
3639
3662
|
where?: Where<Select${Type}>;
|
|
3640
3663
|
orderBy?: string | string[];
|
|
3641
3664
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3665
|
+
distinctOn?: string | string[];
|
|
3642
3666
|
${includeParams};
|
|
3643
3667
|
}`;
|
|
3644
3668
|
} else if (pattern.type === "nested" && pattern.nestedKey) {
|
|
@@ -3654,6 +3678,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
3654
3678
|
where?: Where<Select${Type}>;
|
|
3655
3679
|
orderBy?: string | string[];
|
|
3656
3680
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3681
|
+
distinctOn?: string | string[];
|
|
3657
3682
|
${paramName}?: {
|
|
3658
3683
|
select?: string[];
|
|
3659
3684
|
exclude?: string[];
|
|
@@ -6216,6 +6241,12 @@ function getVectorDistanceOperator(metric?: string): string {
|
|
|
6216
6241
|
}
|
|
6217
6242
|
}
|
|
6218
6243
|
|
|
6244
|
+
/** Builds a SQL ORDER BY clause from parallel cols/dirs arrays. Returns "" when cols is empty. */
|
|
6245
|
+
function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
|
|
6246
|
+
if (cols.length === 0) return "";
|
|
6247
|
+
return \`ORDER BY \${cols.map((c, i) => \`"\${c}" \${(dirs[i] ?? "asc").toUpperCase()}\`).join(", ")}\`;
|
|
6248
|
+
}
|
|
6249
|
+
|
|
6219
6250
|
/**
|
|
6220
6251
|
* LIST operation - Get multiple records with optional filters and vector search
|
|
6221
6252
|
*/
|
|
@@ -6244,6 +6275,17 @@ export async function listRecords(
|
|
|
6244
6275
|
const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
|
|
6245
6276
|
const _distinctOnColsSQL = distinctCols ? distinctCols.map(c => '"' + c + '"').join(', ') : '';
|
|
6246
6277
|
|
|
6278
|
+
// Pre-compute user order cols (reused in ORDER BY block and useSubquery check)
|
|
6279
|
+
const userOrderCols: string[] = orderBy ? (Array.isArray(orderBy) ? orderBy : [orderBy]) : [];
|
|
6280
|
+
|
|
6281
|
+
// Auto-detect subquery form: needed when distinctOn is set AND the caller wants to order
|
|
6282
|
+
// by a column outside of distinctOn (inline DISTINCT ON can't satisfy that without silently
|
|
6283
|
+
// overriding the requested ordering). Vector search always stays inline.
|
|
6284
|
+
const useSubquery: boolean =
|
|
6285
|
+
distinctCols !== null &&
|
|
6286
|
+
!vector &&
|
|
6287
|
+
userOrderCols.some(col => !distinctCols.includes(col));
|
|
6288
|
+
|
|
6247
6289
|
// Get distance operator if vector search
|
|
6248
6290
|
const distanceOp = vector ? getVectorDistanceOperator(vector.metric) : "";
|
|
6249
6291
|
|
|
@@ -6309,41 +6351,35 @@ export async function listRecords(
|
|
|
6309
6351
|
|
|
6310
6352
|
// Build ORDER BY clause
|
|
6311
6353
|
let orderBySQL = "";
|
|
6354
|
+
const userDirs: ("asc" | "desc")[] = userOrderCols.length > 0
|
|
6355
|
+
? (Array.isArray(order) ? order : (order ? Array(userOrderCols.length).fill(order) : Array(userOrderCols.length).fill("asc")))
|
|
6356
|
+
: [];
|
|
6357
|
+
|
|
6312
6358
|
if (vector) {
|
|
6313
|
-
//
|
|
6359
|
+
// Vector search always orders by distance; DISTINCT ON + vector stays inline
|
|
6314
6360
|
orderBySQL = \`ORDER BY "\${vector.field}" \${distanceOp} ($1)::vector\`;
|
|
6315
|
-
} else {
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6361
|
+
} else if (useSubquery) {
|
|
6362
|
+
// Subquery form: outer query gets the user's full ORDER BY.
|
|
6363
|
+
// Inner query only needs to satisfy PG's DISTINCT ON prefix requirement (built at query assembly).
|
|
6364
|
+
orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
|
|
6365
|
+
} else if (distinctCols) {
|
|
6366
|
+
// Inline DISTINCT ON: prepend distinctCols as the leftmost ORDER BY prefix (PG requirement)
|
|
6321
6367
|
const finalCols: string[] = [];
|
|
6322
|
-
const finalDirs:
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
const userIdx = userCols.indexOf(col);
|
|
6328
|
-
finalCols.push(col);
|
|
6329
|
-
finalDirs.push(userIdx >= 0 ? (userDirs[userIdx] || "asc") : "asc");
|
|
6330
|
-
}
|
|
6331
|
-
// Append remaining user-specified cols not already covered by distinctOn
|
|
6332
|
-
for (let i = 0; i < userCols.length; i++) {
|
|
6333
|
-
if (!distinctCols.includes(userCols[i]!)) {
|
|
6334
|
-
finalCols.push(userCols[i]!);
|
|
6335
|
-
finalDirs.push(userDirs[i] || "asc");
|
|
6336
|
-
}
|
|
6337
|
-
}
|
|
6338
|
-
} else {
|
|
6339
|
-
finalCols.push(...userCols);
|
|
6340
|
-
finalDirs.push(...userDirs.map(d => d || "asc"));
|
|
6368
|
+
const finalDirs: ("asc" | "desc")[] = [];
|
|
6369
|
+
for (const col of distinctCols) {
|
|
6370
|
+
const userIdx = userOrderCols.indexOf(col);
|
|
6371
|
+
finalCols.push(col);
|
|
6372
|
+
finalDirs.push(userIdx >= 0 ? (userDirs[userIdx] ?? "asc") : "asc");
|
|
6341
6373
|
}
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6374
|
+
for (let i = 0; i < userOrderCols.length; i++) {
|
|
6375
|
+
if (!distinctCols.includes(userOrderCols[i]!)) {
|
|
6376
|
+
finalCols.push(userOrderCols[i]!);
|
|
6377
|
+
finalDirs.push(userDirs[i] ?? "asc");
|
|
6378
|
+
}
|
|
6346
6379
|
}
|
|
6380
|
+
orderBySQL = buildOrderBySQL(finalCols, finalDirs);
|
|
6381
|
+
} else {
|
|
6382
|
+
orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
|
|
6347
6383
|
}
|
|
6348
6384
|
|
|
6349
6385
|
// Add limit and offset params
|
|
@@ -6360,7 +6396,15 @@ export async function listRecords(
|
|
|
6360
6396
|
const total = parseInt(countResult.rows[0].count, 10);
|
|
6361
6397
|
|
|
6362
6398
|
// Get paginated data
|
|
6363
|
-
|
|
6399
|
+
let text: string;
|
|
6400
|
+
if (useSubquery) {
|
|
6401
|
+
// Inner query: DISTINCT ON with only the distinctCols ORDER BY prefix (PG requirement).
|
|
6402
|
+
// Outer query: free ORDER BY from the user's full orderBy list, plus LIMIT/OFFSET.
|
|
6403
|
+
const innerQuery = \`SELECT DISTINCT ON (\${_distinctOnColsSQL}) \${baseColumns} FROM "\${ctx.table}" \${whereSQL} ORDER BY \${_distinctOnColsSQL}\`;
|
|
6404
|
+
text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
6405
|
+
} else {
|
|
6406
|
+
text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
6407
|
+
}
|
|
6364
6408
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
6365
6409
|
|
|
6366
6410
|
const { rows } = await ctx.pg.query(text, allParams);
|
|
@@ -7247,7 +7291,7 @@ init_emit_sdk_contract();
|
|
|
7247
7291
|
init_utils();
|
|
7248
7292
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
7249
7293
|
var __dirname2 = dirname2(__filename2);
|
|
7250
|
-
var { version: CLI_VERSION } = JSON.parse(readFileSync(
|
|
7294
|
+
var { version: CLI_VERSION } = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
|
|
7251
7295
|
async function generate(configPath) {
|
|
7252
7296
|
if (!existsSync2(configPath)) {
|
|
7253
7297
|
throw new Error(`Config file not found: ${configPath}
|
|
@@ -7280,26 +7324,26 @@ async function generate(configPath) {
|
|
|
7280
7324
|
const sameDirectory = serverDir === originalClientDir;
|
|
7281
7325
|
let clientDir = originalClientDir;
|
|
7282
7326
|
if (sameDirectory) {
|
|
7283
|
-
clientDir =
|
|
7327
|
+
clientDir = join2(originalClientDir, "sdk");
|
|
7284
7328
|
}
|
|
7285
7329
|
const serverFramework = cfg.serverFramework || "hono";
|
|
7286
7330
|
const generateTests = cfg.tests?.generate ?? false;
|
|
7287
7331
|
const originalTestDir = cfg.tests?.output || "./api/tests";
|
|
7288
7332
|
let testDir = originalTestDir;
|
|
7289
7333
|
if (generateTests && (originalTestDir === serverDir || originalTestDir === originalClientDir)) {
|
|
7290
|
-
testDir =
|
|
7334
|
+
testDir = join2(originalTestDir, "tests");
|
|
7291
7335
|
}
|
|
7292
7336
|
const testFramework = cfg.tests?.framework || "vitest";
|
|
7293
7337
|
console.log("\uD83D\uDCC1 Creating directories...");
|
|
7294
7338
|
const dirs = [
|
|
7295
7339
|
serverDir,
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7340
|
+
join2(serverDir, "types"),
|
|
7341
|
+
join2(serverDir, "zod"),
|
|
7342
|
+
join2(serverDir, "routes"),
|
|
7299
7343
|
clientDir,
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7344
|
+
join2(clientDir, "types"),
|
|
7345
|
+
join2(clientDir, "zod"),
|
|
7346
|
+
join2(clientDir, "params")
|
|
7303
7347
|
];
|
|
7304
7348
|
if (generateTests) {
|
|
7305
7349
|
dirs.push(testDir);
|
|
@@ -7307,28 +7351,28 @@ async function generate(configPath) {
|
|
|
7307
7351
|
await ensureDirs(dirs);
|
|
7308
7352
|
const files = [];
|
|
7309
7353
|
const includeSpec = emitIncludeSpec(graph);
|
|
7310
|
-
files.push({ path:
|
|
7311
|
-
files.push({ path:
|
|
7354
|
+
files.push({ path: join2(serverDir, "include-spec.ts"), content: includeSpec });
|
|
7355
|
+
files.push({ path: join2(clientDir, "include-spec.ts"), content: includeSpec });
|
|
7312
7356
|
const includeResolver = emitIncludeResolver(graph, cfg.useJsExtensions);
|
|
7313
|
-
files.push({ path:
|
|
7314
|
-
files.push({ path:
|
|
7315
|
-
files.push({ path:
|
|
7316
|
-
files.push({ path:
|
|
7317
|
-
files.push({ path:
|
|
7357
|
+
files.push({ path: join2(clientDir, "include-resolver.ts"), content: includeResolver });
|
|
7358
|
+
files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
|
|
7359
|
+
files.push({ path: join2(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
|
|
7360
|
+
files.push({ path: join2(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
7361
|
+
files.push({ path: join2(clientDir, "where-types.ts"), content: emitWhereTypes() });
|
|
7318
7362
|
files.push({
|
|
7319
|
-
path:
|
|
7363
|
+
path: join2(serverDir, "include-builder.ts"),
|
|
7320
7364
|
content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
|
|
7321
7365
|
});
|
|
7322
7366
|
files.push({
|
|
7323
|
-
path:
|
|
7367
|
+
path: join2(serverDir, "include-loader.ts"),
|
|
7324
7368
|
content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
|
|
7325
7369
|
});
|
|
7326
|
-
files.push({ path:
|
|
7370
|
+
files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
|
|
7327
7371
|
if (getAuthStrategy(normalizedAuth) !== "none") {
|
|
7328
|
-
files.push({ path:
|
|
7372
|
+
files.push({ path: join2(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
|
|
7329
7373
|
}
|
|
7330
7374
|
files.push({
|
|
7331
|
-
path:
|
|
7375
|
+
path: join2(serverDir, "core", "operations.ts"),
|
|
7332
7376
|
content: emitCoreOperations()
|
|
7333
7377
|
});
|
|
7334
7378
|
if (process.env.SDK_DEBUG) {
|
|
@@ -7337,13 +7381,13 @@ async function generate(configPath) {
|
|
|
7337
7381
|
for (const table of Object.values(model.tables)) {
|
|
7338
7382
|
const numericMode = cfg.numericMode ?? "auto";
|
|
7339
7383
|
const typesSrc = emitTypes(table, { numericMode }, model.enums);
|
|
7340
|
-
files.push({ path:
|
|
7341
|
-
files.push({ path:
|
|
7384
|
+
files.push({ path: join2(serverDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
7385
|
+
files.push({ path: join2(clientDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
7342
7386
|
const zodSrc = emitZod(table, { numericMode }, model.enums);
|
|
7343
|
-
files.push({ path:
|
|
7344
|
-
files.push({ path:
|
|
7387
|
+
files.push({ path: join2(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
7388
|
+
files.push({ path: join2(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
7345
7389
|
const paramsZodSrc = emitParamsZod(table, graph);
|
|
7346
|
-
files.push({ path:
|
|
7390
|
+
files.push({ path: join2(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
|
|
7347
7391
|
let routeContent;
|
|
7348
7392
|
if (serverFramework === "hono") {
|
|
7349
7393
|
routeContent = emitHonoRoutes(table, graph, {
|
|
@@ -7357,11 +7401,11 @@ async function generate(configPath) {
|
|
|
7357
7401
|
throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
|
|
7358
7402
|
}
|
|
7359
7403
|
files.push({
|
|
7360
|
-
path:
|
|
7404
|
+
path: join2(serverDir, "routes", `${table.name}.ts`),
|
|
7361
7405
|
content: routeContent
|
|
7362
7406
|
});
|
|
7363
7407
|
files.push({
|
|
7364
|
-
path:
|
|
7408
|
+
path: join2(clientDir, `${table.name}.ts`),
|
|
7365
7409
|
content: emitClient(table, graph, {
|
|
7366
7410
|
useJsExtensions: cfg.useJsExtensionsClient,
|
|
7367
7411
|
includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
|
|
@@ -7370,12 +7414,12 @@ async function generate(configPath) {
|
|
|
7370
7414
|
});
|
|
7371
7415
|
}
|
|
7372
7416
|
files.push({
|
|
7373
|
-
path:
|
|
7417
|
+
path: join2(clientDir, "index.ts"),
|
|
7374
7418
|
content: emitClientIndex(Object.values(model.tables), cfg.useJsExtensionsClient, graph, { maxDepth: cfg.includeMethodsDepth ?? 2, skipJunctionTables: cfg.skipJunctionTables ?? true })
|
|
7375
7419
|
});
|
|
7376
7420
|
if (serverFramework === "hono") {
|
|
7377
7421
|
files.push({
|
|
7378
|
-
path:
|
|
7422
|
+
path: join2(serverDir, "router.ts"),
|
|
7379
7423
|
content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions, cfg.pullToken)
|
|
7380
7424
|
});
|
|
7381
7425
|
}
|
|
@@ -7385,63 +7429,88 @@ async function generate(configPath) {
|
|
|
7385
7429
|
}
|
|
7386
7430
|
const contract = generateUnifiedContract2(model, cfg, graph);
|
|
7387
7431
|
files.push({
|
|
7388
|
-
path:
|
|
7432
|
+
path: join2(serverDir, "CONTRACT.md"),
|
|
7389
7433
|
content: generateUnifiedContractMarkdown2(contract)
|
|
7390
7434
|
});
|
|
7391
7435
|
files.push({
|
|
7392
|
-
path:
|
|
7436
|
+
path: join2(clientDir, "CONTRACT.md"),
|
|
7393
7437
|
content: generateUnifiedContractMarkdown2(contract)
|
|
7394
7438
|
});
|
|
7395
7439
|
const contractCode = emitUnifiedContract(model, cfg, graph);
|
|
7396
7440
|
files.push({
|
|
7397
|
-
path:
|
|
7441
|
+
path: join2(serverDir, "contract.ts"),
|
|
7398
7442
|
content: contractCode
|
|
7399
7443
|
});
|
|
7400
7444
|
const clientFiles = files.filter((f) => {
|
|
7401
7445
|
return f.path.includes(clientDir);
|
|
7402
7446
|
});
|
|
7403
7447
|
files.push({
|
|
7404
|
-
path:
|
|
7448
|
+
path: join2(serverDir, "sdk-bundle.ts"),
|
|
7405
7449
|
content: emitSdkBundle(clientFiles, clientDir, CLI_VERSION)
|
|
7406
7450
|
});
|
|
7407
7451
|
if (generateTests) {
|
|
7408
7452
|
console.log("\uD83E\uDDEA Generating tests...");
|
|
7409
7453
|
const relativeClientPath = relative(testDir, clientDir);
|
|
7410
7454
|
files.push({
|
|
7411
|
-
path:
|
|
7455
|
+
path: join2(testDir, "setup.ts"),
|
|
7412
7456
|
content: emitTestSetup(relativeClientPath, testFramework)
|
|
7413
7457
|
});
|
|
7414
7458
|
files.push({
|
|
7415
|
-
path:
|
|
7459
|
+
path: join2(testDir, "docker-compose.yml"),
|
|
7416
7460
|
content: emitDockerCompose()
|
|
7417
7461
|
});
|
|
7418
7462
|
files.push({
|
|
7419
|
-
path:
|
|
7463
|
+
path: join2(testDir, "run-tests.sh"),
|
|
7420
7464
|
content: emitTestScript(testFramework, testDir)
|
|
7421
7465
|
});
|
|
7422
7466
|
files.push({
|
|
7423
|
-
path:
|
|
7467
|
+
path: join2(testDir, ".gitignore"),
|
|
7424
7468
|
content: emitTestGitignore()
|
|
7425
7469
|
});
|
|
7426
7470
|
if (testFramework === "vitest") {
|
|
7427
7471
|
files.push({
|
|
7428
|
-
path:
|
|
7472
|
+
path: join2(testDir, "vitest.config.ts"),
|
|
7429
7473
|
content: emitVitestConfig()
|
|
7430
7474
|
});
|
|
7431
7475
|
}
|
|
7432
7476
|
for (const table of Object.values(model.tables)) {
|
|
7433
7477
|
files.push({
|
|
7434
|
-
path:
|
|
7478
|
+
path: join2(testDir, `${table.name}.test.ts`),
|
|
7435
7479
|
content: emitTableTest(table, model, relativeClientPath, testFramework)
|
|
7436
7480
|
});
|
|
7437
7481
|
}
|
|
7438
7482
|
}
|
|
7439
7483
|
console.log("✍️ Writing files...");
|
|
7440
7484
|
const writeResult = await writeFilesIfChanged(files);
|
|
7441
|
-
|
|
7485
|
+
let deleteResult = { deleted: 0, filesDeleted: [] };
|
|
7486
|
+
if (cfg.clean !== false) {
|
|
7487
|
+
const dirsToScan = [
|
|
7488
|
+
serverDir,
|
|
7489
|
+
join2(serverDir, "types"),
|
|
7490
|
+
join2(serverDir, "zod"),
|
|
7491
|
+
join2(serverDir, "routes"),
|
|
7492
|
+
join2(serverDir, "core"),
|
|
7493
|
+
clientDir,
|
|
7494
|
+
join2(clientDir, "types"),
|
|
7495
|
+
join2(clientDir, "zod"),
|
|
7496
|
+
join2(clientDir, "params")
|
|
7497
|
+
];
|
|
7498
|
+
if (generateTests)
|
|
7499
|
+
dirsToScan.push(testDir);
|
|
7500
|
+
const generatedPaths = new Set(files.map((f) => f.path));
|
|
7501
|
+
deleteResult = await deleteStaleFiles(generatedPaths, dirsToScan);
|
|
7502
|
+
}
|
|
7503
|
+
if (writeResult.written === 0 && deleteResult.deleted === 0) {
|
|
7442
7504
|
console.log(`✅ All ${writeResult.unchanged} files up-to-date (no changes)`);
|
|
7443
7505
|
} else {
|
|
7444
|
-
|
|
7506
|
+
const parts = [];
|
|
7507
|
+
if (writeResult.written > 0)
|
|
7508
|
+
parts.push(`updated ${writeResult.written} files`);
|
|
7509
|
+
if (deleteResult.deleted > 0)
|
|
7510
|
+
parts.push(`deleted ${deleteResult.deleted} stale files`);
|
|
7511
|
+
if (writeResult.unchanged > 0)
|
|
7512
|
+
parts.push(`${writeResult.unchanged} unchanged`);
|
|
7513
|
+
console.log(`✅ ${parts.join(", ")}`);
|
|
7445
7514
|
}
|
|
7446
7515
|
console.log(` Server: ${serverDir}`);
|
|
7447
7516
|
console.log(` Client: ${sameDirectory ? clientDir + " (in sdk subdir due to same output dir)" : clientDir}`);
|
|
@@ -7480,10 +7549,10 @@ var import_config2 = __toESM(require_config(), 1);
|
|
|
7480
7549
|
import { resolve as resolve3 } from "node:path";
|
|
7481
7550
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
7482
7551
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7483
|
-
import { dirname as dirname4, join as
|
|
7552
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
7484
7553
|
var __filename3 = fileURLToPath2(import.meta.url);
|
|
7485
7554
|
var __dirname3 = dirname4(__filename3);
|
|
7486
|
-
var packageJson = JSON.parse(readFileSync3(
|
|
7555
|
+
var packageJson = JSON.parse(readFileSync3(join4(__dirname3, "../package.json"), "utf-8"));
|
|
7487
7556
|
var VERSION = packageJson.version;
|
|
7488
7557
|
var args = process.argv.slice(2);
|
|
7489
7558
|
var command = args[0];
|
package/dist/index.js
CHANGED
|
@@ -488,8 +488,8 @@ var require_config = __commonJS(() => {
|
|
|
488
488
|
});
|
|
489
489
|
|
|
490
490
|
// src/utils.ts
|
|
491
|
-
import { mkdir, writeFile, readFile } from "fs/promises";
|
|
492
|
-
import { dirname } from "path";
|
|
491
|
+
import { mkdir, writeFile, readFile, readdir, unlink } from "fs/promises";
|
|
492
|
+
import { dirname, join } from "path";
|
|
493
493
|
import { existsSync } from "fs";
|
|
494
494
|
async function writeFilesIfChanged(files) {
|
|
495
495
|
let written = 0;
|
|
@@ -514,6 +514,28 @@ async function ensureDirs(dirs) {
|
|
|
514
514
|
for (const d of dirs)
|
|
515
515
|
await mkdir(d, { recursive: true });
|
|
516
516
|
}
|
|
517
|
+
async function deleteStaleFiles(generatedPaths, dirsToScan) {
|
|
518
|
+
let deleted = 0;
|
|
519
|
+
const filesDeleted = [];
|
|
520
|
+
for (const dir of dirsToScan) {
|
|
521
|
+
if (!existsSync(dir))
|
|
522
|
+
continue;
|
|
523
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
524
|
+
for (const entry of entries) {
|
|
525
|
+
if (!entry.isFile())
|
|
526
|
+
continue;
|
|
527
|
+
if (!/\.(ts|md|yml|sh)$/.test(entry.name))
|
|
528
|
+
continue;
|
|
529
|
+
const fullPath = join(dir, entry.name);
|
|
530
|
+
if (!generatedPaths.has(fullPath)) {
|
|
531
|
+
await unlink(fullPath);
|
|
532
|
+
deleted++;
|
|
533
|
+
filesDeleted.push(fullPath);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return { deleted, filesDeleted };
|
|
538
|
+
}
|
|
517
539
|
var pascal = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
|
518
540
|
var init_utils = () => {};
|
|
519
541
|
|
|
@@ -1731,7 +1753,7 @@ var init_emit_sdk_contract = __esm(() => {
|
|
|
1731
1753
|
|
|
1732
1754
|
// src/index.ts
|
|
1733
1755
|
var import_config = __toESM(require_config(), 1);
|
|
1734
|
-
import { join, relative, dirname as dirname2 } from "node:path";
|
|
1756
|
+
import { join as join2, relative, dirname as dirname2 } from "node:path";
|
|
1735
1757
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
1736
1758
|
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
1737
1759
|
|
|
@@ -2646,6 +2668,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2646
2668
|
where?: Where<Select${Type}>;
|
|
2647
2669
|
orderBy?: string | string[];
|
|
2648
2670
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2671
|
+
distinctOn?: string | string[];
|
|
2649
2672
|
${paramName}?: {
|
|
2650
2673
|
select?: string[];
|
|
2651
2674
|
exclude?: string[];
|
|
@@ -2678,6 +2701,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2678
2701
|
where?: Where<Select${Type}>;
|
|
2679
2702
|
orderBy?: string | string[];
|
|
2680
2703
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2704
|
+
distinctOn?: string | string[];
|
|
2681
2705
|
${includeParams};
|
|
2682
2706
|
}`;
|
|
2683
2707
|
} else if (pattern.type === "nested" && pattern.nestedKey) {
|
|
@@ -2693,6 +2717,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2693
2717
|
where?: Where<Select${Type}>;
|
|
2694
2718
|
orderBy?: string | string[];
|
|
2695
2719
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
2720
|
+
distinctOn?: string | string[];
|
|
2696
2721
|
${paramName}?: {
|
|
2697
2722
|
select?: string[];
|
|
2698
2723
|
exclude?: string[];
|
|
@@ -5255,6 +5280,12 @@ function getVectorDistanceOperator(metric?: string): string {
|
|
|
5255
5280
|
}
|
|
5256
5281
|
}
|
|
5257
5282
|
|
|
5283
|
+
/** Builds a SQL ORDER BY clause from parallel cols/dirs arrays. Returns "" when cols is empty. */
|
|
5284
|
+
function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
|
|
5285
|
+
if (cols.length === 0) return "";
|
|
5286
|
+
return \`ORDER BY \${cols.map((c, i) => \`"\${c}" \${(dirs[i] ?? "asc").toUpperCase()}\`).join(", ")}\`;
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5258
5289
|
/**
|
|
5259
5290
|
* LIST operation - Get multiple records with optional filters and vector search
|
|
5260
5291
|
*/
|
|
@@ -5283,6 +5314,17 @@ export async function listRecords(
|
|
|
5283
5314
|
const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
|
|
5284
5315
|
const _distinctOnColsSQL = distinctCols ? distinctCols.map(c => '"' + c + '"').join(', ') : '';
|
|
5285
5316
|
|
|
5317
|
+
// Pre-compute user order cols (reused in ORDER BY block and useSubquery check)
|
|
5318
|
+
const userOrderCols: string[] = orderBy ? (Array.isArray(orderBy) ? orderBy : [orderBy]) : [];
|
|
5319
|
+
|
|
5320
|
+
// Auto-detect subquery form: needed when distinctOn is set AND the caller wants to order
|
|
5321
|
+
// by a column outside of distinctOn (inline DISTINCT ON can't satisfy that without silently
|
|
5322
|
+
// overriding the requested ordering). Vector search always stays inline.
|
|
5323
|
+
const useSubquery: boolean =
|
|
5324
|
+
distinctCols !== null &&
|
|
5325
|
+
!vector &&
|
|
5326
|
+
userOrderCols.some(col => !distinctCols.includes(col));
|
|
5327
|
+
|
|
5286
5328
|
// Get distance operator if vector search
|
|
5287
5329
|
const distanceOp = vector ? getVectorDistanceOperator(vector.metric) : "";
|
|
5288
5330
|
|
|
@@ -5348,41 +5390,35 @@ export async function listRecords(
|
|
|
5348
5390
|
|
|
5349
5391
|
// Build ORDER BY clause
|
|
5350
5392
|
let orderBySQL = "";
|
|
5393
|
+
const userDirs: ("asc" | "desc")[] = userOrderCols.length > 0
|
|
5394
|
+
? (Array.isArray(order) ? order : (order ? Array(userOrderCols.length).fill(order) : Array(userOrderCols.length).fill("asc")))
|
|
5395
|
+
: [];
|
|
5396
|
+
|
|
5351
5397
|
if (vector) {
|
|
5352
|
-
//
|
|
5398
|
+
// Vector search always orders by distance; DISTINCT ON + vector stays inline
|
|
5353
5399
|
orderBySQL = \`ORDER BY "\${vector.field}" \${distanceOp} ($1)::vector\`;
|
|
5354
|
-
} else {
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5400
|
+
} else if (useSubquery) {
|
|
5401
|
+
// Subquery form: outer query gets the user's full ORDER BY.
|
|
5402
|
+
// Inner query only needs to satisfy PG's DISTINCT ON prefix requirement (built at query assembly).
|
|
5403
|
+
orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
|
|
5404
|
+
} else if (distinctCols) {
|
|
5405
|
+
// Inline DISTINCT ON: prepend distinctCols as the leftmost ORDER BY prefix (PG requirement)
|
|
5360
5406
|
const finalCols: string[] = [];
|
|
5361
|
-
const finalDirs:
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
const userIdx = userCols.indexOf(col);
|
|
5367
|
-
finalCols.push(col);
|
|
5368
|
-
finalDirs.push(userIdx >= 0 ? (userDirs[userIdx] || "asc") : "asc");
|
|
5369
|
-
}
|
|
5370
|
-
// Append remaining user-specified cols not already covered by distinctOn
|
|
5371
|
-
for (let i = 0; i < userCols.length; i++) {
|
|
5372
|
-
if (!distinctCols.includes(userCols[i]!)) {
|
|
5373
|
-
finalCols.push(userCols[i]!);
|
|
5374
|
-
finalDirs.push(userDirs[i] || "asc");
|
|
5375
|
-
}
|
|
5376
|
-
}
|
|
5377
|
-
} else {
|
|
5378
|
-
finalCols.push(...userCols);
|
|
5379
|
-
finalDirs.push(...userDirs.map(d => d || "asc"));
|
|
5407
|
+
const finalDirs: ("asc" | "desc")[] = [];
|
|
5408
|
+
for (const col of distinctCols) {
|
|
5409
|
+
const userIdx = userOrderCols.indexOf(col);
|
|
5410
|
+
finalCols.push(col);
|
|
5411
|
+
finalDirs.push(userIdx >= 0 ? (userDirs[userIdx] ?? "asc") : "asc");
|
|
5380
5412
|
}
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5413
|
+
for (let i = 0; i < userOrderCols.length; i++) {
|
|
5414
|
+
if (!distinctCols.includes(userOrderCols[i]!)) {
|
|
5415
|
+
finalCols.push(userOrderCols[i]!);
|
|
5416
|
+
finalDirs.push(userDirs[i] ?? "asc");
|
|
5417
|
+
}
|
|
5385
5418
|
}
|
|
5419
|
+
orderBySQL = buildOrderBySQL(finalCols, finalDirs);
|
|
5420
|
+
} else {
|
|
5421
|
+
orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
|
|
5386
5422
|
}
|
|
5387
5423
|
|
|
5388
5424
|
// Add limit and offset params
|
|
@@ -5399,7 +5435,15 @@ export async function listRecords(
|
|
|
5399
5435
|
const total = parseInt(countResult.rows[0].count, 10);
|
|
5400
5436
|
|
|
5401
5437
|
// Get paginated data
|
|
5402
|
-
|
|
5438
|
+
let text: string;
|
|
5439
|
+
if (useSubquery) {
|
|
5440
|
+
// Inner query: DISTINCT ON with only the distinctCols ORDER BY prefix (PG requirement).
|
|
5441
|
+
// Outer query: free ORDER BY from the user's full orderBy list, plus LIMIT/OFFSET.
|
|
5442
|
+
const innerQuery = \`SELECT DISTINCT ON (\${_distinctOnColsSQL}) \${baseColumns} FROM "\${ctx.table}" \${whereSQL} ORDER BY \${_distinctOnColsSQL}\`;
|
|
5443
|
+
text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
5444
|
+
} else {
|
|
5445
|
+
text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
5446
|
+
}
|
|
5403
5447
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
5404
5448
|
|
|
5405
5449
|
const { rows } = await ctx.pg.query(text, allParams);
|
|
@@ -6286,7 +6330,7 @@ init_emit_sdk_contract();
|
|
|
6286
6330
|
init_utils();
|
|
6287
6331
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
6288
6332
|
var __dirname2 = dirname2(__filename2);
|
|
6289
|
-
var { version: CLI_VERSION } = JSON.parse(readFileSync(
|
|
6333
|
+
var { version: CLI_VERSION } = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
|
|
6290
6334
|
async function generate(configPath) {
|
|
6291
6335
|
if (!existsSync2(configPath)) {
|
|
6292
6336
|
throw new Error(`Config file not found: ${configPath}
|
|
@@ -6319,26 +6363,26 @@ async function generate(configPath) {
|
|
|
6319
6363
|
const sameDirectory = serverDir === originalClientDir;
|
|
6320
6364
|
let clientDir = originalClientDir;
|
|
6321
6365
|
if (sameDirectory) {
|
|
6322
|
-
clientDir =
|
|
6366
|
+
clientDir = join2(originalClientDir, "sdk");
|
|
6323
6367
|
}
|
|
6324
6368
|
const serverFramework = cfg.serverFramework || "hono";
|
|
6325
6369
|
const generateTests = cfg.tests?.generate ?? false;
|
|
6326
6370
|
const originalTestDir = cfg.tests?.output || "./api/tests";
|
|
6327
6371
|
let testDir = originalTestDir;
|
|
6328
6372
|
if (generateTests && (originalTestDir === serverDir || originalTestDir === originalClientDir)) {
|
|
6329
|
-
testDir =
|
|
6373
|
+
testDir = join2(originalTestDir, "tests");
|
|
6330
6374
|
}
|
|
6331
6375
|
const testFramework = cfg.tests?.framework || "vitest";
|
|
6332
6376
|
console.log("\uD83D\uDCC1 Creating directories...");
|
|
6333
6377
|
const dirs = [
|
|
6334
6378
|
serverDir,
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6379
|
+
join2(serverDir, "types"),
|
|
6380
|
+
join2(serverDir, "zod"),
|
|
6381
|
+
join2(serverDir, "routes"),
|
|
6338
6382
|
clientDir,
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6383
|
+
join2(clientDir, "types"),
|
|
6384
|
+
join2(clientDir, "zod"),
|
|
6385
|
+
join2(clientDir, "params")
|
|
6342
6386
|
];
|
|
6343
6387
|
if (generateTests) {
|
|
6344
6388
|
dirs.push(testDir);
|
|
@@ -6346,28 +6390,28 @@ async function generate(configPath) {
|
|
|
6346
6390
|
await ensureDirs(dirs);
|
|
6347
6391
|
const files = [];
|
|
6348
6392
|
const includeSpec = emitIncludeSpec(graph);
|
|
6349
|
-
files.push({ path:
|
|
6350
|
-
files.push({ path:
|
|
6393
|
+
files.push({ path: join2(serverDir, "include-spec.ts"), content: includeSpec });
|
|
6394
|
+
files.push({ path: join2(clientDir, "include-spec.ts"), content: includeSpec });
|
|
6351
6395
|
const includeResolver = emitIncludeResolver(graph, cfg.useJsExtensions);
|
|
6352
|
-
files.push({ path:
|
|
6353
|
-
files.push({ path:
|
|
6354
|
-
files.push({ path:
|
|
6355
|
-
files.push({ path:
|
|
6356
|
-
files.push({ path:
|
|
6396
|
+
files.push({ path: join2(clientDir, "include-resolver.ts"), content: includeResolver });
|
|
6397
|
+
files.push({ path: join2(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
|
|
6398
|
+
files.push({ path: join2(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
|
|
6399
|
+
files.push({ path: join2(clientDir, "base-client.ts"), content: emitBaseClient() });
|
|
6400
|
+
files.push({ path: join2(clientDir, "where-types.ts"), content: emitWhereTypes() });
|
|
6357
6401
|
files.push({
|
|
6358
|
-
path:
|
|
6402
|
+
path: join2(serverDir, "include-builder.ts"),
|
|
6359
6403
|
content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
|
|
6360
6404
|
});
|
|
6361
6405
|
files.push({
|
|
6362
|
-
path:
|
|
6406
|
+
path: join2(serverDir, "include-loader.ts"),
|
|
6363
6407
|
content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
|
|
6364
6408
|
});
|
|
6365
|
-
files.push({ path:
|
|
6409
|
+
files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
|
|
6366
6410
|
if (getAuthStrategy(normalizedAuth) !== "none") {
|
|
6367
|
-
files.push({ path:
|
|
6411
|
+
files.push({ path: join2(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
|
|
6368
6412
|
}
|
|
6369
6413
|
files.push({
|
|
6370
|
-
path:
|
|
6414
|
+
path: join2(serverDir, "core", "operations.ts"),
|
|
6371
6415
|
content: emitCoreOperations()
|
|
6372
6416
|
});
|
|
6373
6417
|
if (process.env.SDK_DEBUG) {
|
|
@@ -6376,13 +6420,13 @@ async function generate(configPath) {
|
|
|
6376
6420
|
for (const table of Object.values(model.tables)) {
|
|
6377
6421
|
const numericMode = cfg.numericMode ?? "auto";
|
|
6378
6422
|
const typesSrc = emitTypes(table, { numericMode }, model.enums);
|
|
6379
|
-
files.push({ path:
|
|
6380
|
-
files.push({ path:
|
|
6423
|
+
files.push({ path: join2(serverDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
6424
|
+
files.push({ path: join2(clientDir, "types", `${table.name}.ts`), content: typesSrc });
|
|
6381
6425
|
const zodSrc = emitZod(table, { numericMode }, model.enums);
|
|
6382
|
-
files.push({ path:
|
|
6383
|
-
files.push({ path:
|
|
6426
|
+
files.push({ path: join2(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
6427
|
+
files.push({ path: join2(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
|
|
6384
6428
|
const paramsZodSrc = emitParamsZod(table, graph);
|
|
6385
|
-
files.push({ path:
|
|
6429
|
+
files.push({ path: join2(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
|
|
6386
6430
|
let routeContent;
|
|
6387
6431
|
if (serverFramework === "hono") {
|
|
6388
6432
|
routeContent = emitHonoRoutes(table, graph, {
|
|
@@ -6396,11 +6440,11 @@ async function generate(configPath) {
|
|
|
6396
6440
|
throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
|
|
6397
6441
|
}
|
|
6398
6442
|
files.push({
|
|
6399
|
-
path:
|
|
6443
|
+
path: join2(serverDir, "routes", `${table.name}.ts`),
|
|
6400
6444
|
content: routeContent
|
|
6401
6445
|
});
|
|
6402
6446
|
files.push({
|
|
6403
|
-
path:
|
|
6447
|
+
path: join2(clientDir, `${table.name}.ts`),
|
|
6404
6448
|
content: emitClient(table, graph, {
|
|
6405
6449
|
useJsExtensions: cfg.useJsExtensionsClient,
|
|
6406
6450
|
includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
|
|
@@ -6409,12 +6453,12 @@ async function generate(configPath) {
|
|
|
6409
6453
|
});
|
|
6410
6454
|
}
|
|
6411
6455
|
files.push({
|
|
6412
|
-
path:
|
|
6456
|
+
path: join2(clientDir, "index.ts"),
|
|
6413
6457
|
content: emitClientIndex(Object.values(model.tables), cfg.useJsExtensionsClient, graph, { maxDepth: cfg.includeMethodsDepth ?? 2, skipJunctionTables: cfg.skipJunctionTables ?? true })
|
|
6414
6458
|
});
|
|
6415
6459
|
if (serverFramework === "hono") {
|
|
6416
6460
|
files.push({
|
|
6417
|
-
path:
|
|
6461
|
+
path: join2(serverDir, "router.ts"),
|
|
6418
6462
|
content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions, cfg.pullToken)
|
|
6419
6463
|
});
|
|
6420
6464
|
}
|
|
@@ -6424,63 +6468,88 @@ async function generate(configPath) {
|
|
|
6424
6468
|
}
|
|
6425
6469
|
const contract = generateUnifiedContract2(model, cfg, graph);
|
|
6426
6470
|
files.push({
|
|
6427
|
-
path:
|
|
6471
|
+
path: join2(serverDir, "CONTRACT.md"),
|
|
6428
6472
|
content: generateUnifiedContractMarkdown2(contract)
|
|
6429
6473
|
});
|
|
6430
6474
|
files.push({
|
|
6431
|
-
path:
|
|
6475
|
+
path: join2(clientDir, "CONTRACT.md"),
|
|
6432
6476
|
content: generateUnifiedContractMarkdown2(contract)
|
|
6433
6477
|
});
|
|
6434
6478
|
const contractCode = emitUnifiedContract(model, cfg, graph);
|
|
6435
6479
|
files.push({
|
|
6436
|
-
path:
|
|
6480
|
+
path: join2(serverDir, "contract.ts"),
|
|
6437
6481
|
content: contractCode
|
|
6438
6482
|
});
|
|
6439
6483
|
const clientFiles = files.filter((f) => {
|
|
6440
6484
|
return f.path.includes(clientDir);
|
|
6441
6485
|
});
|
|
6442
6486
|
files.push({
|
|
6443
|
-
path:
|
|
6487
|
+
path: join2(serverDir, "sdk-bundle.ts"),
|
|
6444
6488
|
content: emitSdkBundle(clientFiles, clientDir, CLI_VERSION)
|
|
6445
6489
|
});
|
|
6446
6490
|
if (generateTests) {
|
|
6447
6491
|
console.log("\uD83E\uDDEA Generating tests...");
|
|
6448
6492
|
const relativeClientPath = relative(testDir, clientDir);
|
|
6449
6493
|
files.push({
|
|
6450
|
-
path:
|
|
6494
|
+
path: join2(testDir, "setup.ts"),
|
|
6451
6495
|
content: emitTestSetup(relativeClientPath, testFramework)
|
|
6452
6496
|
});
|
|
6453
6497
|
files.push({
|
|
6454
|
-
path:
|
|
6498
|
+
path: join2(testDir, "docker-compose.yml"),
|
|
6455
6499
|
content: emitDockerCompose()
|
|
6456
6500
|
});
|
|
6457
6501
|
files.push({
|
|
6458
|
-
path:
|
|
6502
|
+
path: join2(testDir, "run-tests.sh"),
|
|
6459
6503
|
content: emitTestScript(testFramework, testDir)
|
|
6460
6504
|
});
|
|
6461
6505
|
files.push({
|
|
6462
|
-
path:
|
|
6506
|
+
path: join2(testDir, ".gitignore"),
|
|
6463
6507
|
content: emitTestGitignore()
|
|
6464
6508
|
});
|
|
6465
6509
|
if (testFramework === "vitest") {
|
|
6466
6510
|
files.push({
|
|
6467
|
-
path:
|
|
6511
|
+
path: join2(testDir, "vitest.config.ts"),
|
|
6468
6512
|
content: emitVitestConfig()
|
|
6469
6513
|
});
|
|
6470
6514
|
}
|
|
6471
6515
|
for (const table of Object.values(model.tables)) {
|
|
6472
6516
|
files.push({
|
|
6473
|
-
path:
|
|
6517
|
+
path: join2(testDir, `${table.name}.test.ts`),
|
|
6474
6518
|
content: emitTableTest(table, model, relativeClientPath, testFramework)
|
|
6475
6519
|
});
|
|
6476
6520
|
}
|
|
6477
6521
|
}
|
|
6478
6522
|
console.log("✍️ Writing files...");
|
|
6479
6523
|
const writeResult = await writeFilesIfChanged(files);
|
|
6480
|
-
|
|
6524
|
+
let deleteResult = { deleted: 0, filesDeleted: [] };
|
|
6525
|
+
if (cfg.clean !== false) {
|
|
6526
|
+
const dirsToScan = [
|
|
6527
|
+
serverDir,
|
|
6528
|
+
join2(serverDir, "types"),
|
|
6529
|
+
join2(serverDir, "zod"),
|
|
6530
|
+
join2(serverDir, "routes"),
|
|
6531
|
+
join2(serverDir, "core"),
|
|
6532
|
+
clientDir,
|
|
6533
|
+
join2(clientDir, "types"),
|
|
6534
|
+
join2(clientDir, "zod"),
|
|
6535
|
+
join2(clientDir, "params")
|
|
6536
|
+
];
|
|
6537
|
+
if (generateTests)
|
|
6538
|
+
dirsToScan.push(testDir);
|
|
6539
|
+
const generatedPaths = new Set(files.map((f) => f.path));
|
|
6540
|
+
deleteResult = await deleteStaleFiles(generatedPaths, dirsToScan);
|
|
6541
|
+
}
|
|
6542
|
+
if (writeResult.written === 0 && deleteResult.deleted === 0) {
|
|
6481
6543
|
console.log(`✅ All ${writeResult.unchanged} files up-to-date (no changes)`);
|
|
6482
6544
|
} else {
|
|
6483
|
-
|
|
6545
|
+
const parts = [];
|
|
6546
|
+
if (writeResult.written > 0)
|
|
6547
|
+
parts.push(`updated ${writeResult.written} files`);
|
|
6548
|
+
if (deleteResult.deleted > 0)
|
|
6549
|
+
parts.push(`deleted ${deleteResult.deleted} stale files`);
|
|
6550
|
+
if (writeResult.unchanged > 0)
|
|
6551
|
+
parts.push(`${writeResult.unchanged} unchanged`);
|
|
6552
|
+
console.log(`✅ ${parts.join(", ")}`);
|
|
6484
6553
|
}
|
|
6485
6554
|
console.log(` Server: ${serverDir}`);
|
|
6486
6555
|
console.log(` Client: ${sameDirectory ? clientDir + " (in sdk subdir due to same output dir)" : clientDir}`);
|
package/dist/types.d.ts
CHANGED
package/dist/utils.d.ts
CHANGED
|
@@ -20,3 +20,11 @@ export declare function writeFilesIfChanged(files: Array<{
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function hashContent(content: string): string;
|
|
22
22
|
export declare function ensureDirs(dirs: string[]): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Delete files in the given directories that are not in the set of generated paths.
|
|
25
|
+
* Used to remove stale files for tables that no longer exist in the schema.
|
|
26
|
+
*/
|
|
27
|
+
export declare function deleteStaleFiles(generatedPaths: Set<string>, dirsToScan: string[]): Promise<{
|
|
28
|
+
deleted: number;
|
|
29
|
+
filesDeleted: string[];
|
|
30
|
+
}>;
|