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 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 join2, dirname as dirname3, resolve as resolve2 } from "path";
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 = join2(config.output, path);
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 = join2(config.output, ".postgresdk.json");
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
- // For vector search, always order by distance
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
- const userCols = orderBy ? (Array.isArray(orderBy) ? orderBy : [orderBy]) : [];
6317
- const userDirs: ("asc" | "desc")[] = orderBy
6318
- ? (Array.isArray(order) ? order : (order ? Array(userCols.length).fill(order) : Array(userCols.length).fill("asc")))
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: string[] = [];
6323
-
6324
- if (distinctCols) {
6325
- // DISTINCT ON requires its columns to be the leftmost ORDER BY prefix
6326
- for (const col of distinctCols) {
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
- if (finalCols.length > 0) {
6344
- const orderParts = finalCols.map((c, i) => \`"\${c}" \${finalDirs[i]!.toUpperCase()}\`);
6345
- orderBySQL = \`ORDER BY \${orderParts.join(", ")}\`;
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
- const text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
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(join(__dirname2, "../package.json"), "utf-8"));
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 = join(originalClientDir, "sdk");
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 = join(originalTestDir, "tests");
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
- join(serverDir, "types"),
7297
- join(serverDir, "zod"),
7298
- join(serverDir, "routes"),
7340
+ join2(serverDir, "types"),
7341
+ join2(serverDir, "zod"),
7342
+ join2(serverDir, "routes"),
7299
7343
  clientDir,
7300
- join(clientDir, "types"),
7301
- join(clientDir, "zod"),
7302
- join(clientDir, "params")
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: join(serverDir, "include-spec.ts"), content: includeSpec });
7311
- files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
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: join(clientDir, "include-resolver.ts"), content: includeResolver });
7314
- files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
7315
- files.push({ path: join(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
7316
- files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
7317
- files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
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: join(serverDir, "include-builder.ts"),
7363
+ path: join2(serverDir, "include-builder.ts"),
7320
7364
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
7321
7365
  });
7322
7366
  files.push({
7323
- path: join(serverDir, "include-loader.ts"),
7367
+ path: join2(serverDir, "include-loader.ts"),
7324
7368
  content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
7325
7369
  });
7326
- files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
7370
+ files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
7327
7371
  if (getAuthStrategy(normalizedAuth) !== "none") {
7328
- files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
7372
+ files.push({ path: join2(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
7329
7373
  }
7330
7374
  files.push({
7331
- path: join(serverDir, "core", "operations.ts"),
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: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
7341
- files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
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: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
7344
- files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
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: join(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
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: join(serverDir, "routes", `${table.name}.ts`),
7404
+ path: join2(serverDir, "routes", `${table.name}.ts`),
7361
7405
  content: routeContent
7362
7406
  });
7363
7407
  files.push({
7364
- path: join(clientDir, `${table.name}.ts`),
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: join(clientDir, "index.ts"),
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: join(serverDir, "router.ts"),
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: join(serverDir, "CONTRACT.md"),
7432
+ path: join2(serverDir, "CONTRACT.md"),
7389
7433
  content: generateUnifiedContractMarkdown2(contract)
7390
7434
  });
7391
7435
  files.push({
7392
- path: join(clientDir, "CONTRACT.md"),
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: join(serverDir, "contract.ts"),
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: join(serverDir, "sdk-bundle.ts"),
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: join(testDir, "setup.ts"),
7455
+ path: join2(testDir, "setup.ts"),
7412
7456
  content: emitTestSetup(relativeClientPath, testFramework)
7413
7457
  });
7414
7458
  files.push({
7415
- path: join(testDir, "docker-compose.yml"),
7459
+ path: join2(testDir, "docker-compose.yml"),
7416
7460
  content: emitDockerCompose()
7417
7461
  });
7418
7462
  files.push({
7419
- path: join(testDir, "run-tests.sh"),
7463
+ path: join2(testDir, "run-tests.sh"),
7420
7464
  content: emitTestScript(testFramework, testDir)
7421
7465
  });
7422
7466
  files.push({
7423
- path: join(testDir, ".gitignore"),
7467
+ path: join2(testDir, ".gitignore"),
7424
7468
  content: emitTestGitignore()
7425
7469
  });
7426
7470
  if (testFramework === "vitest") {
7427
7471
  files.push({
7428
- path: join(testDir, "vitest.config.ts"),
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: join(testDir, `${table.name}.test.ts`),
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
- if (writeResult.written === 0) {
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
- console.log(`✅ Updated ${writeResult.written} files, ${writeResult.unchanged} unchanged`);
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 join3 } from "node:path";
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(join3(__dirname3, "../package.json"), "utf-8"));
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
- // For vector search, always order by distance
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
- const userCols = orderBy ? (Array.isArray(orderBy) ? orderBy : [orderBy]) : [];
5356
- const userDirs: ("asc" | "desc")[] = orderBy
5357
- ? (Array.isArray(order) ? order : (order ? Array(userCols.length).fill(order) : Array(userCols.length).fill("asc")))
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: string[] = [];
5362
-
5363
- if (distinctCols) {
5364
- // DISTINCT ON requires its columns to be the leftmost ORDER BY prefix
5365
- for (const col of distinctCols) {
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
- if (finalCols.length > 0) {
5383
- const orderParts = finalCols.map((c, i) => \`"\${c}" \${finalDirs[i]!.toUpperCase()}\`);
5384
- orderBySQL = \`ORDER BY \${orderParts.join(", ")}\`;
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
- const text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
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(join(__dirname2, "../package.json"), "utf-8"));
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 = join(originalClientDir, "sdk");
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 = join(originalTestDir, "tests");
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
- join(serverDir, "types"),
6336
- join(serverDir, "zod"),
6337
- join(serverDir, "routes"),
6379
+ join2(serverDir, "types"),
6380
+ join2(serverDir, "zod"),
6381
+ join2(serverDir, "routes"),
6338
6382
  clientDir,
6339
- join(clientDir, "types"),
6340
- join(clientDir, "zod"),
6341
- join(clientDir, "params")
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: join(serverDir, "include-spec.ts"), content: includeSpec });
6350
- files.push({ path: join(clientDir, "include-spec.ts"), content: includeSpec });
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: join(clientDir, "include-resolver.ts"), content: includeResolver });
6353
- files.push({ path: join(clientDir, "params", "shared.ts"), content: emitSharedParamsZod() });
6354
- files.push({ path: join(clientDir, "types", "shared.ts"), content: emitSharedTypes() });
6355
- files.push({ path: join(clientDir, "base-client.ts"), content: emitBaseClient() });
6356
- files.push({ path: join(clientDir, "where-types.ts"), content: emitWhereTypes() });
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: join(serverDir, "include-builder.ts"),
6402
+ path: join2(serverDir, "include-builder.ts"),
6359
6403
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
6360
6404
  });
6361
6405
  files.push({
6362
- path: join(serverDir, "include-loader.ts"),
6406
+ path: join2(serverDir, "include-loader.ts"),
6363
6407
  content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
6364
6408
  });
6365
- files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
6409
+ files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
6366
6410
  if (getAuthStrategy(normalizedAuth) !== "none") {
6367
- files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
6411
+ files.push({ path: join2(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
6368
6412
  }
6369
6413
  files.push({
6370
- path: join(serverDir, "core", "operations.ts"),
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: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
6380
- files.push({ path: join(clientDir, "types", `${table.name}.ts`), content: typesSrc });
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: join(serverDir, "zod", `${table.name}.ts`), content: zodSrc });
6383
- files.push({ path: join(clientDir, "zod", `${table.name}.ts`), content: zodSrc });
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: join(clientDir, "params", `${table.name}.ts`), content: paramsZodSrc });
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: join(serverDir, "routes", `${table.name}.ts`),
6443
+ path: join2(serverDir, "routes", `${table.name}.ts`),
6400
6444
  content: routeContent
6401
6445
  });
6402
6446
  files.push({
6403
- path: join(clientDir, `${table.name}.ts`),
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: join(clientDir, "index.ts"),
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: join(serverDir, "router.ts"),
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: join(serverDir, "CONTRACT.md"),
6471
+ path: join2(serverDir, "CONTRACT.md"),
6428
6472
  content: generateUnifiedContractMarkdown2(contract)
6429
6473
  });
6430
6474
  files.push({
6431
- path: join(clientDir, "CONTRACT.md"),
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: join(serverDir, "contract.ts"),
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: join(serverDir, "sdk-bundle.ts"),
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: join(testDir, "setup.ts"),
6494
+ path: join2(testDir, "setup.ts"),
6451
6495
  content: emitTestSetup(relativeClientPath, testFramework)
6452
6496
  });
6453
6497
  files.push({
6454
- path: join(testDir, "docker-compose.yml"),
6498
+ path: join2(testDir, "docker-compose.yml"),
6455
6499
  content: emitDockerCompose()
6456
6500
  });
6457
6501
  files.push({
6458
- path: join(testDir, "run-tests.sh"),
6502
+ path: join2(testDir, "run-tests.sh"),
6459
6503
  content: emitTestScript(testFramework, testDir)
6460
6504
  });
6461
6505
  files.push({
6462
- path: join(testDir, ".gitignore"),
6506
+ path: join2(testDir, ".gitignore"),
6463
6507
  content: emitTestGitignore()
6464
6508
  });
6465
6509
  if (testFramework === "vitest") {
6466
6510
  files.push({
6467
- path: join(testDir, "vitest.config.ts"),
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: join(testDir, `${table.name}.test.ts`),
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
- if (writeResult.written === 0) {
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
- console.log(`✅ Updated ${writeResult.written} files, ${writeResult.unchanged} unchanged`);
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
@@ -34,6 +34,7 @@ export interface Config {
34
34
  pull?: PullConfig;
35
35
  useJsExtensions?: boolean;
36
36
  useJsExtensionsClient?: boolean;
37
+ clean?: boolean;
37
38
  tests?: {
38
39
  generate?: boolean;
39
40
  output?: string;
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
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.18.17",
3
+ "version": "0.18.19",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {